diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..e497dece --- /dev/null +++ b/.editorconfig @@ -0,0 +1,85 @@ + +# https://EditorConfig.org + +root = true + +[*.{json,cfg}] +indent_style = space +indent_size = 2 + +[*.{cs,py}] +indent_style = space +indent_size = 4 + +# Microsoft .NET properties +csharp_new_line_before_members_in_object_initializers = false +csharp_new_line_before_open_brace = all +csharp_preserve_single_line_blocks = true +csharp_space_after_cast = false + +# ReSharper properties +resharper_align_linq_query = true +resharper_align_multiline_binary_expressions_chain = false +resharper_align_multiline_expression_braces = true +resharper_align_multiline_for_stmt = true +resharper_align_multiline_parameter = true +resharper_align_multiple_declaration = true +resharper_align_tuple_components = true +resharper_allow_comment_after_lbrace = true +resharper_blank_lines_after_block_statements = 0 +resharper_blank_lines_after_start_comment = 0 +resharper_blank_lines_after_using_list = 0 +resharper_blank_lines_around_auto_property = 0 +resharper_blank_lines_around_class_definition = 0 +resharper_blank_lines_around_function_definition = 0 +resharper_blank_lines_around_local_method = 0 +resharper_blank_lines_around_property = 0 +resharper_blank_lines_around_single_line_type = 0 +resharper_case_block_braces = next_line_shifted_2 +resharper_continuous_line_indent = none +resharper_cpp_invocable_declaration_braces = end_of_line +resharper_cpp_keep_blank_lines_in_code = 100 +resharper_cpp_max_line_length = 256 +resharper_cpp_other_braces = end_of_line +resharper_cpp_type_declaration_braces = end_of_line +resharper_cpp_wrap_arguments_style = chop_if_long +resharper_csharp_blank_lines_around_field = 0 +resharper_csharp_blank_lines_around_invocable = 0 +resharper_csharp_blank_lines_around_namespace = 0 +resharper_csharp_blank_lines_around_region = 0 +resharper_csharp_blank_lines_around_type = 0 +resharper_csharp_indent_method_decl_pars = outside +resharper_csharp_insert_final_newline = true +resharper_csharp_keep_blank_lines_in_code = 100 +resharper_csharp_keep_blank_lines_in_declarations = 100 +resharper_csharp_max_line_length = 3874 +resharper_csharp_wrap_extends_list_style = chop_if_long +resharper_csharp_wrap_ternary_expr_style = wrap_if_long +resharper_disable_space_changes_before_trailing_comment = true +resharper_indent_comment = false +resharper_indent_nested_foreach_stmt = true +resharper_indent_nested_for_stmt = true +resharper_indent_pars = outside +resharper_indent_preprocessor_directives = do_not_change +resharper_indent_preprocessor_if = do_not_change +resharper_indent_preprocessor_region = do_not_change +resharper_int_align_comments = true +resharper_keep_existing_arrangement = true +resharper_max_array_initializer_elements_on_line = 2 +resharper_max_attribute_length_for_same_line = 10000 +resharper_place_expr_accessor_on_single_line = true +resharper_place_expr_method_on_single_line = true +resharper_place_expr_property_on_single_line = true +resharper_place_field_attribute_on_same_line = if_owner_is_single_line +resharper_place_simple_embedded_statement_on_same_line = false +resharper_place_simple_initializer_on_single_line = false +resharper_remove_blank_lines_near_braces_in_code = false +resharper_remove_blank_lines_near_braces_in_declarations = false +resharper_simple_embedded_statement_style = on_single_line +resharper_space_within_single_line_array_initializer_braces = true +resharper_toplevel_function_declaration_return_type_style = on_single_line +resharper_toplevel_function_definition_return_type_style = on_single_line +resharper_wrap_after_expression_lbrace = false +resharper_wrap_before_expression_rbrace = false +resharper_wrap_for_stmt_header_style = wrap_if_long +resharper_wrap_object_and_collection_initializer_style = chop_always diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..c61fd2e1 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +b5c9a69a0cc4f18d10f261e17d2a0eba49177ea1 +886f900ed7390e22ac6c8698eae06ad21d294309 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 360abd11..ed0174d5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -483,4 +483,3 @@ jobs: target_commitish: ${{ needs.configuration.outputs.currentrelease }} tag_name: ${{ needs.configuration.outputs.version }} files: releases/* - diff --git a/.github/workflows/export_secrets.yml b/.github/workflows/export_secrets.yml new file mode 100644 index 00000000..034f50b6 --- /dev/null +++ b/.github/workflows/export_secrets.yml @@ -0,0 +1,33 @@ +--- +# yamllint disable rule:line-length +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: Backup secrets (to OpenSSL encrypted file) +on: # yamllint disable-line rule:truthy + workflow_dispatch: + +jobs: + backup_secrets: + runs-on: ubuntu-latest + steps: + - name: Backup secrets + env: + SECRETS: ${{ toJSON(secrets) }} + VARS: ${{ toJSON(vars) }} + OPENSSL_ITER: 1000 + OPENSSL_PASS: ${{ secrets.SECRET_EXPORT_OPENSSL_PASSWORD }} + run: | + echo "$SECRETS" | tee secrets.txt + echo "$VARS" | tee vars.txt + openssl enc -aes-256-cbc -md sha512 -pbkdf2 -iter $OPENSSL_ITER -salt -in secrets.txt -out secrets.enc.txt -pass pass:$OPENSSL_PASS + openssl enc -aes-256-cbc -md sha512 -pbkdf2 -iter $OPENSSL_ITER -salt -in vars.txt -out vars.enc.txt -pass pass:$OPENSSL_PASS + echo "To decrypt the secrets, use the following command(s):" + echo "openssl enc -aes-256-cbc -d -md sha512 -pbkdf2 -iter $OPENSSL_ITER -salt -in secrets.enc.txt -out secrets.txt -pass pass:" + echo "openssl enc -aes-256-cbc -d -md sha512 -pbkdf2 -iter $OPENSSL_ITER -salt -in vars.enc.txt -out vars.txt -pass pass:" + + - name: Upload encrypted secrets + uses: actions/upload-artifact@v4 + with: + name: exports + path: | + secrets.enc.txt + vars.enc.txt diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..4a5f64a6 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,39 @@ +--- +# yamllint disable rule:line-length +name: pre-commit + +on: # yamllint disable-line rule:truthy + pull_request: + push: + branches: + - main # We never expect this to fail, since it must have passed on the pull request, but this will let us create a cache on main that other PRs can use, speeding up the process + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5.1.0 + with: + python-version: '3.12' + - uses: actions/setup-dotnet@v4.0.0 + with: + dotnet-version: '8.0.x' + - name: Install pre-commit + run: python -m pip install pre-commit + shell: bash + - name: Cache pre-commit environments + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} + - name: Setup pre-commit environments + run: pre-commit run + - name: Run pre-commit dotnet-format, with retries + uses: Wandalen/wretry.action@v3 + with: + command: pre-commit run dotnet-format --show-diff-on-failure --color=always --all-files || { git checkout -- . ; exit 1 ; } # In case dotnet-format fails, reset the changes it made. This way, we can differentiate between a NuGet failure and a real formatting issue + - name: Remove dotnet-format from the list of pre-commit jobs to run (since we already ran it) + run: yq eval 'del(.repos[] | select(.hooks[].id == "dotnet-format"))' -i .pre-commit-config.yaml + - name: Run the rest of pre-commit + run: pre-commit run --show-diff-on-failure --color=always --all-files diff --git a/.github/workflows/test_unity_credentials.yml b/.github/workflows/test_unity_credentials.yml new file mode 100644 index 00000000..3ee00c3f --- /dev/null +++ b/.github/workflows/test_unity_credentials.yml @@ -0,0 +1,22 @@ +--- +# yamllint disable rule:line-length +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: Test Unity Credentials +on: + workflow_dispatch: + +env: + UNITY_VERSION: "2021.3.30f1" + UNITY_EMAIL: ${{ vars.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + +jobs: + test_license: + runs-on: ubuntu-latest + steps: + - name: Unity - Activate + uses: game-ci/unity-activate@v2 + - name: Unity - Return License + uses: game-ci/unity-return-license@v2 + if: always() diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..506bf620 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,48 @@ +--- +repos: + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.35.1 + hooks: + - id: yamllint + - repo: https://github.com/psf/black + rev: 24.3.0 + hooks: + - id: black + files: ^Support/ + language_version: python3 + - repo: https://github.com/pycqa/flake8 + rev: 7.0.0 + hooks: + - id: flake8 + files: ^Support/ + - repo: https://github.com/PyCQA/pylint.git + rev: v3.1.0 + hooks: + - id: pylint + name: pylint + files: ^Support/ + language_version: python3 + additional_dependencies: + - typing_extensions + args: + - --load-plugins=pylint.extensions.redefined_variable_type,pylint.extensions.bad_builtin + - --disable=import-error + - repo: https://github.com/google/yamlfmt + rev: v0.11.0 + hooks: + - id: yamlfmt + args: + - -conf + - .yamlfmt + - repo: local + hooks: + # Use dotnet format already installed on your machine + - id: dotnet-format + name: dotnet-format + language: system + entry: dotnet format whitespace + types_or: [c#, vb] + exclude: ^(Assets/ThirdParty)|(Packages/)|(Assets/Photon/) + args: + - --folder + - --include diff --git a/.yamlfmt b/.yamlfmt new file mode 100644 index 00000000..fd7281cd --- /dev/null +++ b/.yamlfmt @@ -0,0 +1,6 @@ +--- +formatter: + include_document_start: true + indent: 2 + retain_line_breaks_single: true + pad_line_comments: 2 diff --git a/Assets/Editor/tests/model/controller/TouchPadLocationTest.cs b/Assets/Editor/tests/model/controller/TouchPadLocationTest.cs index 22b502f1..8abb2ffb 100644 --- a/Assets/Editor/tests/model/controller/TouchPadLocationTest.cs +++ b/Assets/Editor/tests/model/controller/TouchPadLocationTest.cs @@ -15,39 +15,44 @@ using NUnit.Framework; using UnityEngine; -namespace com.google.apps.peltzer.client.model.controller { +namespace com.google.apps.peltzer.client.model.controller +{ - [TestFixture] - // Tests for TouchpadLocation. - public class TouchpadLocationTest { - [Test] - public void TestBasicQuadrants() { - NUnit.Framework.Assert.IsTrue( - TouchpadLocation.RIGHT.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(.5f, 0)))); - NUnit.Framework.Assert.IsTrue( - TouchpadLocation.TOP.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(0, .5f)))); - NUnit.Framework.Assert.IsTrue( - TouchpadLocation.LEFT.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(-.5f, 0)))); - NUnit.Framework.Assert.IsTrue( - TouchpadLocation.BOTTOM.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(0, -.5f)))); - } + [TestFixture] + // Tests for TouchpadLocation. + public class TouchpadLocationTest + { + [Test] + public void TestBasicQuadrants() + { + NUnit.Framework.Assert.IsTrue( + TouchpadLocation.RIGHT.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(.5f, 0)))); + NUnit.Framework.Assert.IsTrue( + TouchpadLocation.TOP.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(0, .5f)))); + NUnit.Framework.Assert.IsTrue( + TouchpadLocation.LEFT.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(-.5f, 0)))); + NUnit.Framework.Assert.IsTrue( + TouchpadLocation.BOTTOM.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(0, -.5f)))); + } - [Test] - public void TestCenter() { - NUnit.Framework.Assert.IsTrue( - TouchpadLocation.CENTER.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(0, 0)))); - } + [Test] + public void TestCenter() + { + NUnit.Framework.Assert.IsTrue( + TouchpadLocation.CENTER.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(0, 0)))); + } - [Test] - public void TestXYCombinations() { - NUnit.Framework.Assert.IsTrue( - TouchpadLocation.TOP.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(.1f, .5f)))); - NUnit.Framework.Assert.IsTrue( - TouchpadLocation.BOTTOM.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(.1f, -.5f)))); - NUnit.Framework.Assert.IsTrue( - TouchpadLocation.BOTTOM.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(-.1f, -.5f)))); - NUnit.Framework.Assert.IsTrue( - TouchpadLocation.LEFT.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(-.5f, 0)))); + [Test] + public void TestXYCombinations() + { + NUnit.Framework.Assert.IsTrue( + TouchpadLocation.TOP.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(.1f, .5f)))); + NUnit.Framework.Assert.IsTrue( + TouchpadLocation.BOTTOM.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(.1f, -.5f)))); + NUnit.Framework.Assert.IsTrue( + TouchpadLocation.BOTTOM.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(-.1f, -.5f)))); + NUnit.Framework.Assert.IsTrue( + TouchpadLocation.LEFT.Equals(TouchpadLocationHelper.GetTouchpadLocation(new Vector2(-.5f, 0)))); + } } - } } diff --git a/Assets/Editor/tests/model/core/CommandTest.cs b/Assets/Editor/tests/model/core/CommandTest.cs index 538908e6..661c9097 100644 --- a/Assets/Editor/tests/model/core/CommandTest.cs +++ b/Assets/Editor/tests/model/core/CommandTest.cs @@ -18,149 +18,156 @@ using NUnit.Framework; using UnityEngine; -namespace com.google.apps.peltzer.client.model.core { - - [TestFixture] - // Tests for Command and implementations. - public class CommandTest { - - [Test] - public void TestCompositeMove() { - int meshOne = 1; - int meshTwo = 2; - - Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); - - // Add two meshes. - model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( - meshOne, Vector3.zero, Vector3.one, /* materialId */ 3))); - model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( - meshTwo, Vector3.one, Vector3.one, /* materialId */ 4))); - - // Check the meshes are where we think they are. - NUnit.Framework.Assert.Less( - Vector3.Distance(model.GetMesh(meshOne).offset, - new Vector3(0, 0, 0)), 0.001f); - NUnit.Framework.Assert.Less( - Vector3.Distance(model.GetMesh(meshTwo).offset, - new Vector3(1, 1, 1)), 0.001f); - - // Make and apply a CompositeCommand that moves both of them. - MoveMeshCommand move1 = new MoveMeshCommand( - meshOne, new Vector3(1, 0, 0), Quaternion.identity); - MoveMeshCommand move2 = new MoveMeshCommand( - meshTwo, new Vector3(0, 1, 0), Quaternion.identity); - List commands = new List(); - commands.Add(move1); - commands.Add(move2); - - model.ApplyCommand(new CompositeCommand(commands)); - - // Check the meshes' new locations. - NUnit.Framework.Assert.Less( - Vector3.Distance(model.GetMesh(meshOne).offset, - new Vector3(1, 0, 0)), 0.001f); - NUnit.Framework.Assert.Less( - Vector3.Distance(model.GetMesh(meshTwo).offset, - new Vector3(1, 2, 1)), 0.001f); - - // Undo and make sure they are back. - model.Undo(); - NUnit.Framework.Assert.Less( - Vector3.Distance(model.GetMesh(meshOne).offset, - new Vector3(0, 0, 0)), 0.001f); - NUnit.Framework.Assert.Less( - Vector3.Distance(model.GetMesh(meshTwo).offset, - new Vector3(1, 1, 1)), 0.001f); +namespace com.google.apps.peltzer.client.model.core +{ + + [TestFixture] + // Tests for Command and implementations. + public class CommandTest + { + + [Test] + public void TestCompositeMove() + { + int meshOne = 1; + int meshTwo = 2; + + Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); + + // Add two meshes. + model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( + meshOne, Vector3.zero, Vector3.one, /* materialId */ 3))); + model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( + meshTwo, Vector3.one, Vector3.one, /* materialId */ 4))); + + // Check the meshes are where we think they are. + NUnit.Framework.Assert.Less( + Vector3.Distance(model.GetMesh(meshOne).offset, + new Vector3(0, 0, 0)), 0.001f); + NUnit.Framework.Assert.Less( + Vector3.Distance(model.GetMesh(meshTwo).offset, + new Vector3(1, 1, 1)), 0.001f); + + // Make and apply a CompositeCommand that moves both of them. + MoveMeshCommand move1 = new MoveMeshCommand( + meshOne, new Vector3(1, 0, 0), Quaternion.identity); + MoveMeshCommand move2 = new MoveMeshCommand( + meshTwo, new Vector3(0, 1, 0), Quaternion.identity); + List commands = new List(); + commands.Add(move1); + commands.Add(move2); + + model.ApplyCommand(new CompositeCommand(commands)); + + // Check the meshes' new locations. + NUnit.Framework.Assert.Less( + Vector3.Distance(model.GetMesh(meshOne).offset, + new Vector3(1, 0, 0)), 0.001f); + NUnit.Framework.Assert.Less( + Vector3.Distance(model.GetMesh(meshTwo).offset, + new Vector3(1, 2, 1)), 0.001f); + + // Undo and make sure they are back. + model.Undo(); + NUnit.Framework.Assert.Less( + Vector3.Distance(model.GetMesh(meshOne).offset, + new Vector3(0, 0, 0)), 0.001f); + NUnit.Framework.Assert.Less( + Vector3.Distance(model.GetMesh(meshTwo).offset, + new Vector3(1, 1, 1)), 0.001f); + } + + [Test] + public void TestCompositeUndo() + { + int meshOne = 1; + + Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); + + // Add a Mesh. + model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( + meshOne, Vector3.zero, Vector3.one, /* materialId */ 3))); + + // Create a CompositeCommand that deletes and adds the same mesh. + List commands = new List(); + commands.Add(new DeleteMeshCommand(meshOne)); + commands.Add(new AddMeshCommand(Primitives.AxisAlignedBox( + meshOne, Vector3.zero, Vector3.one, /* materialId */ 3))); + + CompositeCommand composite = new CompositeCommand(commands); + + CompositeCommand undoCommand = + (CompositeCommand)composite.GetUndoCommand(model); + + List undoList = undoCommand.GetCommands(); + + // The order of commands should be the same, we invert the commands + // *and* invert the order. + NUnit.Framework.Assert.NotNull(undoList[1] as AddMeshCommand); + NUnit.Framework.Assert.NotNull(undoList[0] as DeleteMeshCommand); + } + + [Test] + public void TestRotation() + { + Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); + + MMesh mesh = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, /* materialId */ 2); + model.AddMesh(mesh); + + CheckBounds(model.GetMesh(1).bounds, new Bounds(Vector3.zero, Vector3.one * 2.0f)); + + MoveMeshCommand cmd = new MoveMeshCommand(1, Vector3.zero, Quaternion.AngleAxis(45, Vector3.up)); + model.ApplyCommand(cmd); + // Bounds should have grown, since the box is no longer axis-aligned. + float extends = 2.0f * Mathf.Sqrt(2); + CheckBounds(model.GetMesh(1).bounds, new Bounds(Vector3.zero, new Vector3(extends, 2.0f, extends))); + + model.Undo(); + // Make sure we are back: + CheckBounds(model.GetMesh(1).bounds, new Bounds(Vector3.zero, Vector3.one * 2.0f)); + } + + private void CheckBounds(Bounds left, Bounds right) + { + NUnit.Framework.Assert.Less(Vector3.Distance(left.center, right.center), 0.0001); + NUnit.Framework.Assert.Less(Vector3.Distance(left.extents, right.extents), 0.0001); + } + + [Test] + public void TestChangeFacePropertiesCommand() + { + Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); + + int baseMaterialId = 2; + int newMaterial1 = 3; + int newMaterial2 = 4; + MMesh mesh = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, baseMaterialId); + model.AddMesh(mesh); + + Dictionary props = new Dictionary(); + props[1] = new FaceProperties(newMaterial1); + props[2] = new FaceProperties(newMaterial2); + + ChangeFacePropertiesCommand command = new ChangeFacePropertiesCommand(1, props); + + // Apply the command, ensure the mesh was updated. + model.ApplyCommand(command); + NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(0).properties.materialId, baseMaterialId); + NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(1).properties.materialId, newMaterial1); + NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(2).properties.materialId, newMaterial2); + + // Undo, faces should be back to normal. + model.Undo(); + NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(0).properties.materialId, baseMaterialId); + NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(1).properties.materialId, baseMaterialId); + NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(2).properties.materialId, baseMaterialId); + + // Redo, changes should be applied again. + model.Redo(); + NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(0).properties.materialId, baseMaterialId); + NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(1).properties.materialId, newMaterial1); + NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(2).properties.materialId, newMaterial2); + } } - - [Test] - public void TestCompositeUndo() { - int meshOne = 1; - - Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); - - // Add a Mesh. - model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( - meshOne, Vector3.zero, Vector3.one, /* materialId */ 3))); - - // Create a CompositeCommand that deletes and adds the same mesh. - List commands = new List(); - commands.Add(new DeleteMeshCommand(meshOne)); - commands.Add(new AddMeshCommand(Primitives.AxisAlignedBox( - meshOne, Vector3.zero, Vector3.one, /* materialId */ 3))); - - CompositeCommand composite = new CompositeCommand(commands); - - CompositeCommand undoCommand = - (CompositeCommand) composite.GetUndoCommand(model); - - List undoList = undoCommand.GetCommands(); - - // The order of commands should be the same, we invert the commands - // *and* invert the order. - NUnit.Framework.Assert.NotNull(undoList[1] as AddMeshCommand); - NUnit.Framework.Assert.NotNull(undoList[0] as DeleteMeshCommand); - } - - [Test] - public void TestRotation() { - Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); - - MMesh mesh = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, /* materialId */ 2); - model.AddMesh(mesh); - - CheckBounds(model.GetMesh(1).bounds, new Bounds(Vector3.zero, Vector3.one * 2.0f)); - - MoveMeshCommand cmd = new MoveMeshCommand(1, Vector3.zero, Quaternion.AngleAxis(45, Vector3.up)); - model.ApplyCommand(cmd); - // Bounds should have grown, since the box is no longer axis-aligned. - float extends = 2.0f * Mathf.Sqrt(2); - CheckBounds(model.GetMesh(1).bounds, new Bounds(Vector3.zero, new Vector3(extends, 2.0f, extends))); - - model.Undo(); - // Make sure we are back: - CheckBounds(model.GetMesh(1).bounds, new Bounds(Vector3.zero, Vector3.one * 2.0f)); - } - - private void CheckBounds(Bounds left, Bounds right) { - NUnit.Framework.Assert.Less(Vector3.Distance(left.center, right.center), 0.0001); - NUnit.Framework.Assert.Less(Vector3.Distance(left.extents, right.extents), 0.0001); - } - - [Test] - public void TestChangeFacePropertiesCommand() { - Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); - - int baseMaterialId = 2; - int newMaterial1 = 3; - int newMaterial2 = 4; - MMesh mesh = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, baseMaterialId); - model.AddMesh(mesh); - - Dictionary props = new Dictionary(); - props[1] = new FaceProperties(newMaterial1); - props[2] = new FaceProperties(newMaterial2); - - ChangeFacePropertiesCommand command = new ChangeFacePropertiesCommand(1, props); - - // Apply the command, ensure the mesh was updated. - model.ApplyCommand(command); - NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(0).properties.materialId, baseMaterialId); - NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(1).properties.materialId, newMaterial1); - NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(2).properties.materialId, newMaterial2); - - // Undo, faces should be back to normal. - model.Undo(); - NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(0).properties.materialId, baseMaterialId); - NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(1).properties.materialId, baseMaterialId); - NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(2).properties.materialId, baseMaterialId); - - // Redo, changes should be applied again. - model.Redo(); - NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(0).properties.materialId, baseMaterialId); - NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(1).properties.materialId, newMaterial1); - NUnit.Framework.Assert.AreEqual(model.GetMesh(1).GetFace(2).properties.materialId, newMaterial2); - } - } } diff --git a/Assets/Editor/tests/model/core/FaceTest.cs b/Assets/Editor/tests/model/core/FaceTest.cs index 3fb90fb9..d8dbb17d 100644 --- a/Assets/Editor/tests/model/core/FaceTest.cs +++ b/Assets/Editor/tests/model/core/FaceTest.cs @@ -17,33 +17,36 @@ using NUnit.Framework; using UnityEngine; -namespace com.google.apps.peltzer.client.model.core { +namespace com.google.apps.peltzer.client.model.core +{ - [TestFixture] - // Tests for Face. - public class FaceTest { + [TestFixture] + // Tests for Face. + public class FaceTest + { - [Test] - public void TestClone() { - List verts = new List(); - Vector3 norm = new Vector3(0, 1, 0); - FaceProperties props = new FaceProperties(47); + [Test] + public void TestClone() + { + List verts = new List(); + Vector3 norm = new Vector3(0, 1, 0); + FaceProperties props = new FaceProperties(47); - verts.Add(0); - verts.Add(1); + verts.Add(0); + verts.Add(1); - Face face = new Face(/* faceId */ 2, verts.AsReadOnly(), norm, props); + Face face = new Face(/* faceId */ 2, verts.AsReadOnly(), norm, props); - Face clone = face.Clone(); + Face clone = face.Clone(); - NUnit.Framework.Assert.AreSame(face.normal, clone.normal, - "Normals are immutable and should be shared."); - NUnit.Framework.Assert.AreSame(face.vertexIds, clone.vertexIds, - "Vertices are immutable and should be shared."); - NUnit.Framework.Assert.AreNotSame(face.properties, clone.properties); + NUnit.Framework.Assert.AreSame(face.normal, clone.normal, + "Normals are immutable and should be shared."); + NUnit.Framework.Assert.AreSame(face.vertexIds, clone.vertexIds, + "Vertices are immutable and should be shared."); + NUnit.Framework.Assert.AreNotSame(face.properties, clone.properties); - NUnit.Framework.Assert.AreEqual( - face.properties.materialId, clone.properties.materialId); + NUnit.Framework.Assert.AreEqual( + face.properties.materialId, clone.properties.materialId); + } } - } } diff --git a/Assets/Editor/tests/model/core/MMeshTest.cs b/Assets/Editor/tests/model/core/MMeshTest.cs index 56efb338..88932208 100644 --- a/Assets/Editor/tests/model/core/MMeshTest.cs +++ b/Assets/Editor/tests/model/core/MMeshTest.cs @@ -17,51 +17,56 @@ using UnityEngine; using NUnit.Framework; -namespace com.google.apps.peltzer.client.model.core { +namespace com.google.apps.peltzer.client.model.core +{ - [TestFixture] - // Tests for MMesh. - public class MMeshTest { + [TestFixture] + // Tests for MMesh. + public class MMeshTest + { - [Test] - public void TestClone() { - MMesh mesh = Primitives.AxisAlignedBox( - /* meshId */ 2, Vector3.zero, Vector3.one, /* materialId */ 1); + [Test] + public void TestClone() + { + MMesh mesh = Primitives.AxisAlignedBox( + /* meshId */ 2, Vector3.zero, Vector3.one, /* materialId */ 1); - MMesh clone = mesh.Clone(); + MMesh clone = mesh.Clone(); - NUnit.Framework.Assert.AreEqual(mesh.id, clone.id); + NUnit.Framework.Assert.AreEqual(mesh.id, clone.id); - NUnit.Framework.Assert.AreNotSame(mesh.GetFaces(), clone.GetFaces()); - NUnit.Framework.Assert.AreNotSame(mesh.GetFace(0), clone.GetFace(0)); + NUnit.Framework.Assert.AreNotSame(mesh.GetFaces(), clone.GetFaces()); + NUnit.Framework.Assert.AreNotSame(mesh.GetFace(0), clone.GetFace(0)); - NUnit.Framework.Assert.AreNotSame(mesh.GetVertices(), clone.GetVertices()); - int vertexTestId = mesh.GetVertexIds().GetEnumerator().Current; - NUnit.Framework.Assert.AreSame(mesh.GetVertex(vertexTestId), clone.GetVertex(vertexTestId), - "Vertices are immutable. They should be shared."); - } + NUnit.Framework.Assert.AreNotSame(mesh.GetVertices(), clone.GetVertices()); + int vertexTestId = mesh.GetVertexIds().GetEnumerator().Current; + NUnit.Framework.Assert.AreSame(mesh.GetVertex(vertexTestId), clone.GetVertex(vertexTestId), + "Vertices are immutable. They should be shared."); + } - [Test] - public void TestBounds() { - MMesh mesh = Primitives.AxisAlignedBox( - /* meshId */ 2, Vector3.zero, Vector3.one, /* materialId */ 1); + [Test] + public void TestBounds() + { + MMesh mesh = Primitives.AxisAlignedBox( + /* meshId */ 2, Vector3.zero, Vector3.one, /* materialId */ 1); - // Check the mesh bounds. - Bounds bounds = mesh.bounds; - AssertClose(bounds.center, Vector3.zero); - AssertClose(bounds.extents, Vector3.one); + // Check the mesh bounds. + Bounds bounds = mesh.bounds; + AssertClose(bounds.center, Vector3.zero); + AssertClose(bounds.extents, Vector3.one); - // Move the mesh and recheck. - mesh.offset += new Vector3(1, 0, 0); - mesh.RecalcBounds(); - bounds = mesh.bounds; - AssertClose(bounds.center, new Vector3(1, 0, 0)); - AssertClose(bounds.extents, Vector3.one); - } + // Move the mesh and recheck. + mesh.offset += new Vector3(1, 0, 0); + mesh.RecalcBounds(); + bounds = mesh.bounds; + AssertClose(bounds.center, new Vector3(1, 0, 0)); + AssertClose(bounds.extents, Vector3.one); + } - private void AssertClose(Vector3 left, Vector3 right) { - NUnit.Framework.Assert.True(Vector3.Distance(left, right) < 0.001f, - left + " was not similar to " + right); + private void AssertClose(Vector3 left, Vector3 right) + { + NUnit.Framework.Assert.True(Vector3.Distance(left, right) < 0.001f, + left + " was not similar to " + right); + } } - } } diff --git a/Assets/Editor/tests/model/core/MeshMathTest.cs b/Assets/Editor/tests/model/core/MeshMathTest.cs index 150e1566..1b6f688f 100644 --- a/Assets/Editor/tests/model/core/MeshMathTest.cs +++ b/Assets/Editor/tests/model/core/MeshMathTest.cs @@ -18,31 +18,35 @@ using UnityEngine; using System.Linq; -namespace com.google.apps.peltzer.client.model.core { - - [TestFixture] - // Tests for mesh math. - public class MeshMathTest { - - [Test] - public void TestCalculateNormalForTriangle() { - List simpleTriangle = new List() { new Vector3(0, 0), new Vector3(0, 1), new Vector3(1, 0) }; - Vector3 normal = new Vector3(0, 0, -1); - NUnit.Framework.Assert.Less((normal - MeshMath.CalculateNormal(simpleTriangle)).magnitude, .0001f); - - // move the triangle and normal to some crazy place, should still calculate correctly - Matrix4x4 transformMatrix = Matrix4x4.TRS(new Vector3(3, -2, 7), Quaternion.Euler(13, 7.5f, 20), new Vector3(.4f, 9, 9)); - List transformedTriangle = simpleTriangle.Select(v => transformMatrix.MultiplyPoint(v)).ToList(); - Vector3 transformedNormal = transformMatrix.MultiplyVector(normal).normalized; - Vector3 newNormal = MeshMath.CalculateNormal(transformedTriangle); - - NUnit.Framework.Assert.Less((transformedNormal - newNormal).magnitude, .0001f); - } - - [Test] - public void TestCalculateNormalForConvex() { - // Regular hex inscibed in a cube using midpoints of cube's edges. - List hex = new List() { +namespace com.google.apps.peltzer.client.model.core +{ + + [TestFixture] + // Tests for mesh math. + public class MeshMathTest + { + + [Test] + public void TestCalculateNormalForTriangle() + { + List simpleTriangle = new List() { new Vector3(0, 0), new Vector3(0, 1), new Vector3(1, 0) }; + Vector3 normal = new Vector3(0, 0, -1); + NUnit.Framework.Assert.Less((normal - MeshMath.CalculateNormal(simpleTriangle)).magnitude, .0001f); + + // move the triangle and normal to some crazy place, should still calculate correctly + Matrix4x4 transformMatrix = Matrix4x4.TRS(new Vector3(3, -2, 7), Quaternion.Euler(13, 7.5f, 20), new Vector3(.4f, 9, 9)); + List transformedTriangle = simpleTriangle.Select(v => transformMatrix.MultiplyPoint(v)).ToList(); + Vector3 transformedNormal = transformMatrix.MultiplyVector(normal).normalized; + Vector3 newNormal = MeshMath.CalculateNormal(transformedTriangle); + + NUnit.Framework.Assert.Less((transformedNormal - newNormal).magnitude, .0001f); + } + + [Test] + public void TestCalculateNormalForConvex() + { + // Regular hex inscibed in a cube using midpoints of cube's edges. + List hex = new List() { new Vector3(1, 0, 1), new Vector3(0, -1, 1), new Vector3(-1, -1, 0), @@ -50,15 +54,16 @@ public void TestCalculateNormalForConvex() { new Vector3(0, 1, -1), new Vector3(1, 1, 0) }; - // Conveniently, the normal of this hex is along the diagonal of the cube. - Vector3 normal = new Vector3(1, -1, -1).normalized; - NUnit.Framework.Assert.Less((normal - MeshMath.CalculateNormal(hex)).magnitude, .0001f); - } - - [Test] - public void TestCalculateNormalForConcave() { - // Bring every other vertex towards the center of the cube, making the shape concave. - List sortaHex = new List() { + // Conveniently, the normal of this hex is along the diagonal of the cube. + Vector3 normal = new Vector3(1, -1, -1).normalized; + NUnit.Framework.Assert.Less((normal - MeshMath.CalculateNormal(hex)).magnitude, .0001f); + } + + [Test] + public void TestCalculateNormalForConcave() + { + // Bring every other vertex towards the center of the cube, making the shape concave. + List sortaHex = new List() { new Vector3(.1f, 0, .1f), new Vector3(0, -1, 1), new Vector3(-.1f, -.1f, 0), @@ -66,40 +71,42 @@ public void TestCalculateNormalForConcave() { new Vector3(0, .1f, -.1f), new Vector3(1, 1, 0) }; - Vector3 normal = new Vector3(1, -1, -1).normalized; - // Reflex vertices should not affect resulting normal. - NUnit.Framework.Assert.Less((normal - MeshMath.CalculateNormal(sortaHex)).magnitude, .0001f); - } - - [Test] - public void TestIsCloseToFace() { - const float kFaceClosenessThreshold = 0.0001f; - const float kVertexDistanceThreshold = 0.0001f; - List simpleTriangle = new List() { new Vector3(0, 0), new Vector3(0, 1), new Vector3(1, 0) }; - Vector3 onTriangle = new Vector3(.1f, .1f); - NUnit.Framework.Assert.True(MeshMath.IsCloseToFaceInterior(onTriangle, MeshMath.CalculateNormal(simpleTriangle), simpleTriangle, - kFaceClosenessThreshold, kVertexDistanceThreshold)); - - Vector3 offTriangle = new Vector3(.51f, .51f); - NUnit.Framework.Assert.False(MeshMath.IsCloseToFaceInterior(offTriangle, MeshMath.CalculateNormal(simpleTriangle), simpleTriangle, - kFaceClosenessThreshold, kVertexDistanceThreshold)); - - Vector3 slightlyAbove = new Vector3(.1f, .1f, .0002f); - NUnit.Framework.Assert.False(MeshMath.IsCloseToFaceInterior(slightlyAbove, MeshMath.CalculateNormal(simpleTriangle), simpleTriangle, - kFaceClosenessThreshold, kVertexDistanceThreshold)); - - Vector3 closeEnough = new Vector3(.1f, .1f, .00005f); - NUnit.Framework.Assert.True(MeshMath.IsCloseToFaceInterior(closeEnough, MeshMath.CalculateNormal(simpleTriangle), simpleTriangle, - kFaceClosenessThreshold, kVertexDistanceThreshold)); - - Vector3 onVertex = new Vector3(1f, 0f, 0f); - NUnit.Framework.Assert.False(MeshMath.IsCloseToFaceInterior(onVertex, MeshMath.CalculateNormal(simpleTriangle), simpleTriangle, - kFaceClosenessThreshold, kVertexDistanceThreshold)); - } - - [Test] - public void TestFindCornerVertices() { - List coplanarVertices = new List() { + Vector3 normal = new Vector3(1, -1, -1).normalized; + // Reflex vertices should not affect resulting normal. + NUnit.Framework.Assert.Less((normal - MeshMath.CalculateNormal(sortaHex)).magnitude, .0001f); + } + + [Test] + public void TestIsCloseToFace() + { + const float kFaceClosenessThreshold = 0.0001f; + const float kVertexDistanceThreshold = 0.0001f; + List simpleTriangle = new List() { new Vector3(0, 0), new Vector3(0, 1), new Vector3(1, 0) }; + Vector3 onTriangle = new Vector3(.1f, .1f); + NUnit.Framework.Assert.True(MeshMath.IsCloseToFaceInterior(onTriangle, MeshMath.CalculateNormal(simpleTriangle), simpleTriangle, + kFaceClosenessThreshold, kVertexDistanceThreshold)); + + Vector3 offTriangle = new Vector3(.51f, .51f); + NUnit.Framework.Assert.False(MeshMath.IsCloseToFaceInterior(offTriangle, MeshMath.CalculateNormal(simpleTriangle), simpleTriangle, + kFaceClosenessThreshold, kVertexDistanceThreshold)); + + Vector3 slightlyAbove = new Vector3(.1f, .1f, .0002f); + NUnit.Framework.Assert.False(MeshMath.IsCloseToFaceInterior(slightlyAbove, MeshMath.CalculateNormal(simpleTriangle), simpleTriangle, + kFaceClosenessThreshold, kVertexDistanceThreshold)); + + Vector3 closeEnough = new Vector3(.1f, .1f, .00005f); + NUnit.Framework.Assert.True(MeshMath.IsCloseToFaceInterior(closeEnough, MeshMath.CalculateNormal(simpleTriangle), simpleTriangle, + kFaceClosenessThreshold, kVertexDistanceThreshold)); + + Vector3 onVertex = new Vector3(1f, 0f, 0f); + NUnit.Framework.Assert.False(MeshMath.IsCloseToFaceInterior(onVertex, MeshMath.CalculateNormal(simpleTriangle), simpleTriangle, + kFaceClosenessThreshold, kVertexDistanceThreshold)); + } + + [Test] + public void TestFindCornerVertices() + { + List coplanarVertices = new List() { new Vector3(1, 0, 0), new Vector3(1, 1, 0), new Vector3(0, 1, 0), @@ -109,43 +116,44 @@ public void TestFindCornerVertices() { new Vector3(0, -1, 0), new Vector3(1, -1, 0)}; - List expectedCornerVertices = new List() { + List expectedCornerVertices = new List() { new Vector3(1, 1, 0), new Vector3(-1, 1, 0), new Vector3(-1, -1, 0), new Vector3(1, -1, 0)}; - Assert.AreEqual(expectedCornerVertices, MeshMath.FindCornerVertices(coplanarVertices)); - } + Assert.AreEqual(expectedCornerVertices, MeshMath.FindCornerVertices(coplanarVertices)); + } - [Test] - public void TestFindClosestEdgeInFace() { - List vertexPositions = new List() { + [Test] + public void TestFindClosestEdgeInFace() + { + List vertexPositions = new List() { new Vector3(-1, -1, -1), new Vector3(-1, -1, 1), new Vector3(-1, 1, 1), new Vector3(-1, 1, -1)}; - Vector3 position = new Vector3(-1, 0.90f, 0f); - KeyValuePair expectedClosestEdge = - new KeyValuePair(new Vector3(-1, 1, 1), new Vector3(-1, 1, -1)); - KeyValuePair actualClosestEdge = MeshMath.FindClosestEdgeInFace(position, vertexPositions); - Assert.AreEqual(expectedClosestEdge, actualClosestEdge); - - position = new Vector3(-1, -0.90f, 0f); - expectedClosestEdge = new KeyValuePair(new Vector3(-1, -1, -1), new Vector3(-1, -1, 1)); - actualClosestEdge = MeshMath.FindClosestEdgeInFace(position, vertexPositions); - Assert.AreEqual(expectedClosestEdge, actualClosestEdge); - - position = new Vector3(-1, 0f, -.90f); - expectedClosestEdge = new KeyValuePair(new Vector3(-1, 1, -1), new Vector3(-1, -1, -1)); - actualClosestEdge = MeshMath.FindClosestEdgeInFace(position, vertexPositions); - Assert.AreEqual(expectedClosestEdge, actualClosestEdge); - - position = new Vector3(-1, 0f, .90f); - expectedClosestEdge = new KeyValuePair(new Vector3(-1, -1, 1), new Vector3(-1, 1, 1)); - actualClosestEdge = MeshMath.FindClosestEdgeInFace(position, vertexPositions); - Assert.AreEqual(expectedClosestEdge, actualClosestEdge); + Vector3 position = new Vector3(-1, 0.90f, 0f); + KeyValuePair expectedClosestEdge = + new KeyValuePair(new Vector3(-1, 1, 1), new Vector3(-1, 1, -1)); + KeyValuePair actualClosestEdge = MeshMath.FindClosestEdgeInFace(position, vertexPositions); + Assert.AreEqual(expectedClosestEdge, actualClosestEdge); + + position = new Vector3(-1, -0.90f, 0f); + expectedClosestEdge = new KeyValuePair(new Vector3(-1, -1, -1), new Vector3(-1, -1, 1)); + actualClosestEdge = MeshMath.FindClosestEdgeInFace(position, vertexPositions); + Assert.AreEqual(expectedClosestEdge, actualClosestEdge); + + position = new Vector3(-1, 0f, -.90f); + expectedClosestEdge = new KeyValuePair(new Vector3(-1, 1, -1), new Vector3(-1, -1, -1)); + actualClosestEdge = MeshMath.FindClosestEdgeInFace(position, vertexPositions); + Assert.AreEqual(expectedClosestEdge, actualClosestEdge); + + position = new Vector3(-1, 0f, .90f); + expectedClosestEdge = new KeyValuePair(new Vector3(-1, -1, 1), new Vector3(-1, 1, 1)); + actualClosestEdge = MeshMath.FindClosestEdgeInFace(position, vertexPositions); + Assert.AreEqual(expectedClosestEdge, actualClosestEdge); + } } - } } diff --git a/Assets/Editor/tests/model/core/MeshUtilTest.cs b/Assets/Editor/tests/model/core/MeshUtilTest.cs index 97233f5c..e9d99a2f 100644 --- a/Assets/Editor/tests/model/core/MeshUtilTest.cs +++ b/Assets/Editor/tests/model/core/MeshUtilTest.cs @@ -17,133 +17,141 @@ using NUnit.Framework; using UnityEngine; -namespace com.google.apps.peltzer.client.model.core { - [TestFixture] - public class MeshUtilTest { - [Test] - public void TestNonSplitSimple() { - // Create a three point triangle. - Vertex v1 = new Vertex(1, new Vector3(0, 0, 0)); - Vertex v2 = new Vertex(2, new Vector3(0, 0, 1)); - Vertex v3 = new Vertex(3, new Vector3(0, 1, 0)); - - Vector3 normal = new Vector3(1, 0, 0); - - Face face = new Face(1, - new List(new int[] { 1, 2, 3 }).AsReadOnly(), - normal, - new FaceProperties()); - - Dictionary vertById = new Dictionary(); - vertById[1] = v1; - vertById[2] = v2; - vertById[3] = v3; - Dictionary facesById = new Dictionary(); - facesById[1] = face; - - MMesh mesh = new MMesh(1, Vector3.zero, Quaternion.identity, vertById, facesById); - - // Face has only three verts, should never be split. - MMesh meshCopy = mesh.Clone(); - MMesh.GeometryOperation operation = meshCopy.StartOperation(); - operation.ModifyVertexMeshSpace(1, new Vector3(0.1f, 0, 0)); - MeshUtil.SplitFaceIfNeeded(operation, mesh.GetFace(1), 1); - operation.Commit(); - NUnit.Framework.Assert.AreEqual(1, mesh.faceCount, "Should not have split face."); - } - - [Test] - public void TestNonSplit() { - // Create a square in a plane. - Vertex v1 = new Vertex(1, new Vector3(0, 0, 0)); - Vertex v2 = new Vertex(2, new Vector3(0, 0, 1)); - Vertex v3 = new Vertex(3, new Vector3(0, 1, 1)); - Vertex v4 = new Vertex(4, new Vector3(0, 1, 0)); - - Vector3 normal = new Vector3(1, 0, 0); - - Face face = new Face(1, - new List(new int[] { 1, 2, 3, 4 }).AsReadOnly(), - normal, - new FaceProperties()); - - Dictionary vertById = new Dictionary(); - vertById[1] = v1; - vertById[2] = v2; - vertById[3] = v3; - vertById[4] = v4; - Dictionary facesById = new Dictionary(); - facesById[1] = face; - - MMesh mesh = new MMesh(1, Vector3.zero, Quaternion.identity, vertById, facesById); - - // Move first corner, but within the plane. - MMesh meshCopy = mesh.Clone(); - MMesh.GeometryOperation operation = meshCopy.StartOperation(); - operation.ModifyVertexMeshSpace(1, new Vector3(0, -1, -1)); - MeshUtil.SplitFaceIfNeeded(operation, mesh.GetFace(1), 1); - operation.Commit(); - NUnit.Framework.Assert.AreEqual(1, mesh.faceCount, "Should not have split face."); - } +namespace com.google.apps.peltzer.client.model.core +{ + [TestFixture] + public class MeshUtilTest + { + [Test] + public void TestNonSplitSimple() + { + // Create a three point triangle. + Vertex v1 = new Vertex(1, new Vector3(0, 0, 0)); + Vertex v2 = new Vertex(2, new Vector3(0, 0, 1)); + Vertex v3 = new Vertex(3, new Vector3(0, 1, 0)); + + Vector3 normal = new Vector3(1, 0, 0); + + Face face = new Face(1, + new List(new int[] { 1, 2, 3 }).AsReadOnly(), + normal, + new FaceProperties()); + + Dictionary vertById = new Dictionary(); + vertById[1] = v1; + vertById[2] = v2; + vertById[3] = v3; + Dictionary facesById = new Dictionary(); + facesById[1] = face; + + MMesh mesh = new MMesh(1, Vector3.zero, Quaternion.identity, vertById, facesById); + + // Face has only three verts, should never be split. + MMesh meshCopy = mesh.Clone(); + MMesh.GeometryOperation operation = meshCopy.StartOperation(); + operation.ModifyVertexMeshSpace(1, new Vector3(0.1f, 0, 0)); + MeshUtil.SplitFaceIfNeeded(operation, mesh.GetFace(1), 1); + operation.Commit(); + NUnit.Framework.Assert.AreEqual(1, mesh.faceCount, "Should not have split face."); + } - [Test] - public void TestSplit() { - int vertToMove = 1; - - // Create a square in a plane. - Vertex v1 = new Vertex(vertToMove, new Vector3(0, 0, 0)); - Vertex v2 = new Vertex(2, new Vector3(0, 0, 1)); - Vertex v3 = new Vertex(3, new Vector3(0, 1, 1)); - Vertex v4 = new Vertex(4, new Vector3(0, 1, 0)); - - Vector3 normal = new Vector3(1, 0, 0); - - Face face = new Face(1, - new List(new int[] { 1, 2, 3, 4 }).AsReadOnly(), - normal, - new FaceProperties()); - - Dictionary vertById = new Dictionary(); - vertById[vertToMove] = v1; - vertById[2] = v2; - vertById[3] = v3; - vertById[4] = v4; - Dictionary facesById = new Dictionary(); - facesById[1] = face; - - MMesh mesh = new MMesh(1, Vector3.zero, Quaternion.identity, vertById, facesById); - - // Move first corner, out of plane. - MMesh meshCopy = mesh.Clone(); - MMesh.GeometryOperation operation = meshCopy.StartOperation(); - operation.ModifyVertexMeshSpace(1, new Vector3(0.1f, 0, 0)); - MeshUtil.SplitFaceIfNeeded(operation, mesh.GetFace(1), 1); - operation.Commit(); - - NUnit.Framework.Assert.AreEqual(2, mesh.faceCount, "Should have split face."); - - // Make sure the vertex was removed from this face. - Face updatedFace = mesh.GetFace(1); - - string s = updatedFace.id + " "; - foreach (int n in updatedFace.vertexIds) { - s += n + ", "; - } - - NUnit.Framework.Assert.AreEqual(3, updatedFace.vertexIds.Count); - NUnit.Framework.Assert.False(updatedFace.vertexIds.Contains(vertToMove), "Vertex should have been removed: " + s); - - // Find the other face, it should contain the vert. - Face newFace = null; - foreach (Face f in mesh.GetFaces()) { - if (f.id != 1) { - newFace = f; + [Test] + public void TestNonSplit() + { + // Create a square in a plane. + Vertex v1 = new Vertex(1, new Vector3(0, 0, 0)); + Vertex v2 = new Vertex(2, new Vector3(0, 0, 1)); + Vertex v3 = new Vertex(3, new Vector3(0, 1, 1)); + Vertex v4 = new Vertex(4, new Vector3(0, 1, 0)); + + Vector3 normal = new Vector3(1, 0, 0); + + Face face = new Face(1, + new List(new int[] { 1, 2, 3, 4 }).AsReadOnly(), + normal, + new FaceProperties()); + + Dictionary vertById = new Dictionary(); + vertById[1] = v1; + vertById[2] = v2; + vertById[3] = v3; + vertById[4] = v4; + Dictionary facesById = new Dictionary(); + facesById[1] = face; + + MMesh mesh = new MMesh(1, Vector3.zero, Quaternion.identity, vertById, facesById); + + // Move first corner, but within the plane. + MMesh meshCopy = mesh.Clone(); + MMesh.GeometryOperation operation = meshCopy.StartOperation(); + operation.ModifyVertexMeshSpace(1, new Vector3(0, -1, -1)); + MeshUtil.SplitFaceIfNeeded(operation, mesh.GetFace(1), 1); + operation.Commit(); + NUnit.Framework.Assert.AreEqual(1, mesh.faceCount, "Should not have split face."); } - } - NUnit.Framework.Assert.NotNull(newFace); - NUnit.Framework.Assert.AreEqual(3, newFace.vertexIds.Count); - NUnit.Framework.Assert.True(newFace.vertexIds.Contains(vertToMove), "Vertex should be in new face."); + [Test] + public void TestSplit() + { + int vertToMove = 1; + + // Create a square in a plane. + Vertex v1 = new Vertex(vertToMove, new Vector3(0, 0, 0)); + Vertex v2 = new Vertex(2, new Vector3(0, 0, 1)); + Vertex v3 = new Vertex(3, new Vector3(0, 1, 1)); + Vertex v4 = new Vertex(4, new Vector3(0, 1, 0)); + + Vector3 normal = new Vector3(1, 0, 0); + + Face face = new Face(1, + new List(new int[] { 1, 2, 3, 4 }).AsReadOnly(), + normal, + new FaceProperties()); + + Dictionary vertById = new Dictionary(); + vertById[vertToMove] = v1; + vertById[2] = v2; + vertById[3] = v3; + vertById[4] = v4; + Dictionary facesById = new Dictionary(); + facesById[1] = face; + + MMesh mesh = new MMesh(1, Vector3.zero, Quaternion.identity, vertById, facesById); + + // Move first corner, out of plane. + MMesh meshCopy = mesh.Clone(); + MMesh.GeometryOperation operation = meshCopy.StartOperation(); + operation.ModifyVertexMeshSpace(1, new Vector3(0.1f, 0, 0)); + MeshUtil.SplitFaceIfNeeded(operation, mesh.GetFace(1), 1); + operation.Commit(); + + NUnit.Framework.Assert.AreEqual(2, mesh.faceCount, "Should have split face."); + + // Make sure the vertex was removed from this face. + Face updatedFace = mesh.GetFace(1); + + string s = updatedFace.id + " "; + foreach (int n in updatedFace.vertexIds) + { + s += n + ", "; + } + + NUnit.Framework.Assert.AreEqual(3, updatedFace.vertexIds.Count); + NUnit.Framework.Assert.False(updatedFace.vertexIds.Contains(vertToMove), "Vertex should have been removed: " + s); + + // Find the other face, it should contain the vert. + Face newFace = null; + foreach (Face f in mesh.GetFaces()) + { + if (f.id != 1) + { + newFace = f; + } + } + + NUnit.Framework.Assert.NotNull(newFace); + NUnit.Framework.Assert.AreEqual(3, newFace.vertexIds.Count); + NUnit.Framework.Assert.True(newFace.vertexIds.Contains(vertToMove), "Vertex should be in new face."); + } } - } } \ No newline at end of file diff --git a/Assets/Editor/tests/model/core/ModelTest.cs b/Assets/Editor/tests/model/core/ModelTest.cs index 4b78b921..12e0cee3 100644 --- a/Assets/Editor/tests/model/core/ModelTest.cs +++ b/Assets/Editor/tests/model/core/ModelTest.cs @@ -19,257 +19,273 @@ using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.model.core { - - [TestFixture] - // Tests for Model. - public class ModelTest { - - [Test] - public void TestAddMesh() { - Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); - int meshId = 1; - - // Add a mesh. - MMesh mesh = Primitives.AxisAlignedBox(meshId, Vector3.zero, Vector3.one, /* materialId */ 2); - - model.AddMesh(mesh); - - NUnit.Framework.Assert.AreEqual(mesh, model.GetMesh(meshId)); - NUnit.Framework.Assert.True(model.HasMesh(meshId)); - NUnit.Framework.Assert.AreEqual(1, model.GetNumberOfMeshes()); - - // Try to add a mesh with same id. Should fail. - try { - MMesh dupMesh = Primitives.AxisAlignedBox(meshId, Vector3.zero, Vector3.one, /* materialId */ 2); - model.AddMesh(dupMesh); - NUnit.Framework.Assert.IsTrue(false, "Expected exception"); - } catch (Exception) { - // Expected. - } - - // Ensure mesh was not updated. - NUnit.Framework.Assert.AreEqual(mesh, model.GetMesh(1)); - } - - [Test] - public void TestDeleteMesh() { - Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); - - int meshId = 1; - // Add a mesh. - MMesh mesh = Primitives.AxisAlignedBox(meshId, Vector3.zero, Vector3.one, /* materialId */ 2); - model.AddMesh(mesh); - NUnit.Framework.Assert.AreEqual(mesh, model.GetMesh(meshId)); - NUnit.Framework.Assert.AreEqual(1, model.GetNumberOfMeshes()); - - // Try to delete a non-existent mesh. Should fail. - try { - model.DeleteMesh(999); - NUnit.Framework.Assert.IsTrue(false, "Expected exception"); - } catch (Exception) { - // Expected - } - - // Delete the mesh. - model.DeleteMesh(meshId); - NUnit.Framework.Assert.False(model.HasMesh(meshId)); - NUnit.Framework.Assert.AreEqual(0, model.GetNumberOfMeshes()); - try { - model.GetMesh(meshId); - NUnit.Framework.Assert.IsTrue(false, "Expected exception"); - } catch (Exception) { - // Expected. - } - } - - [Test] - public void TestUndoRedo() { - int meshOne = 1; - int meshTwo = 2; - - Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); - NUnit.Framework.Assert.AreEqual(0, model.GetNumberOfMeshes(), - "Initial model should be empty."); - - AddMeshCommand command = new AddMeshCommand(Primitives.AxisAlignedBox( - meshOne, Vector3.zero, Vector3.one, /* materialId */ 3)); - - model.ApplyCommand(command); - NUnit.Framework.Assert.True(model.HasMesh(meshOne)); - - command = new AddMeshCommand(Primitives.AxisAlignedBox( - meshTwo, Vector3.one, Vector3.one, /* materialId */ 4)); - - model.ApplyCommand(command); - // Ensure both Meshes are in the model. - NUnit.Framework.Assert.True(model.HasMesh(meshOne)); - NUnit.Framework.Assert.True(model.HasMesh(meshTwo)); - - model.Undo(); - // Should have removed meshTwo. - NUnit.Framework.Assert.True(model.HasMesh(meshOne)); - NUnit.Framework.Assert.False(model.HasMesh(meshTwo)); - - model.Redo(); - // Both should be back. - NUnit.Framework.Assert.True(model.HasMesh(meshOne)); - NUnit.Framework.Assert.True(model.HasMesh(meshTwo)); - - model.Undo(); - model.Undo(); - // Both should be gone. - NUnit.Framework.Assert.False(model.HasMesh(meshOne)); - NUnit.Framework.Assert.False(model.HasMesh(meshTwo)); - - // Now add a new command. It should force the redo stack - // to clear. - model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( - meshOne, Vector3.zero, Vector3.one, /* materialId */ 3))); - - NUnit.Framework.Assert.IsFalse(model.Redo(), - "Expected redo stack to be empty"); - } - - [Test] - public void TestRemesher() { - int meshOne = 1; - int meshTwo = 2; - - Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); - - model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( - meshOne, Vector3.zero, Vector3.one, /* materialId */ 3))); - - model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( - meshTwo, Vector3.one, Vector3.one, /* materialId */ 4))); - - // Make sure the ReMesher knows about these. - ReMesher remesher = model.GetReMesher(); - NUnit.Framework.Assert.IsTrue(remesher.HasMesh(meshOne)); - NUnit.Framework.Assert.IsTrue(remesher.HasMesh(meshTwo)); - - // Hide one and make sure ReMesher is updated. - model.HideMeshForTestOrTutorial(meshOne); - NUnit.Framework.Assert.IsFalse(remesher.HasMesh(meshOne)); - NUnit.Framework.Assert.IsTrue(remesher.HasMesh(meshTwo)); - - // Unhide and check again. - model.UnhideMeshForTestOrTutorial(meshOne); - NUnit.Framework.Assert.IsTrue(remesher.HasMesh(meshOne)); - NUnit.Framework.Assert.IsTrue(remesher.HasMesh(meshTwo)); - } - - [Test] - public void TestGrouping() { - int meshOneId = 1; - int meshTwoId = 2; - int meshThreeId = 3; - int meshFourId = 4; - int meshFiveId = 5; - int meshSixId = 6; - - Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); - model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( - meshOneId, Vector3.zero, Vector3.one, /* materialId */ 3))); - model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( - meshTwoId, Vector3.one, Vector3.one, /* materialId */ 4))); - model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( - meshThreeId, Vector3.zero, Vector3.one, /* materialId */ 3))); - model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( - meshFourId, Vector3.one, Vector3.one, /* materialId */ 4))); - model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( - meshFiveId, Vector3.one, Vector3.one, /* materialId */ 4))); - model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( - meshSixId, Vector3.one, Vector3.one, /* materialId */ 4))); - - MMesh meshOne = model.GetMesh(meshOneId); - MMesh meshTwo = model.GetMesh(meshTwoId); - MMesh meshThree = model.GetMesh(meshThreeId); - MMesh meshFour = model.GetMesh(meshFourId); - MMesh meshFive = model.GetMesh(meshFiveId); - MMesh meshSix = model.GetMesh(meshSixId); - - // Group meshes one and two into one group. - int groupA = model.GenerateGroupId(); - model.SetMeshGroup(meshOne.id, groupA); - model.SetMeshGroup(meshTwo.id, groupA); - - // Group meshes three and four into another group. - int groupB = model.GenerateGroupId(); - model.SetMeshGroup(meshThree.id, groupB); - model.SetMeshGroup(meshFour.id, groupB); - - // Leave mesh5 and mesh6 alone, ungrouped. - - // Check that AreMeshesInSameGroup behaves as expected. - NUnit.Framework.Assert.IsTrue(model.AreMeshesInSameGroup(new int[] {})); - NUnit.Framework.Assert.IsTrue(model.AreMeshesInSameGroup(new int[] { meshOneId })); - NUnit.Framework.Assert.IsTrue( - model.AreMeshesInSameGroup(new int[] { meshOneId, meshTwoId })); - NUnit.Framework.Assert.IsFalse( - model.AreMeshesInSameGroup(new int[] { meshOneId, meshThreeId })); - NUnit.Framework.Assert.IsTrue( - model.AreMeshesInSameGroup(new int[] { meshThreeId, meshFourId })); - NUnit.Framework.Assert.IsFalse( - model.AreMeshesInSameGroup(new int[] { meshThreeId, meshFiveId })); - // meshFive and meshSix are in GROUP_NONE, so it should return false even though - // both are in the "same group". - NUnit.Framework.Assert.IsFalse( - model.AreMeshesInSameGroup(new int[] { meshFiveId, meshSixId })); - - // Sanity check with empty group. - HashSet meshIds = new HashSet(); - model.ExpandMeshIdsToGroupMates(meshIds); - NUnit.Framework.Assert.IsTrue(meshIds.Count == 0); - - // Group mates for mesh one should be { meshOne, meshTwo }. - meshIds = new HashSet(new int[] { meshOne.id }); - model.ExpandMeshIdsToGroupMates(meshIds); - NUnit.Framework.Assert.AreEqual(2, meshIds.Count); - NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshOne.id)); - NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshTwo.id)); - - // Group mates for mesh four should be { meshThree, meshFour }. - meshIds = new HashSet(new int[] { meshFour.id }); - model.ExpandMeshIdsToGroupMates(meshIds); - NUnit.Framework.Assert.AreEqual(2, meshIds.Count); - NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshThree.id)); - NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshFour.id)); - - // Group mates for mesh five (ungrouped) should be only itself. - meshIds = new HashSet(new int[] { meshFive.id }); - model.ExpandMeshIdsToGroupMates(meshIds); - NUnit.Framework.Assert.AreEqual(1, meshIds.Count); - NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshFive.id)); - - // Now merge the two groups together. - int groupC = model.GenerateGroupId(); - model.SetMeshGroup(meshOne.id, groupC); - model.SetMeshGroup(meshTwo.id, groupC); - model.SetMeshGroup(meshThree.id, groupC); - model.SetMeshGroup(meshFour.id, groupC); - - // And check that it worked. - meshIds = new HashSet(new int[] { meshOne.id }); - model.ExpandMeshIdsToGroupMates(meshIds); - NUnit.Framework.Assert.AreEqual(4, meshIds.Count); - NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshOne.id)); - NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshTwo.id)); - NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshThree.id)); - NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshFour.id)); - - // Try ungrouping. - model.SetMeshGroup(meshOne.id, MMesh.GROUP_NONE); - model.SetMeshGroup(meshTwo.id, MMesh.GROUP_NONE); - model.SetMeshGroup(meshThree.id, MMesh.GROUP_NONE); - model.SetMeshGroup(meshFour.id, MMesh.GROUP_NONE); - - // Now the meshes should not have any group mates. - meshIds = new HashSet(new int[] { meshTwo.id }); - model.ExpandMeshIdsToGroupMates(meshIds); - NUnit.Framework.Assert.AreEqual(1, meshIds.Count); - NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshTwo.id)); +namespace com.google.apps.peltzer.client.model.core +{ + + [TestFixture] + // Tests for Model. + public class ModelTest + { + + [Test] + public void TestAddMesh() + { + Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); + int meshId = 1; + + // Add a mesh. + MMesh mesh = Primitives.AxisAlignedBox(meshId, Vector3.zero, Vector3.one, /* materialId */ 2); + + model.AddMesh(mesh); + + NUnit.Framework.Assert.AreEqual(mesh, model.GetMesh(meshId)); + NUnit.Framework.Assert.True(model.HasMesh(meshId)); + NUnit.Framework.Assert.AreEqual(1, model.GetNumberOfMeshes()); + + // Try to add a mesh with same id. Should fail. + try + { + MMesh dupMesh = Primitives.AxisAlignedBox(meshId, Vector3.zero, Vector3.one, /* materialId */ 2); + model.AddMesh(dupMesh); + NUnit.Framework.Assert.IsTrue(false, "Expected exception"); + } + catch (Exception) + { + // Expected. + } + + // Ensure mesh was not updated. + NUnit.Framework.Assert.AreEqual(mesh, model.GetMesh(1)); + } + + [Test] + public void TestDeleteMesh() + { + Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); + + int meshId = 1; + // Add a mesh. + MMesh mesh = Primitives.AxisAlignedBox(meshId, Vector3.zero, Vector3.one, /* materialId */ 2); + model.AddMesh(mesh); + NUnit.Framework.Assert.AreEqual(mesh, model.GetMesh(meshId)); + NUnit.Framework.Assert.AreEqual(1, model.GetNumberOfMeshes()); + + // Try to delete a non-existent mesh. Should fail. + try + { + model.DeleteMesh(999); + NUnit.Framework.Assert.IsTrue(false, "Expected exception"); + } + catch (Exception) + { + // Expected + } + + // Delete the mesh. + model.DeleteMesh(meshId); + NUnit.Framework.Assert.False(model.HasMesh(meshId)); + NUnit.Framework.Assert.AreEqual(0, model.GetNumberOfMeshes()); + try + { + model.GetMesh(meshId); + NUnit.Framework.Assert.IsTrue(false, "Expected exception"); + } + catch (Exception) + { + // Expected. + } + } + + [Test] + public void TestUndoRedo() + { + int meshOne = 1; + int meshTwo = 2; + + Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); + NUnit.Framework.Assert.AreEqual(0, model.GetNumberOfMeshes(), + "Initial model should be empty."); + + AddMeshCommand command = new AddMeshCommand(Primitives.AxisAlignedBox( + meshOne, Vector3.zero, Vector3.one, /* materialId */ 3)); + + model.ApplyCommand(command); + NUnit.Framework.Assert.True(model.HasMesh(meshOne)); + + command = new AddMeshCommand(Primitives.AxisAlignedBox( + meshTwo, Vector3.one, Vector3.one, /* materialId */ 4)); + + model.ApplyCommand(command); + // Ensure both Meshes are in the model. + NUnit.Framework.Assert.True(model.HasMesh(meshOne)); + NUnit.Framework.Assert.True(model.HasMesh(meshTwo)); + + model.Undo(); + // Should have removed meshTwo. + NUnit.Framework.Assert.True(model.HasMesh(meshOne)); + NUnit.Framework.Assert.False(model.HasMesh(meshTwo)); + + model.Redo(); + // Both should be back. + NUnit.Framework.Assert.True(model.HasMesh(meshOne)); + NUnit.Framework.Assert.True(model.HasMesh(meshTwo)); + + model.Undo(); + model.Undo(); + // Both should be gone. + NUnit.Framework.Assert.False(model.HasMesh(meshOne)); + NUnit.Framework.Assert.False(model.HasMesh(meshTwo)); + + // Now add a new command. It should force the redo stack + // to clear. + model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( + meshOne, Vector3.zero, Vector3.one, /* materialId */ 3))); + + NUnit.Framework.Assert.IsFalse(model.Redo(), + "Expected redo stack to be empty"); + } + + [Test] + public void TestRemesher() + { + int meshOne = 1; + int meshTwo = 2; + + Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); + + model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( + meshOne, Vector3.zero, Vector3.one, /* materialId */ 3))); + + model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( + meshTwo, Vector3.one, Vector3.one, /* materialId */ 4))); + + // Make sure the ReMesher knows about these. + ReMesher remesher = model.GetReMesher(); + NUnit.Framework.Assert.IsTrue(remesher.HasMesh(meshOne)); + NUnit.Framework.Assert.IsTrue(remesher.HasMesh(meshTwo)); + + // Hide one and make sure ReMesher is updated. + model.HideMeshForTestOrTutorial(meshOne); + NUnit.Framework.Assert.IsFalse(remesher.HasMesh(meshOne)); + NUnit.Framework.Assert.IsTrue(remesher.HasMesh(meshTwo)); + + // Unhide and check again. + model.UnhideMeshForTestOrTutorial(meshOne); + NUnit.Framework.Assert.IsTrue(remesher.HasMesh(meshOne)); + NUnit.Framework.Assert.IsTrue(remesher.HasMesh(meshTwo)); + } + + [Test] + public void TestGrouping() + { + int meshOneId = 1; + int meshTwoId = 2; + int meshThreeId = 3; + int meshFourId = 4; + int meshFiveId = 5; + int meshSixId = 6; + + Model model = new Model(new Bounds(Vector3.zero, Vector3.one * 10)); + model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( + meshOneId, Vector3.zero, Vector3.one, /* materialId */ 3))); + model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( + meshTwoId, Vector3.one, Vector3.one, /* materialId */ 4))); + model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( + meshThreeId, Vector3.zero, Vector3.one, /* materialId */ 3))); + model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( + meshFourId, Vector3.one, Vector3.one, /* materialId */ 4))); + model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( + meshFiveId, Vector3.one, Vector3.one, /* materialId */ 4))); + model.ApplyCommand(new AddMeshCommand(Primitives.AxisAlignedBox( + meshSixId, Vector3.one, Vector3.one, /* materialId */ 4))); + + MMesh meshOne = model.GetMesh(meshOneId); + MMesh meshTwo = model.GetMesh(meshTwoId); + MMesh meshThree = model.GetMesh(meshThreeId); + MMesh meshFour = model.GetMesh(meshFourId); + MMesh meshFive = model.GetMesh(meshFiveId); + MMesh meshSix = model.GetMesh(meshSixId); + + // Group meshes one and two into one group. + int groupA = model.GenerateGroupId(); + model.SetMeshGroup(meshOne.id, groupA); + model.SetMeshGroup(meshTwo.id, groupA); + + // Group meshes three and four into another group. + int groupB = model.GenerateGroupId(); + model.SetMeshGroup(meshThree.id, groupB); + model.SetMeshGroup(meshFour.id, groupB); + + // Leave mesh5 and mesh6 alone, ungrouped. + + // Check that AreMeshesInSameGroup behaves as expected. + NUnit.Framework.Assert.IsTrue(model.AreMeshesInSameGroup(new int[] { })); + NUnit.Framework.Assert.IsTrue(model.AreMeshesInSameGroup(new int[] { meshOneId })); + NUnit.Framework.Assert.IsTrue( + model.AreMeshesInSameGroup(new int[] { meshOneId, meshTwoId })); + NUnit.Framework.Assert.IsFalse( + model.AreMeshesInSameGroup(new int[] { meshOneId, meshThreeId })); + NUnit.Framework.Assert.IsTrue( + model.AreMeshesInSameGroup(new int[] { meshThreeId, meshFourId })); + NUnit.Framework.Assert.IsFalse( + model.AreMeshesInSameGroup(new int[] { meshThreeId, meshFiveId })); + // meshFive and meshSix are in GROUP_NONE, so it should return false even though + // both are in the "same group". + NUnit.Framework.Assert.IsFalse( + model.AreMeshesInSameGroup(new int[] { meshFiveId, meshSixId })); + + // Sanity check with empty group. + HashSet meshIds = new HashSet(); + model.ExpandMeshIdsToGroupMates(meshIds); + NUnit.Framework.Assert.IsTrue(meshIds.Count == 0); + + // Group mates for mesh one should be { meshOne, meshTwo }. + meshIds = new HashSet(new int[] { meshOne.id }); + model.ExpandMeshIdsToGroupMates(meshIds); + NUnit.Framework.Assert.AreEqual(2, meshIds.Count); + NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshOne.id)); + NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshTwo.id)); + + // Group mates for mesh four should be { meshThree, meshFour }. + meshIds = new HashSet(new int[] { meshFour.id }); + model.ExpandMeshIdsToGroupMates(meshIds); + NUnit.Framework.Assert.AreEqual(2, meshIds.Count); + NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshThree.id)); + NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshFour.id)); + + // Group mates for mesh five (ungrouped) should be only itself. + meshIds = new HashSet(new int[] { meshFive.id }); + model.ExpandMeshIdsToGroupMates(meshIds); + NUnit.Framework.Assert.AreEqual(1, meshIds.Count); + NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshFive.id)); + + // Now merge the two groups together. + int groupC = model.GenerateGroupId(); + model.SetMeshGroup(meshOne.id, groupC); + model.SetMeshGroup(meshTwo.id, groupC); + model.SetMeshGroup(meshThree.id, groupC); + model.SetMeshGroup(meshFour.id, groupC); + + // And check that it worked. + meshIds = new HashSet(new int[] { meshOne.id }); + model.ExpandMeshIdsToGroupMates(meshIds); + NUnit.Framework.Assert.AreEqual(4, meshIds.Count); + NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshOne.id)); + NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshTwo.id)); + NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshThree.id)); + NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshFour.id)); + + // Try ungrouping. + model.SetMeshGroup(meshOne.id, MMesh.GROUP_NONE); + model.SetMeshGroup(meshTwo.id, MMesh.GROUP_NONE); + model.SetMeshGroup(meshThree.id, MMesh.GROUP_NONE); + model.SetMeshGroup(meshFour.id, MMesh.GROUP_NONE); + + // Now the meshes should not have any group mates. + meshIds = new HashSet(new int[] { meshTwo.id }); + model.ExpandMeshIdsToGroupMates(meshIds); + NUnit.Framework.Assert.AreEqual(1, meshIds.Count); + NUnit.Framework.Assert.IsTrue(meshIds.Contains(meshTwo.id)); + } } - } } diff --git a/Assets/Editor/tests/model/core/PrimitivesTest.cs b/Assets/Editor/tests/model/core/PrimitivesTest.cs index 8eab73da..30dbd3e0 100644 --- a/Assets/Editor/tests/model/core/PrimitivesTest.cs +++ b/Assets/Editor/tests/model/core/PrimitivesTest.cs @@ -20,208 +20,237 @@ using com.google.apps.peltzer.client.model.render; using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.model.core { - - [TestFixture] - // Tests for Primitives class. - public class PrimitivesTest { - - [Test] - public void TestAxisAlignedBox() { - // Hard to test that it "looks" like a cube. So we'll just do some - // sanity checks. - - MMesh mesh = Primitives.AxisAlignedBox( - 1, new Vector3(1, 2, 3), new Vector3(2, 3, 4), /*material id*/ 2); - - NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); - - // Make sure there are 6 faces. - NUnit.Framework.Assert.AreEqual(6, mesh.faceCount); - - HashSet usedVertIds = new HashSet(); - foreach (Face face in mesh.GetFaces()) { - foreach (int vertId in face.vertexIds) { - usedVertIds.Add(vertId); +namespace com.google.apps.peltzer.client.model.core +{ + + [TestFixture] + // Tests for Primitives class. + public class PrimitivesTest + { + + [Test] + public void TestAxisAlignedBox() + { + // Hard to test that it "looks" like a cube. So we'll just do some + // sanity checks. + + MMesh mesh = Primitives.AxisAlignedBox( + 1, new Vector3(1, 2, 3), new Vector3(2, 3, 4), /*material id*/ 2); + + NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); + + // Make sure there are 6 faces. + NUnit.Framework.Assert.AreEqual(6, mesh.faceCount); + + HashSet usedVertIds = new HashSet(); + foreach (Face face in mesh.GetFaces()) + { + foreach (int vertId in face.vertexIds) + { + usedVertIds.Add(vertId); + } + } + + // Make sure there are 8 unique verts. + NUnit.Framework.Assert.AreEqual(8, usedVertIds.Count); + + foreach (Face face in mesh.GetFaces()) + { + NUnit.Framework.Assert.AreEqual(2, face.properties.materialId); + } + + // Make sure all vertex ids are in the map correctly. + foreach (int id in mesh.GetVertexIds()) + { + NUnit.Framework.Assert.AreEqual(id, mesh.GetVertex(id).id); + } + // And Faces. + foreach (int id in mesh.GetFaceIds()) + { + NUnit.Framework.Assert.AreEqual(id, mesh.GetFace(id).id); + } + + // Center should be at 1, 2, 3. + Vector3 middle = mesh.bounds.center; + + // There might be a rounding error. Make sure it is close. + NUnit.Framework.Assert.Less( + Vector3.Distance(middle, new Vector3(1, 2, 3)), + 0.001f); } - } - - // Make sure there are 8 unique verts. - NUnit.Framework.Assert.AreEqual(8, usedVertIds.Count); - - foreach (Face face in mesh.GetFaces()) { - NUnit.Framework.Assert.AreEqual(2, face.properties.materialId); - } - - // Make sure all vertex ids are in the map correctly. - foreach (int id in mesh.GetVertexIds()) { - NUnit.Framework.Assert.AreEqual(id, mesh.GetVertex(id).id); - } - // And Faces. - foreach (int id in mesh.GetFaceIds()) { - NUnit.Framework.Assert.AreEqual(id, mesh.GetFace(id).id); - } - - // Center should be at 1, 2, 3. - Vector3 middle = mesh.bounds.center; - - // There might be a rounding error. Make sure it is close. - NUnit.Framework.Assert.Less( - Vector3.Distance(middle, new Vector3(1, 2, 3)), - 0.001f); - } - [Test] - public void TestCylinder() { - const int SLICES = 12; // Same as in code. - - // No holes. - MMesh mesh = Primitives.AxisAlignedCylinder( - 1, new Vector3(1, 2, 3), Vector3.one, null, /*material id*/ 2); - - NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); - - // Should be SLICES + 2 faces. - NUnit.Framework.Assert.AreEqual(SLICES + 2, mesh.faceCount); - - // And SLICES * 2 Verts. - NUnit.Framework.Assert.AreEqual(SLICES * 2, mesh.vertexCount); - - // Make sure all vertex ids are in the map correctly. - foreach (int id in mesh.GetVertexIds()) { - NUnit.Framework.Assert.AreEqual(id, mesh.GetVertex(id).id); - } - // And Faces. - foreach (int id in mesh.GetFaceIds()) { - NUnit.Framework.Assert.AreEqual(id, mesh.GetFace(id).id); - } - - // Each vertex should be used exactly 3 times. - foreach (Vertex vertex in mesh.GetVertices()) { - int vertId = vertex.id; - int sum = 0; - foreach (Face face in mesh.GetFaces()) { - sum += face.vertexIds.Where(x => x == vertId).Count(); + [Test] + public void TestCylinder() + { + const int SLICES = 12; // Same as in code. + + // No holes. + MMesh mesh = Primitives.AxisAlignedCylinder( + 1, new Vector3(1, 2, 3), Vector3.one, null, /*material id*/ 2); + + NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); + + // Should be SLICES + 2 faces. + NUnit.Framework.Assert.AreEqual(SLICES + 2, mesh.faceCount); + + // And SLICES * 2 Verts. + NUnit.Framework.Assert.AreEqual(SLICES * 2, mesh.vertexCount); + + // Make sure all vertex ids are in the map correctly. + foreach (int id in mesh.GetVertexIds()) + { + NUnit.Framework.Assert.AreEqual(id, mesh.GetVertex(id).id); + } + // And Faces. + foreach (int id in mesh.GetFaceIds()) + { + NUnit.Framework.Assert.AreEqual(id, mesh.GetFace(id).id); + } + + // Each vertex should be used exactly 3 times. + foreach (Vertex vertex in mesh.GetVertices()) + { + int vertId = vertex.id; + int sum = 0; + foreach (Face face in mesh.GetFaces()) + { + sum += face.vertexIds.Where(x => x == vertId).Count(); + } + NUnit.Framework.Assert.AreEqual(3, sum); + } } - NUnit.Framework.Assert.AreEqual(3, sum); - } - } - [Test] - public void TestAxisAlignedCone() { - const int SLICES = 12; // Same as in code. + [Test] + public void TestAxisAlignedCone() + { + const int SLICES = 12; // Same as in code. - MMesh mesh = Primitives.AxisAlignedCone( - 1, new Vector3(1, 2, 3), Vector3.one, /*material id*/ 2); + MMesh mesh = Primitives.AxisAlignedCone( + 1, new Vector3(1, 2, 3), Vector3.one, /*material id*/ 2); - NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); + NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); - // Make sure there are SLICES + 1 faces. - NUnit.Framework.Assert.AreEqual(SLICES + 1, mesh.faceCount); + // Make sure there are SLICES + 1 faces. + NUnit.Framework.Assert.AreEqual(SLICES + 1, mesh.faceCount); - // Make sure there are SLICES + 1 vertices. - NUnit.Framework.Assert.AreEqual(SLICES + 1, mesh.vertexCount); + // Make sure there are SLICES + 1 vertices. + NUnit.Framework.Assert.AreEqual(SLICES + 1, mesh.vertexCount); - // Make sure all vertex ids are in the map correctly. - foreach (int id in mesh.GetVertexIds()) { - NUnit.Framework.Assert.AreEqual(id, mesh.GetVertex(id).id); - } - // And Faces. - foreach (int id in mesh.GetFaceIds()) { - NUnit.Framework.Assert.AreEqual(id, mesh.GetFace(id).id); - } + // Make sure all vertex ids are in the map correctly. + foreach (int id in mesh.GetVertexIds()) + { + NUnit.Framework.Assert.AreEqual(id, mesh.GetVertex(id).id); + } + // And Faces. + foreach (int id in mesh.GetFaceIds()) + { + NUnit.Framework.Assert.AreEqual(id, mesh.GetFace(id).id); + } - // Center should be at 1, 2, 3. - Vector3 middle = mesh.bounds.center; - - // There might be a rounding error. Make sure it is close. - NUnit.Framework.Assert.Less( - Vector3.Distance(middle, new Vector3(1, 2, 3)), - 0.001f); - } + // Center should be at 1, 2, 3. + Vector3 middle = mesh.bounds.center; - [Test] - public void TestTriangularPyramid() { - MMesh mesh = Primitives.TriangularPyramid( - 1, new Vector3(1, 2, 3), Vector3.one, /*material id*/ 2); - - NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); - - // Make sure there are 4 faces. - NUnit.Framework.Assert.AreEqual(4, mesh.faceCount); - - // Make sure there are 4 vertices. - NUnit.Framework.Assert.AreEqual(4, mesh.vertexCount); - - // Make sure all vertex ids are in the map correctly. - foreach (int id in mesh.GetVertexIds()) { - NUnit.Framework.Assert.AreEqual(id, mesh.GetVertex(id).id); - } - // And Faces. - foreach (int id in mesh.GetFaceIds()) { - NUnit.Framework.Assert.AreEqual(id, mesh.GetFace(id).id); - } - - // Each vertex should be used exactly 3 times. - for (int i = 0; i < 4; i++) { - int sum = 0; - for (int j = 0; j < 4; j++) { - sum += mesh.GetFace(j).vertexIds.Where(x => x == i).Count(); + // There might be a rounding error. Make sure it is close. + NUnit.Framework.Assert.Less( + Vector3.Distance(middle, new Vector3(1, 2, 3)), + 0.001f); } - NUnit.Framework.Assert.AreEqual(3, sum); - } - } - - [Test] - public void TestTorus() { - const int SLICES = 12; // Same as in code. - - MMesh mesh = Primitives.Torus( - 1, new Vector3(1, 2, 3), Vector3.one * 2, /*material id*/ 2); - - NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); - - // Make sure there are n^2 faces. - NUnit.Framework.Assert.AreEqual(SLICES * SLICES, mesh.faceCount); - // Make sure there are n^2 vertices. - NUnit.Framework.Assert.AreEqual(SLICES * SLICES, mesh.vertexCount); - - // Make sure all vertex ids are in the map correctly. - foreach (int id in mesh.GetVertexIds()) { - NUnit.Framework.Assert.AreEqual(id, mesh.GetVertex(id).id); - } - // And Faces. - foreach (int id in mesh.GetFaceIds()) { - NUnit.Framework.Assert.AreEqual(id, mesh.GetFace(id).id); - } + [Test] + public void TestTriangularPyramid() + { + MMesh mesh = Primitives.TriangularPyramid( + 1, new Vector3(1, 2, 3), Vector3.one, /*material id*/ 2); + + NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); + + // Make sure there are 4 faces. + NUnit.Framework.Assert.AreEqual(4, mesh.faceCount); + + // Make sure there are 4 vertices. + NUnit.Framework.Assert.AreEqual(4, mesh.vertexCount); + + // Make sure all vertex ids are in the map correctly. + foreach (int id in mesh.GetVertexIds()) + { + NUnit.Framework.Assert.AreEqual(id, mesh.GetVertex(id).id); + } + // And Faces. + foreach (int id in mesh.GetFaceIds()) + { + NUnit.Framework.Assert.AreEqual(id, mesh.GetFace(id).id); + } + + // Each vertex should be used exactly 3 times. + for (int i = 0; i < 4; i++) + { + int sum = 0; + for (int j = 0; j < 4; j++) + { + sum += mesh.GetFace(j).vertexIds.Where(x => x == i).Count(); + } + NUnit.Framework.Assert.AreEqual(3, sum); + } + } - // Each vertex should be used exactly 4 times. - for (int i = 0; i < (SLICES * SLICES); i++) { - int sum = 0; - for (int j = 0; j < (SLICES * SLICES); j++) { - sum += mesh.GetFace(j).vertexIds.Where(x => x == i).Count(); + [Test] + public void TestTorus() + { + const int SLICES = 12; // Same as in code. + + MMesh mesh = Primitives.Torus( + 1, new Vector3(1, 2, 3), Vector3.one * 2, /*material id*/ 2); + + NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); + + // Make sure there are n^2 faces. + NUnit.Framework.Assert.AreEqual(SLICES * SLICES, mesh.faceCount); + + // Make sure there are n^2 vertices. + NUnit.Framework.Assert.AreEqual(SLICES * SLICES, mesh.vertexCount); + + // Make sure all vertex ids are in the map correctly. + foreach (int id in mesh.GetVertexIds()) + { + NUnit.Framework.Assert.AreEqual(id, mesh.GetVertex(id).id); + } + // And Faces. + foreach (int id in mesh.GetFaceIds()) + { + NUnit.Framework.Assert.AreEqual(id, mesh.GetFace(id).id); + } + + // Each vertex should be used exactly 4 times. + for (int i = 0; i < (SLICES * SLICES); i++) + { + int sum = 0; + for (int j = 0; j < (SLICES * SLICES); j++) + { + sum += mesh.GetFace(j).vertexIds.Where(x => x == i).Count(); + } + NUnit.Framework.Assert.AreEqual(4, sum); + } } - NUnit.Framework.Assert.AreEqual(4, sum); - } - } - [Test] - public void TestSphere() { - MMesh mesh = Primitives.AxisAlignedIcosphere( - 1, new Vector3(1, 2, 3), Vector3.one, /*material id*/ 2); - - // Make sure all vertex ids are in the map correctly. - foreach (int id in mesh.GetVertexIds()) { - NUnit.Framework.Assert.AreEqual(id, mesh.GetVertex(id).id); - } - // And Faces. - foreach (int id in mesh.GetFaceIds()) { - NUnit.Framework.Assert.AreEqual(id, mesh.GetFace(id).id); - } - - NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); - NUnit.Framework.Assert.AreEqual(80, mesh.faceCount); + [Test] + public void TestSphere() + { + MMesh mesh = Primitives.AxisAlignedIcosphere( + 1, new Vector3(1, 2, 3), Vector3.one, /*material id*/ 2); + + // Make sure all vertex ids are in the map correctly. + foreach (int id in mesh.GetVertexIds()) + { + NUnit.Framework.Assert.AreEqual(id, mesh.GetVertex(id).id); + } + // And Faces. + foreach (int id in mesh.GetFaceIds()) + { + NUnit.Framework.Assert.AreEqual(id, mesh.GetFace(id).id); + } + + NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); + NUnit.Framework.Assert.AreEqual(80, mesh.faceCount); + } } - } } \ No newline at end of file diff --git a/Assets/Editor/tests/model/core/SnapGridTest.cs b/Assets/Editor/tests/model/core/SnapGridTest.cs index 566c36ab..5ecc1310 100644 --- a/Assets/Editor/tests/model/core/SnapGridTest.cs +++ b/Assets/Editor/tests/model/core/SnapGridTest.cs @@ -19,189 +19,197 @@ using System.Linq; using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.model.core { +namespace com.google.apps.peltzer.client.model.core +{ + + [TestFixture] + // Tests for SnapGrid. + public class SnapGridTest + { + private readonly float EPSILON = 0.0001f; + + [Test] + public void TestFindForwardAxis() + { + MMesh mesh = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, 1); + Face face = mesh.GetFace(0); + List f0Vertices = face.vertexIds.Select(idx => mesh.VertexPositionInModelCoords(idx)).ToList(); + + Plane plane = new Plane(f0Vertices[0], f0Vertices[1], f0Vertices[2]); + + Assert.AreEqual(plane.normal, SnapGrid.FindForwardAxisForTest(f0Vertices)); + } + + [Test] + public void TestFindRightAxis() + { + // ClosestEdge in the Primitive from TestFindForwardAxis() for position new Vector3(-1, 0.90f, 0f). + KeyValuePair closestEdge = new KeyValuePair( + new Vector3(-1, 1, 1), + new Vector3(-1, 1, -1)); + + Assert.IsTrue( + Math3d.CompareVectors(new Vector3(0, 0, 2).normalized, SnapGrid.FindRightAxisForTest(closestEdge), EPSILON)); + + closestEdge = new KeyValuePair(new Vector3(1, 1, -1), new Vector3(1, 1, 1)); + Assert.IsTrue( + Math3d.CompareVectors(new Vector3(0, 0, -2).normalized, SnapGrid.FindRightAxisForTest(closestEdge), EPSILON)); + + closestEdge = new KeyValuePair(new Vector3(1, 1, 1), new Vector3(1, 1, -1)); + Assert.IsTrue( + Math3d.CompareVectors(new Vector3(0, 0, 2).normalized, SnapGrid.FindRightAxisForTest(closestEdge), EPSILON)); + } + + [Test] + public void TestFindUpAxis() + { + Vector3 forwardAxis = (new Vector3(-1, 0, 0)).normalized; + Vector3 rightAxis = (new Vector3(0, 0, 2)).normalized; + Vector3 upAxis = (new Vector3(0, 2, 0)).normalized; + + Assert.IsTrue( + Math3d.CompareVectors(upAxis, SnapGrid.FindUpAxisForTest(rightAxis, forwardAxis), EPSILON)); + + forwardAxis = (new Vector3(1, 0, 0)).normalized; + rightAxis = (new Vector3(0, 0, -2)).normalized; + upAxis = (new Vector3(0, 2, 0)).normalized; + + Assert.IsTrue( + Math3d.CompareVectors(upAxis, SnapGrid.FindUpAxisForTest(rightAxis, forwardAxis), EPSILON)); + + forwardAxis = (new Vector3(0, 1, 0)).normalized; + rightAxis = (new Vector3(0, 0, 2)).normalized; + upAxis = (new Vector3(2, 0, 0)).normalized; + + Assert.IsTrue( + Math3d.CompareVectors(upAxis, SnapGrid.FindUpAxisForTest(rightAxis, forwardAxis), EPSILON)); + } + + [Test] + // Tests to see if face axis are square and correctly related. + public void TestExampleAxis() + { + Vector3 forward = (new Vector3(2, 1, 3)).normalized; + Vector3 right = (new Vector3(3, 0, -2)).normalized; + Assert.AreEqual(90.0f, Vector3.Angle(forward, right)); + + Vector3 up = SnapGrid.FindUpAxisForTest(right, forward); + Assert.AreEqual(90.0f, Vector3.Angle(forward, up)); + Assert.AreEqual(90.0f, Vector3.Angle(right, up)); + Assert.AreEqual(90.0f, Vector3.Angle(right, forward)); + Assert.AreEqual(90.0f, Vector3.Angle(forward, right)); + Assert.AreEqual(90.0f, Vector3.Angle(forward, up)); + + Assert.IsTrue(Math3d.CompareVectors(up, Vector3.Cross(forward, right), EPSILON)); + Assert.IsTrue(Math3d.CompareVectors(right, Vector3.Cross(up, forward), EPSILON)); + Assert.IsTrue(Math3d.CompareVectors(forward, Vector3.Cross(right, up), EPSILON)); - [TestFixture] - // Tests for SnapGrid. - public class SnapGridTest { - private readonly float EPSILON = 0.0001f; + forward = (new Vector3(-1, 0, 0)).normalized; + right = (new Vector3(0, 0, 2)).normalized; + up = (new Vector3(0, 2, 0)).normalized; - [Test] - public void TestFindForwardAxis() { - MMesh mesh = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, 1); - Face face = mesh.GetFace(0); - List f0Vertices = face.vertexIds.Select(idx => mesh.VertexPositionInModelCoords(idx)).ToList(); + Assert.IsTrue(Math3d.CompareVectors(up, SnapGrid.FindUpAxisForTest(right, forward), EPSILON)); + Assert.AreEqual(90.0f, Vector3.Angle(forward, up)); + Assert.AreEqual(90.0f, Vector3.Angle(right, up)); + Assert.AreEqual(90.0f, Vector3.Angle(right, forward)); + Assert.AreEqual(90.0f, Vector3.Angle(forward, right)); + Assert.AreEqual(90.0f, Vector3.Angle(forward, up)); - Plane plane = new Plane(f0Vertices[0], f0Vertices[1], f0Vertices[2]); + Assert.IsTrue(Math3d.CompareVectors(up, Vector3.Cross(forward, right), EPSILON)); + Assert.IsTrue(Math3d.CompareVectors(right, Vector3.Cross(up, forward), EPSILON)); + Assert.IsTrue(Math3d.CompareVectors(forward, Vector3.Cross(right, up), EPSILON)); + } - Assert.AreEqual(plane.normal, SnapGrid.FindForwardAxisForTest(f0Vertices)); - } - - [Test] - public void TestFindRightAxis() { - // ClosestEdge in the Primitive from TestFindForwardAxis() for position new Vector3(-1, 0.90f, 0f). - KeyValuePair closestEdge = new KeyValuePair( - new Vector3(-1, 1, 1), - new Vector3(-1, 1, -1)); - - Assert.IsTrue( - Math3d.CompareVectors(new Vector3(0, 0, 2).normalized, SnapGrid.FindRightAxisForTest(closestEdge), EPSILON)); + [Test] + // Tests if the rotation found applied to the universal axis returns the face axis. + public void TestFindRotation() + { + Vector3 forward = new Vector3(-1, 0, 0).normalized; + Vector3 right = new Vector3(0, 0, 2).normalized; + Vector3 up = new Vector3(0, 2, 0).normalized; - closestEdge = new KeyValuePair(new Vector3(1, 1, -1), new Vector3(1, 1, 1)); - Assert.IsTrue( - Math3d.CompareVectors(new Vector3(0, 0, -2).normalized, SnapGrid.FindRightAxisForTest(closestEdge), EPSILON)); - - closestEdge = new KeyValuePair(new Vector3(1, 1, 1), new Vector3(1, 1, -1)); - Assert.IsTrue( - Math3d.CompareVectors(new Vector3(0, 0, 2).normalized, SnapGrid.FindRightAxisForTest(closestEdge), EPSILON)); - } + Quaternion rotation = SnapGrid.FindFaceRotationForTest(right, up, forward); - [Test] - public void TestFindUpAxis() { - Vector3 forwardAxis = (new Vector3(-1, 0, 0)).normalized; - Vector3 rightAxis = (new Vector3(0, 0, 2)).normalized; - Vector3 upAxis = (new Vector3(0, 2, 0)).normalized; + Assert.IsTrue(Math3d.CompareVectors(right, rotation * Vector3.right, EPSILON)); + Assert.IsTrue(Math3d.CompareVectors(up, rotation * Vector3.up, EPSILON)); + Assert.IsTrue(Math3d.CompareVectors(forward, rotation * Vector3.forward, EPSILON)); - Assert.IsTrue( - Math3d.CompareVectors(upAxis, SnapGrid.FindUpAxisForTest(rightAxis, forwardAxis), EPSILON)); + forward = new Vector3(1, 0, 0).normalized; + right = new Vector3(0, 0, -2).normalized; + up = new Vector3(0, 2, 0).normalized; - forwardAxis = (new Vector3(1, 0, 0)).normalized; - rightAxis = (new Vector3(0, 0, -2)).normalized; - upAxis = (new Vector3(0, 2, 0)).normalized; + rotation = SnapGrid.FindFaceRotationForTest(right, up, forward); - Assert.IsTrue( - Math3d.CompareVectors(upAxis, SnapGrid.FindUpAxisForTest(rightAxis, forwardAxis), EPSILON)); + Assert.IsTrue(Math3d.CompareVectors(right, rotation * Vector3.right, EPSILON)); + Assert.IsTrue(Math3d.CompareVectors(up, rotation * Vector3.up, EPSILON)); + Assert.IsTrue(Math3d.CompareVectors(forward, rotation * Vector3.forward, EPSILON)); - forwardAxis = (new Vector3(0, 1, 0)).normalized; - rightAxis = (new Vector3(0, 0, 2)).normalized; - upAxis = (new Vector3(2, 0, 0)).normalized; - - Assert.IsTrue( - Math3d.CompareVectors(upAxis, SnapGrid.FindUpAxisForTest(rightAxis, forwardAxis), EPSILON)); - } - - [Test] - // Tests to see if face axis are square and correctly related. - public void TestExampleAxis() { - Vector3 forward = (new Vector3(2, 1, 3)).normalized; - Vector3 right = (new Vector3(3, 0, -2)).normalized; - Assert.AreEqual(90.0f, Vector3.Angle(forward, right)); - - Vector3 up = SnapGrid.FindUpAxisForTest(right, forward); - Assert.AreEqual(90.0f, Vector3.Angle(forward, up)); - Assert.AreEqual(90.0f, Vector3.Angle(right, up)); - Assert.AreEqual(90.0f, Vector3.Angle(right, forward)); - Assert.AreEqual(90.0f, Vector3.Angle(forward, right)); - Assert.AreEqual(90.0f, Vector3.Angle(forward, up)); - - Assert.IsTrue(Math3d.CompareVectors(up , Vector3.Cross(forward, right), EPSILON)); - Assert.IsTrue(Math3d.CompareVectors(right , Vector3.Cross(up, forward), EPSILON)); - Assert.IsTrue(Math3d.CompareVectors(forward , Vector3.Cross(right, up), EPSILON)); - - forward = (new Vector3(-1, 0, 0)).normalized; - right = (new Vector3(0, 0, 2)).normalized; - up = (new Vector3(0, 2, 0)).normalized; - - Assert.IsTrue(Math3d.CompareVectors(up, SnapGrid.FindUpAxisForTest(right, forward), EPSILON)); - Assert.AreEqual(90.0f, Vector3.Angle(forward, up)); - Assert.AreEqual(90.0f, Vector3.Angle(right, up)); - Assert.AreEqual(90.0f, Vector3.Angle(right, forward)); - Assert.AreEqual(90.0f, Vector3.Angle(forward, right)); - Assert.AreEqual(90.0f, Vector3.Angle(forward, up)); - - Assert.IsTrue(Math3d.CompareVectors(up , Vector3.Cross(forward, right), EPSILON)); - Assert.IsTrue(Math3d.CompareVectors(right , Vector3.Cross(up, forward), EPSILON)); - Assert.IsTrue(Math3d.CompareVectors(forward , Vector3.Cross(right, up), EPSILON)); - } + forward = (new Vector3(2, 1, 3)).normalized; + right = (new Vector3(3, 0, -2)).normalized; + up = SnapGrid.FindUpAxisForTest(right, forward); - [Test] - // Tests if the rotation found applied to the universal axis returns the face axis. - public void TestFindRotation() { - Vector3 forward = new Vector3(-1, 0, 0).normalized; - Vector3 right = new Vector3(0, 0, 2).normalized; - Vector3 up = new Vector3(0, 2, 0).normalized; + rotation = SnapGrid.FindFaceRotationForTest(right, up, forward); - Quaternion rotation = SnapGrid.FindFaceRotationForTest(right, up, forward); + Assert.IsTrue(Math3d.CompareVectors(right, rotation * Vector3.right, EPSILON)); + Assert.IsTrue(Math3d.CompareVectors(up, rotation * Vector3.up, EPSILON)); + Assert.IsTrue(Math3d.CompareVectors(forward, rotation * Vector3.forward, EPSILON)); - Assert.IsTrue(Math3d.CompareVectors(right , rotation * Vector3.right, EPSILON)); - Assert.IsTrue(Math3d.CompareVectors(up , rotation * Vector3.up, EPSILON)); - Assert.IsTrue(Math3d.CompareVectors(forward , rotation * Vector3.forward, EPSILON)); + forward = new Vector3(0.7055063f, -0.6813365f, -0.195042f).normalized; + right = new Vector3(-0.5638906f, -0.7063745f, 0.4278581f).normalized; + up = new Vector3(-0.429288f, -0.1918742f, -0.882551f).normalized; - forward = new Vector3(1, 0, 0).normalized; - right = new Vector3(0, 0, -2).normalized; - up = new Vector3(0, 2, 0).normalized; + rotation = SnapGrid.FindFaceRotationForTest(right, up, forward); - rotation = SnapGrid.FindFaceRotationForTest(right, up, forward); + Assert.IsTrue(Math3d.CompareVectors(right, rotation * Vector3.right, EPSILON)); + Assert.IsTrue(Math3d.CompareVectors(up, rotation * Vector3.up, EPSILON)); + Assert.IsTrue(Math3d.CompareVectors(forward, rotation * Vector3.forward, EPSILON)); - Assert.IsTrue(Math3d.CompareVectors(right , rotation * Vector3.right, EPSILON)); - Assert.IsTrue(Math3d.CompareVectors(up , rotation * Vector3.up, EPSILON)); - Assert.IsTrue(Math3d.CompareVectors(forward , rotation * Vector3.forward, EPSILON)); + forward = new Vector3(0.3194726f, -0.4424828f, -0.8379416f).normalized; + right = new Vector3(0.9473031f, 0.1710962f, 0.2708191f).normalized; + up = new Vector3(0.02353583f, -0.880304f, 0.4738259f).normalized; - forward = (new Vector3(2, 1, 3)).normalized; - right = (new Vector3(3, 0, -2)).normalized; - up = SnapGrid.FindUpAxisForTest(right, forward); + rotation = SnapGrid.FindFaceRotationForTest(right, up, forward); - rotation = SnapGrid.FindFaceRotationForTest(right, up, forward); - - Assert.IsTrue(Math3d.CompareVectors(right , rotation * Vector3.right, EPSILON)); - Assert.IsTrue(Math3d.CompareVectors(up , rotation * Vector3.up, EPSILON)); - Assert.IsTrue(Math3d.CompareVectors(forward , rotation * Vector3.forward, EPSILON)); - - forward = new Vector3(0.7055063f, -0.6813365f, -0.195042f).normalized; - right = new Vector3(-0.5638906f, -0.7063745f, 0.4278581f).normalized; - up = new Vector3(-0.429288f, -0.1918742f, -0.882551f).normalized; - - rotation = SnapGrid.FindFaceRotationForTest(right, up, forward); - - Assert.IsTrue(Math3d.CompareVectors(right , rotation * Vector3.right, EPSILON)); - Assert.IsTrue(Math3d.CompareVectors(up , rotation * Vector3.up, EPSILON)); - Assert.IsTrue(Math3d.CompareVectors(forward , rotation * Vector3.forward, EPSILON)); - - forward = new Vector3(0.3194726f, -0.4424828f, -0.8379416f).normalized; - right = new Vector3(0.9473031f, 0.1710962f, 0.2708191f).normalized; - up = new Vector3(0.02353583f, -0.880304f, 0.4738259f).normalized; - - rotation = SnapGrid.FindFaceRotationForTest(right, up, forward); - - Assert.IsTrue(Math3d.CompareVectors(right , rotation * Vector3.right, EPSILON)); - Assert.IsTrue(Math3d.CompareVectors(up , rotation * Vector3.up, EPSILON)); - Assert.IsTrue(Math3d.CompareVectors(forward , rotation * Vector3.forward, EPSILON)); - } + Assert.IsTrue(Math3d.CompareVectors(right, rotation * Vector3.right, EPSILON)); + Assert.IsTrue(Math3d.CompareVectors(up, rotation * Vector3.up, EPSILON)); + Assert.IsTrue(Math3d.CompareVectors(forward, rotation * Vector3.forward, EPSILON)); + } - [Test] - // Tests if face rotation is the same as a meshes rotation given the mesh is unmodified. - public void TestIfRotationAppliesToFullMesh() { - GameObject testCube = GameObject.CreatePrimitive(PrimitiveType.Cube); + [Test] + // Tests if face rotation is the same as a meshes rotation given the mesh is unmodified. + public void TestIfRotationAppliesToFullMesh() + { + GameObject testCube = GameObject.CreatePrimitive(PrimitiveType.Cube); - testCube.transform.rotation = Quaternion.Euler(new Vector3(30f, 25f, 10f)); + testCube.transform.rotation = Quaternion.Euler(new Vector3(30f, 25f, 10f)); - Vector3 forward = testCube.transform.forward; - Vector3 right = testCube.transform.right; - Vector3 up = testCube.transform.up; + Vector3 forward = testCube.transform.forward; + Vector3 right = testCube.transform.right; + Vector3 up = testCube.transform.up; - Quaternion rotation = SnapGrid.FindFaceRotationForTest(right, up, forward); + Quaternion rotation = SnapGrid.FindFaceRotationForTest(right, up, forward); - Assert.IsTrue(Math3d.CompareQuaternions(testCube.transform.rotation, rotation, EPSILON)); + Assert.IsTrue(Math3d.CompareQuaternions(testCube.transform.rotation, rotation, EPSILON)); - testCube.transform.rotation = Quaternion.Euler(new Vector3(15f, 19f, 72f)); + testCube.transform.rotation = Quaternion.Euler(new Vector3(15f, 19f, 72f)); - forward = testCube.transform.forward; - right = testCube.transform.right; - up = testCube.transform.up; + forward = testCube.transform.forward; + right = testCube.transform.right; + up = testCube.transform.up; - rotation = SnapGrid.FindFaceRotationForTest(right, up, forward); + rotation = SnapGrid.FindFaceRotationForTest(right, up, forward); - Assert.IsTrue(Math3d.CompareQuaternions(testCube.transform.rotation, rotation, EPSILON)); + Assert.IsTrue(Math3d.CompareQuaternions(testCube.transform.rotation, rotation, EPSILON)); - testCube.transform.rotation = Quaternion.Euler(new Vector3(0f, 192f, 29f)); + testCube.transform.rotation = Quaternion.Euler(new Vector3(0f, 192f, 29f)); - forward = testCube.transform.forward; - right = testCube.transform.right; - up = testCube.transform.up; + forward = testCube.transform.forward; + right = testCube.transform.right; + up = testCube.transform.up; - rotation = SnapGrid.FindFaceRotationForTest(right, up, forward); + rotation = SnapGrid.FindFaceRotationForTest(right, up, forward); - Assert.IsTrue(Math3d.CompareQuaternions(testCube.transform.rotation, rotation, EPSILON)); + Assert.IsTrue(Math3d.CompareQuaternions(testCube.transform.rotation, rotation, EPSILON)); + } } - } } diff --git a/Assets/Editor/tests/model/core/SpatialIndexTest.cs b/Assets/Editor/tests/model/core/SpatialIndexTest.cs index 7824d43a..493df0b7 100644 --- a/Assets/Editor/tests/model/core/SpatialIndexTest.cs +++ b/Assets/Editor/tests/model/core/SpatialIndexTest.cs @@ -19,128 +19,135 @@ using NUnit.Framework; using UnityEngine; -namespace com.google.apps.peltzer.client.model.core { - [TestFixture] - public class SpatialIndexTest { - [Test] - public void TestFindVertex () { - int centerId = 1; - int farAwayId = 2; - MMesh cubeAtCenter = Primitives.AxisAlignedBox( - centerId, Vector3.zero, /* radius */ Vector3.one, /* material */ 2); - MMesh cubeFarAway = Primitives.AxisAlignedBox( - farAwayId, Vector3.one * 3, /* radius */ Vector3.one, /* material */ 2); - SpatialIndex index = new SpatialIndex(new Bounds(Vector3.zero, Vector3.one * 10)); - - // Add a cube at the center and another further away. - index.AddMesh(cubeAtCenter); - index.AddMesh(cubeFarAway); - - // Seach for vertices within 1.5 meters from *near* center: - Vector3 searchLoc = new Vector3(0.1f, 0.2f, 0.3f); - List> vertices; - NUnit.Framework.Assert.IsTrue(index.FindVerticesClosestTo(searchLoc, 3f, out vertices)); - - // Should have all vertices of the center cube, none of the other: - NUnit.Framework.Assert.AreEqual(8, vertices.Count); - - foreach(DistancePair vertex in vertices) { - NUnit.Framework.Assert.AreEqual(vertex.value.meshId, centerId); - } - - // Should be sorted by distance from searchLoc - for (int i = 0; i < (vertices.Count - 1); i++) { - NUnit.Framework.Assert.LessOrEqual( - Vector3.Distance(searchLoc, cubeAtCenter.VertexPositionInModelCoords(vertices[i].value.vertexId)), - Vector3.Distance(searchLoc, cubeAtCenter.VertexPositionInModelCoords(vertices[i + 1].value.vertexId))); - } - - // Remove the center cube and do the same searc. SHould get an empty result. - index.RemoveMesh(cubeAtCenter); - NUnit.Framework.Assert.IsFalse(index.FindVerticesClosestTo(searchLoc, 3f, out vertices)); +namespace com.google.apps.peltzer.client.model.core +{ + [TestFixture] + public class SpatialIndexTest + { + [Test] + public void TestFindVertex() + { + int centerId = 1; + int farAwayId = 2; + MMesh cubeAtCenter = Primitives.AxisAlignedBox( + centerId, Vector3.zero, /* radius */ Vector3.one, /* material */ 2); + MMesh cubeFarAway = Primitives.AxisAlignedBox( + farAwayId, Vector3.one * 3, /* radius */ Vector3.one, /* material */ 2); + SpatialIndex index = new SpatialIndex(new Bounds(Vector3.zero, Vector3.one * 10)); + + // Add a cube at the center and another further away. + index.AddMesh(cubeAtCenter); + index.AddMesh(cubeFarAway); + + // Seach for vertices within 1.5 meters from *near* center: + Vector3 searchLoc = new Vector3(0.1f, 0.2f, 0.3f); + List> vertices; + NUnit.Framework.Assert.IsTrue(index.FindVerticesClosestTo(searchLoc, 3f, out vertices)); + + // Should have all vertices of the center cube, none of the other: + NUnit.Framework.Assert.AreEqual(8, vertices.Count); + + foreach (DistancePair vertex in vertices) + { + NUnit.Framework.Assert.AreEqual(vertex.value.meshId, centerId); + } + + // Should be sorted by distance from searchLoc + for (int i = 0; i < (vertices.Count - 1); i++) + { + NUnit.Framework.Assert.LessOrEqual( + Vector3.Distance(searchLoc, cubeAtCenter.VertexPositionInModelCoords(vertices[i].value.vertexId)), + Vector3.Distance(searchLoc, cubeAtCenter.VertexPositionInModelCoords(vertices[i + 1].value.vertexId))); + } + + // Remove the center cube and do the same searc. SHould get an empty result. + index.RemoveMesh(cubeAtCenter); + NUnit.Framework.Assert.IsFalse(index.FindVerticesClosestTo(searchLoc, 3f, out vertices)); + } + + [Test] + public void TestFindFace() + { + // Add two cubes. One is bigger. Have one face from each in the same plane with the same center: + int smallId = 1; + int largeId = 2; + MMesh smallCube = Primitives.AxisAlignedBox( + smallId, new Vector3(1, 0, 0), /* radius */ Vector3.one, /* material */ 2); + MMesh largeCube = Primitives.AxisAlignedBox( + largeId, Vector3.zero, /* radius */ Vector3.one * 2, /* material */ 2); + SpatialIndex index = new SpatialIndex(new Bounds(Vector3.zero, Vector3.one * 10)); + index.AddMesh(smallCube); + index.AddMesh(largeCube); + + // For a point (practically) on both faces near their ceneter. + List> faces; + NUnit.Framework.Assert.IsTrue(index.FindFacesClosestTo(new Vector3(2.1f, 0, 0), 0.1f, false, out faces)); + + // Should be two faces: + NUnit.Framework.Assert.AreEqual(2, faces.Count); + + // Expect same results when looking for meshes, since we really just look for faces: + List> meshIds; + NUnit.Framework.Assert.IsTrue(index.FindMeshesClosestTo(new Vector3(2.1f, 0, 0), 0.1f, out meshIds)); + NUnit.Framework.Assert.AreEqual(2, meshIds.Count); + + // Move away from the center, outside the smaller bounding box: + NUnit.Framework.Assert.IsTrue(index.FindFacesClosestTo(new Vector3(2.1f, 1.1f, 1.1f), 0.1f, false, out faces)); + + // Should only be the larger cube: + NUnit.Framework.Assert.AreEqual(1, faces.Count); + NUnit.Framework.Assert.AreEqual(largeId, faces[0].value.meshId); + + // Remove the larger cube and try again. + index.RemoveMesh(largeCube); + // Near center: + NUnit.Framework.Assert.IsTrue(index.FindFacesClosestTo(new Vector3(2.1f, 0, 0), 0.1f, false, out faces)); + // Outside of small cube: + NUnit.Framework.Assert.IsFalse(index.FindFacesClosestTo(new Vector3(2.1f, 1.1f, 1.1f), 0.1f, false, out faces)); + } + + [Test] + public void TestFindEdge() + { + // Add two cubes. One is bigger. Have one edge on each on the same line. + int smallId = 1; + int largeId = 2; + MMesh smallCube = Primitives.AxisAlignedBox( + smallId, new Vector3(1, 1, 0), /* radius */ Vector3.one, /* material */ 2); + MMesh largeCube = Primitives.AxisAlignedBox( + largeId, Vector3.zero, /* radius */ Vector3.one * 2, /* material */ 2); + SpatialIndex index = new SpatialIndex(new Bounds(Vector3.zero, Vector3.one * 10)); + index.AddMesh(smallCube); + index.AddMesh(largeCube); + + // Find edges near a point that is near the overlappinge edges. + Vector3 point = new Vector3(2.01f, 2.03f, 0.05f); + List> edges; + NUnit.Framework.Assert.IsTrue(index.FindEdgesClosestTo(point, 0.25f, false, out edges)); + + // Should be two: + NUnit.Framework.Assert.AreEqual(2, edges.Count); + + // Find a vertex on the other side, near larger cube: + point = new Vector3(-2.01f, -2.03f, 1.05f); + NUnit.Framework.Assert.IsTrue(index.FindEdgesClosestTo(point, 0.25f, false, out edges)); + + // Should be one: + NUnit.Framework.Assert.AreEqual(1, edges.Count); + + // Smaller cube should be first: + NUnit.Framework.Assert.AreEqual(largeId, edges[0].value.meshId); + + // Look somwhere near neither cubes: + point = new Vector3(-3.01f, 4.03f, 1.05f); + NUnit.Framework.Assert.IsFalse(index.FindEdgesClosestTo(point, 0.25f, false, out edges)); + + // Remove the big cube, should only find stuff near small one. + index.RemoveMesh(largeCube); + point = new Vector3(2.01f, 2.03f, 0.05f); + NUnit.Framework.Assert.IsTrue(index.FindEdgesClosestTo(point, 0.25f, false, out edges)); + NUnit.Framework.Assert.AreEqual(1, edges.Count); + NUnit.Framework.Assert.AreEqual(smallId, edges[0].value.meshId); + } } - - [Test] - public void TestFindFace() { - // Add two cubes. One is bigger. Have one face from each in the same plane with the same center: - int smallId = 1; - int largeId = 2; - MMesh smallCube = Primitives.AxisAlignedBox( - smallId, new Vector3(1, 0, 0), /* radius */ Vector3.one, /* material */ 2); - MMesh largeCube = Primitives.AxisAlignedBox( - largeId, Vector3.zero, /* radius */ Vector3.one * 2, /* material */ 2); - SpatialIndex index = new SpatialIndex(new Bounds(Vector3.zero, Vector3.one * 10)); - index.AddMesh(smallCube); - index.AddMesh(largeCube); - - // For a point (practically) on both faces near their ceneter. - List> faces; - NUnit.Framework.Assert.IsTrue(index.FindFacesClosestTo(new Vector3(2.1f, 0, 0), 0.1f, false, out faces)); - - // Should be two faces: - NUnit.Framework.Assert.AreEqual(2, faces.Count); - - // Expect same results when looking for meshes, since we really just look for faces: - List> meshIds; - NUnit.Framework.Assert.IsTrue(index.FindMeshesClosestTo(new Vector3(2.1f, 0, 0), 0.1f, out meshIds)); - NUnit.Framework.Assert.AreEqual(2, meshIds.Count); - - // Move away from the center, outside the smaller bounding box: - NUnit.Framework.Assert.IsTrue(index.FindFacesClosestTo(new Vector3(2.1f, 1.1f, 1.1f), 0.1f, false, out faces)); - - // Should only be the larger cube: - NUnit.Framework.Assert.AreEqual(1, faces.Count); - NUnit.Framework.Assert.AreEqual(largeId, faces[0].value.meshId); - - // Remove the larger cube and try again. - index.RemoveMesh(largeCube); - // Near center: - NUnit.Framework.Assert.IsTrue(index.FindFacesClosestTo(new Vector3(2.1f, 0, 0), 0.1f, false, out faces)); - // Outside of small cube: - NUnit.Framework.Assert.IsFalse(index.FindFacesClosestTo(new Vector3(2.1f, 1.1f, 1.1f), 0.1f, false, out faces)); - } - - [Test] - public void TestFindEdge() { - // Add two cubes. One is bigger. Have one edge on each on the same line. - int smallId = 1; - int largeId = 2; - MMesh smallCube = Primitives.AxisAlignedBox( - smallId, new Vector3(1, 1, 0), /* radius */ Vector3.one, /* material */ 2); - MMesh largeCube = Primitives.AxisAlignedBox( - largeId, Vector3.zero, /* radius */ Vector3.one * 2, /* material */ 2); - SpatialIndex index = new SpatialIndex(new Bounds(Vector3.zero, Vector3.one * 10)); - index.AddMesh(smallCube); - index.AddMesh(largeCube); - - // Find edges near a point that is near the overlappinge edges. - Vector3 point = new Vector3(2.01f, 2.03f, 0.05f); - List> edges; - NUnit.Framework.Assert.IsTrue(index.FindEdgesClosestTo(point, 0.25f, false, out edges)); - - // Should be two: - NUnit.Framework.Assert.AreEqual(2, edges.Count); - - // Find a vertex on the other side, near larger cube: - point = new Vector3(-2.01f, -2.03f, 1.05f); - NUnit.Framework.Assert.IsTrue(index.FindEdgesClosestTo(point, 0.25f, false, out edges)); - - // Should be one: - NUnit.Framework.Assert.AreEqual(1, edges.Count); - - // Smaller cube should be first: - NUnit.Framework.Assert.AreEqual(largeId, edges[0].value.meshId); - - // Look somwhere near neither cubes: - point = new Vector3(-3.01f, 4.03f, 1.05f); - NUnit.Framework.Assert.IsFalse(index.FindEdgesClosestTo(point, 0.25f, false, out edges)); - - // Remove the big cube, should only find stuff near small one. - index.RemoveMesh(largeCube); - point = new Vector3(2.01f, 2.03f, 0.05f); - NUnit.Framework.Assert.IsTrue(index.FindEdgesClosestTo(point, 0.25f, false, out edges)); - NUnit.Framework.Assert.AreEqual(1, edges.Count); - NUnit.Framework.Assert.AreEqual(smallId, edges[0].value.meshId); - } - } } diff --git a/Assets/Editor/tests/model/csg/CsgCasesTest.cs b/Assets/Editor/tests/model/csg/CsgCasesTest.cs index a9c74df1..2d6c3ed2 100644 --- a/Assets/Editor/tests/model/csg/CsgCasesTest.cs +++ b/Assets/Editor/tests/model/csg/CsgCasesTest.cs @@ -26,265 +26,277 @@ /// into a coroutine in PeltzerMain). It makes it easy to /// spot obvious problems. /// -namespace com.google.apps.peltzer.client.model.csg { - [TestFixture] - public class CsgCasesTest { - [Test] - public void TestOne() { - Vector3 scale1 = new Vector3(0.06f, 0.06f, 0.06f); - Vector3 offset1 = new Vector3(-0.2588203f, 0.7604792f, 0.08228087f); - Quaternion rot1 = Quaternion.Euler(337.6894f, 345.5614f, 303.5857f); - - Vector3 scale2 = new Vector3(0.03f, 0.03f, 0.03f); - Vector3 offset2 = new Vector3(-0.2294091f, 0.7091361f, 0.06302771f); - Quaternion rot2 = Quaternion.Euler(355.6298f, 5.137078f, 291.4288f); - - MMesh shape1 = Primitives.AxisAlignedBox(0, Vector3.zero, scale1, 1); - shape1.offset = offset1; - shape1.rotation = rot1; - MMesh shape2 = Primitives.AxisAlignedCylinder(1, Vector3.zero, scale2, null, 2); - shape2.offset = offset2; - shape2.rotation = rot2; - - Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); - SpatialIndex spatialIndex = new SpatialIndex(bounds); - spatialIndex.AddMesh(shape1); - Model m = new Model(bounds); - m.AddMesh(shape1); - - NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); +namespace com.google.apps.peltzer.client.model.csg +{ + [TestFixture] + public class CsgCasesTest + { + [Test] + public void TestOne() + { + Vector3 scale1 = new Vector3(0.06f, 0.06f, 0.06f); + Vector3 offset1 = new Vector3(-0.2588203f, 0.7604792f, 0.08228087f); + Quaternion rot1 = Quaternion.Euler(337.6894f, 345.5614f, 303.5857f); + + Vector3 scale2 = new Vector3(0.03f, 0.03f, 0.03f); + Vector3 offset2 = new Vector3(-0.2294091f, 0.7091361f, 0.06302771f); + Quaternion rot2 = Quaternion.Euler(355.6298f, 5.137078f, 291.4288f); + + MMesh shape1 = Primitives.AxisAlignedBox(0, Vector3.zero, scale1, 1); + shape1.offset = offset1; + shape1.rotation = rot1; + MMesh shape2 = Primitives.AxisAlignedCylinder(1, Vector3.zero, scale2, null, 2); + shape2.offset = offset2; + shape2.rotation = rot2; + + Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); + SpatialIndex spatialIndex = new SpatialIndex(bounds); + spatialIndex.AddMesh(shape1); + Model m = new Model(bounds); + m.AddMesh(shape1); + + NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); + } + + [Test] + public void TestTwo() + { + Vector3 scale1 = new Vector3(0.05f, 0.05f, 0.05f); + Vector3 offset1 = new Vector3(-0.3578773f, 0.7828305f, -0.02011965f); + Quaternion rot1 = Quaternion.Euler(341.5305f, 311.4361f, 350.6624f); + + Vector3 scale2 = new Vector3(0.04f, 0.04f, 0.04f); + Vector3 offset2 = new Vector3(-0.3189396f, 0.741451f, -0.01238456f); + Quaternion rot2 = Quaternion.Euler(337.015f, 341.9556f, 302.1871f); + + MMesh shape1 = Primitives.AxisAlignedBox(0, Vector3.zero, scale1, 1); + shape1.offset = offset1; + shape1.rotation = rot1; + MMesh shape2 = Primitives.AxisAlignedCylinder(1, Vector3.zero, scale2, null, 2); + shape2.offset = offset2; + shape2.rotation = rot2; + + Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); + SpatialIndex spatialIndex = new SpatialIndex(bounds); + spatialIndex.AddMesh(shape1); + Model m = new Model(bounds); + m.AddMesh(shape1); + + NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); + } + + [Test] + public void TestThree() + { + Vector3 scale1 = new Vector3(0.09f, 0.09f, 0.09f); + Vector3 offset1 = new Vector3(-0.4822676f, 0.840179f, -0.2138236f); + Quaternion rot1 = Quaternion.Euler(325.8044f, 269.0391f, 340.9526f); + + Vector3 scale2 = new Vector3(0.04f, 0.04f, 0.04f); + Vector3 offset2 = new Vector3(-0.3975593f, 0.8605151f, -0.1950957f); + Quaternion rot2 = Quaternion.Euler(327.2958f, 267.8308f, 343.3483f); + + MMesh shape1 = Primitives.AxisAlignedBox(0, Vector3.zero, scale1, 1); + shape1.offset = offset1; + shape1.rotation = rot1; + MMesh shape2 = Primitives.AxisAlignedBox(1, Vector3.zero, scale2, 2); + shape2.offset = offset2; + shape2.rotation = rot2; + + Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); + SpatialIndex spatialIndex = new SpatialIndex(bounds); + spatialIndex.AddMesh(shape1); + Model m = new Model(bounds); + m.AddMesh(shape1); + + NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); + } + + [Test] + public void TestFour() + { + Vector3 scale1 = new Vector3(0.09f, 0.09f, 0.09f); + Vector3 offset1 = new Vector3(-0.3445286f, 0.7810592f, 0.1000385f); + Quaternion rot1 = Quaternion.Euler(335.4548f, 326.2354f, 311.7785f); + + Vector3 scale2 = new Vector3(0.03f, 0.03f, 0.03f); + Vector3 offset2 = new Vector3(-0.2409048f, 0.8018835f, 0.05735187f); + Quaternion rot2 = Quaternion.Euler(329.2861f, 336.1176f, 314.4811f); + + MMesh shape1 = Primitives.AxisAlignedBox(0, Vector3.zero, scale1, 1); + shape1.offset = offset1; + shape1.rotation = rot1; + MMesh shape2 = Primitives.AxisAlignedIcosphere(1, Vector3.zero, scale2, 2); + shape2.offset = offset2; + shape2.rotation = rot2; + + Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); + SpatialIndex spatialIndex = new SpatialIndex(bounds); + spatialIndex.AddMesh(shape1); + Model m = new Model(bounds); + m.AddMesh(shape1); + + NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); + } + + [Test] + public void TestFive() + { + Vector3 scale1 = new Vector3(0.04f, 0.04f, 0.04f); + Vector3 offset1 = new Vector3(-0.3964509f, 0.8120602f, -0.03975228f); + Quaternion rot1 = Quaternion.Euler(328.3585f, 286.2379f, 348.8317f); + + Vector3 scale2 = new Vector3(0.03f, 0.03f, 0.03f); + Vector3 offset2 = new Vector3(-0.3812525f, 0.8245335f, -0.03840983f); + Quaternion rot2 = Quaternion.Euler(331.8435f, 304.2563f, 339.4354f); + + MMesh shape1 = Primitives.AxisAlignedBox(0, Vector3.zero, scale1, 1); + shape1.offset = offset1; + shape1.rotation = rot1; + MMesh shape2 = Primitives.AxisAlignedIcosphere(1, Vector3.zero, scale2, 2); + shape2.offset = offset2; + shape2.rotation = rot2; + + Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); + SpatialIndex spatialIndex = new SpatialIndex(bounds); + spatialIndex.AddMesh(shape1); + Model m = new Model(bounds); + m.AddMesh(shape1); + + NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); + } + + [Test] + public void TestSix() + { + Vector3 scale1 = new Vector3(0.06f, 0.06f, 0.06f); + Vector3 offset1 = new Vector3(-0.2539979f, 0.8296162f, -0.01753259f); + Quaternion rot1 = Quaternion.Euler(323.3995f, 335.1265f, 290.1707f); + + Vector3 scale2 = new Vector3(0.03f, 0.03f, 0.03f); + Vector3 offset2 = new Vector3(-0.2052484f, 0.8434253f, -0.02635695f); + Quaternion rot2 = Quaternion.Euler(317.0431f, 355.3317f, 294.1518f); + + MMesh shape1 = Primitives.AxisAlignedIcosphere(0, Vector3.zero, scale1, 1); + shape1.offset = offset1; + shape1.rotation = rot1; + MMesh shape2 = Primitives.AxisAlignedCylinder(1, Vector3.zero, scale2, null, 2); + shape2.offset = offset2; + shape2.rotation = rot2; + + Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); + SpatialIndex spatialIndex = new SpatialIndex(bounds); + spatialIndex.AddMesh(shape1); + Model m = new Model(bounds); + m.AddMesh(shape1); + + NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); + } + + [Test] + public void TestSeven() + { + Vector3 scale1 = new Vector3(0.09f, 0.09f, 0.09f); + Vector3 offset1 = new Vector3(-0.3976895f, 0.8503361f, -0.167705f); + Quaternion rot1 = Quaternion.Euler(314.5735f, 284.1546f, 350.7589f); + Vector3 scale2 = new Vector3(0.14f, 0.14f, 0.14f); + Vector3 offset2 = new Vector3(-0.3232127f, 0.9357966f, -0.182199f); + Quaternion rot2 = Quaternion.Euler(307.9687f, 283.914f, 348.6634f); + + MMesh shape1 = Primitives.AxisAlignedBox(0, Vector3.zero, scale1, 1); + shape1.offset = offset1; + shape1.rotation = rot1; + MMesh shape2 = Primitives.AxisAlignedBox(1, Vector3.zero, scale2, 2); + shape2.offset = offset2; + shape2.rotation = rot2; + + Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); + SpatialIndex spatialIndex = new SpatialIndex(bounds); + spatialIndex.AddMesh(shape1); + Model m = new Model(bounds); + m.AddMesh(shape1); + + NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); + } + + [Test] + public void TestEight() + { + Vector3 scale1 = new Vector3(0.17f, 0.17f, 0.17f); + Vector3 offset1 = new Vector3(-0.4381654f, 0.8735071f, -0.2654698f); + Quaternion rot1 = Quaternion.Euler(324.5257f, 267.3036f, 1.283594f); + Vector3 scale2 = new Vector3(0.18f, 0.18f, 0.18f); + Vector3 offset2 = new Vector3(-0.4458212f, 0.8969067f, -0.2600397f); + Quaternion rot2 = Quaternion.Euler(323.0872f, 274.6339f, 355.8188f); + + MMesh shape1 = Primitives.AxisAlignedBox(0, Vector3.zero, scale1, 1); + shape1.offset = offset1; + shape1.rotation = rot1; + MMesh shape2 = Primitives.AxisAlignedBox(1, Vector3.zero, scale2, 2); + shape2.offset = offset2; + shape2.rotation = rot2; + + Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); + SpatialIndex spatialIndex = new SpatialIndex(bounds); + spatialIndex.AddMesh(shape1); + Model m = new Model(bounds); + m.AddMesh(shape1); + + NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); + } + + [Test] + public void TestNine() + { + Vector3 scale1 = new Vector3(0.09999999f, 0.09999999f, 0.09999999f); + Vector3 offset1 = new Vector3(-0.2459444f, 0.8127318f, -0.02276482f); + Quaternion rot1 = Quaternion.Euler(323.0423f, 342.4444f, 312.1532f); + + Vector3 scale2 = new Vector3(0.05f, 0.05f, 0.05f); + Vector3 offset2 = new Vector3(-0.1958988f, 0.755046f, 0.02580362f); + Quaternion rot2 = Quaternion.Euler(333.4003f, 346.5612f, 333.1861f); + + MMesh shape1 = Primitives.Torus(0, Vector3.zero, scale1, 1); + shape1.offset = offset1; + shape1.rotation = rot1; + MMesh shape2 = Primitives.AxisAlignedIcosphere(1, Vector3.zero, scale2, 2); + shape2.offset = offset2; + shape2.rotation = rot2; + + Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); + SpatialIndex spatialIndex = new SpatialIndex(bounds); + spatialIndex.AddMesh(shape1); + Model m = new Model(bounds); + m.AddMesh(shape1); + + NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); + } + + [Test] + public void TestTen() + { + Vector3 scale1 = new Vector3(0.13f, 0.13f, 0.13f); + Vector3 offset1 = new Vector3(-0.3696883f, 0.8180166f, 0.09428416f); + Quaternion rot1 = Quaternion.Euler(329.1212f, 310.4952f, 342.6105f); + + Vector3 scale2 = new Vector3(0.2f, 0.2f, 0.2f); + Vector3 offset2 = new Vector3(-0.3002505f, 0.8771363f, 0.1747152f); + Quaternion rot2 = Quaternion.Euler(327.2445f, 318.5047f, 26.39776f); + + MMesh shape1 = Primitives.AxisAlignedIcosphere(0, Vector3.zero, scale1, 1); + shape1.offset = offset1; + shape1.rotation = rot1; + MMesh shape2 = Primitives.Torus(1, Vector3.zero, scale2, 2); + shape2.offset = offset2; + shape2.rotation = rot2; + + Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); + SpatialIndex spatialIndex = new SpatialIndex(bounds); + spatialIndex.AddMesh(shape1); + Model m = new Model(bounds); + m.AddMesh(shape1); + + NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); + } } - - [Test] - public void TestTwo() { - Vector3 scale1 = new Vector3(0.05f, 0.05f, 0.05f); - Vector3 offset1 = new Vector3(-0.3578773f, 0.7828305f, -0.02011965f); - Quaternion rot1 = Quaternion.Euler(341.5305f, 311.4361f, 350.6624f); - - Vector3 scale2 = new Vector3(0.04f, 0.04f, 0.04f); - Vector3 offset2 = new Vector3(-0.3189396f, 0.741451f, -0.01238456f); - Quaternion rot2 = Quaternion.Euler(337.015f, 341.9556f, 302.1871f); - - MMesh shape1 = Primitives.AxisAlignedBox(0, Vector3.zero, scale1, 1); - shape1.offset = offset1; - shape1.rotation = rot1; - MMesh shape2 = Primitives.AxisAlignedCylinder(1, Vector3.zero, scale2, null, 2); - shape2.offset = offset2; - shape2.rotation = rot2; - - Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); - SpatialIndex spatialIndex = new SpatialIndex(bounds); - spatialIndex.AddMesh(shape1); - Model m = new Model(bounds); - m.AddMesh(shape1); - - NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); - } - - [Test] - public void TestThree() { - Vector3 scale1 = new Vector3(0.09f, 0.09f, 0.09f); - Vector3 offset1 = new Vector3(-0.4822676f, 0.840179f, -0.2138236f); - Quaternion rot1 = Quaternion.Euler(325.8044f, 269.0391f, 340.9526f); - - Vector3 scale2 = new Vector3(0.04f, 0.04f, 0.04f); - Vector3 offset2 = new Vector3(-0.3975593f, 0.8605151f, -0.1950957f); - Quaternion rot2 = Quaternion.Euler(327.2958f, 267.8308f, 343.3483f); - - MMesh shape1 = Primitives.AxisAlignedBox(0, Vector3.zero, scale1, 1); - shape1.offset = offset1; - shape1.rotation = rot1; - MMesh shape2 = Primitives.AxisAlignedBox(1, Vector3.zero, scale2, 2); - shape2.offset = offset2; - shape2.rotation = rot2; - - Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); - SpatialIndex spatialIndex = new SpatialIndex(bounds); - spatialIndex.AddMesh(shape1); - Model m = new Model(bounds); - m.AddMesh(shape1); - - NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); - } - - [Test] - public void TestFour() { - Vector3 scale1 = new Vector3(0.09f, 0.09f, 0.09f); - Vector3 offset1 = new Vector3(-0.3445286f, 0.7810592f, 0.1000385f); - Quaternion rot1 = Quaternion.Euler(335.4548f, 326.2354f, 311.7785f); - - Vector3 scale2 = new Vector3(0.03f, 0.03f, 0.03f); - Vector3 offset2 = new Vector3(-0.2409048f, 0.8018835f, 0.05735187f); - Quaternion rot2 = Quaternion.Euler(329.2861f, 336.1176f, 314.4811f); - - MMesh shape1 = Primitives.AxisAlignedBox(0, Vector3.zero, scale1, 1); - shape1.offset = offset1; - shape1.rotation = rot1; - MMesh shape2 = Primitives.AxisAlignedIcosphere(1, Vector3.zero, scale2, 2); - shape2.offset = offset2; - shape2.rotation = rot2; - - Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); - SpatialIndex spatialIndex = new SpatialIndex(bounds); - spatialIndex.AddMesh(shape1); - Model m = new Model(bounds); - m.AddMesh(shape1); - - NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); - } - - [Test] - public void TestFive() { - Vector3 scale1 = new Vector3(0.04f, 0.04f, 0.04f); - Vector3 offset1 = new Vector3(-0.3964509f, 0.8120602f, -0.03975228f); - Quaternion rot1 = Quaternion.Euler(328.3585f, 286.2379f, 348.8317f); - - Vector3 scale2 = new Vector3(0.03f, 0.03f, 0.03f); - Vector3 offset2 = new Vector3(-0.3812525f, 0.8245335f, -0.03840983f); - Quaternion rot2 = Quaternion.Euler(331.8435f, 304.2563f, 339.4354f); - - MMesh shape1 = Primitives.AxisAlignedBox(0, Vector3.zero, scale1, 1); - shape1.offset = offset1; - shape1.rotation = rot1; - MMesh shape2 = Primitives.AxisAlignedIcosphere(1, Vector3.zero, scale2, 2); - shape2.offset = offset2; - shape2.rotation = rot2; - - Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); - SpatialIndex spatialIndex = new SpatialIndex(bounds); - spatialIndex.AddMesh(shape1); - Model m = new Model(bounds); - m.AddMesh(shape1); - - NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); - } - - [Test] - public void TestSix() { - Vector3 scale1 = new Vector3(0.06f, 0.06f, 0.06f); - Vector3 offset1 = new Vector3(-0.2539979f, 0.8296162f, -0.01753259f); - Quaternion rot1 = Quaternion.Euler(323.3995f, 335.1265f, 290.1707f); - - Vector3 scale2 = new Vector3(0.03f, 0.03f, 0.03f); - Vector3 offset2 = new Vector3(-0.2052484f, 0.8434253f, -0.02635695f); - Quaternion rot2 = Quaternion.Euler(317.0431f, 355.3317f, 294.1518f); - - MMesh shape1 = Primitives.AxisAlignedIcosphere(0, Vector3.zero, scale1, 1); - shape1.offset = offset1; - shape1.rotation = rot1; - MMesh shape2 = Primitives.AxisAlignedCylinder(1, Vector3.zero, scale2, null, 2); - shape2.offset = offset2; - shape2.rotation = rot2; - - Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); - SpatialIndex spatialIndex = new SpatialIndex(bounds); - spatialIndex.AddMesh(shape1); - Model m = new Model(bounds); - m.AddMesh(shape1); - - NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); - } - - [Test] - public void TestSeven() { - Vector3 scale1 = new Vector3(0.09f, 0.09f, 0.09f); - Vector3 offset1 = new Vector3(-0.3976895f, 0.8503361f, -0.167705f); - Quaternion rot1 = Quaternion.Euler(314.5735f, 284.1546f, 350.7589f); - Vector3 scale2 = new Vector3(0.14f, 0.14f, 0.14f); - Vector3 offset2 = new Vector3(-0.3232127f, 0.9357966f, -0.182199f); - Quaternion rot2 = Quaternion.Euler(307.9687f, 283.914f, 348.6634f); - - MMesh shape1 = Primitives.AxisAlignedBox(0, Vector3.zero, scale1, 1); - shape1.offset = offset1; - shape1.rotation = rot1; - MMesh shape2 = Primitives.AxisAlignedBox(1, Vector3.zero, scale2, 2); - shape2.offset = offset2; - shape2.rotation = rot2; - - Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); - SpatialIndex spatialIndex = new SpatialIndex(bounds); - spatialIndex.AddMesh(shape1); - Model m = new Model(bounds); - m.AddMesh(shape1); - - NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); - } - - [Test] - public void TestEight() { - Vector3 scale1 = new Vector3(0.17f, 0.17f, 0.17f); - Vector3 offset1 = new Vector3(-0.4381654f, 0.8735071f, -0.2654698f); - Quaternion rot1 = Quaternion.Euler(324.5257f, 267.3036f, 1.283594f); - Vector3 scale2 = new Vector3(0.18f, 0.18f, 0.18f); - Vector3 offset2 = new Vector3(-0.4458212f, 0.8969067f, -0.2600397f); - Quaternion rot2 = Quaternion.Euler(323.0872f, 274.6339f, 355.8188f); - - MMesh shape1 = Primitives.AxisAlignedBox(0, Vector3.zero, scale1, 1); - shape1.offset = offset1; - shape1.rotation = rot1; - MMesh shape2 = Primitives.AxisAlignedBox(1, Vector3.zero, scale2, 2); - shape2.offset = offset2; - shape2.rotation = rot2; - - Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); - SpatialIndex spatialIndex = new SpatialIndex(bounds); - spatialIndex.AddMesh(shape1); - Model m = new Model(bounds); - m.AddMesh(shape1); - - NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); - } - - [Test] - public void TestNine() { - Vector3 scale1 = new Vector3(0.09999999f, 0.09999999f, 0.09999999f); - Vector3 offset1 = new Vector3(-0.2459444f, 0.8127318f, -0.02276482f); - Quaternion rot1 = Quaternion.Euler(323.0423f, 342.4444f, 312.1532f); - - Vector3 scale2 = new Vector3(0.05f, 0.05f, 0.05f); - Vector3 offset2 = new Vector3(-0.1958988f, 0.755046f, 0.02580362f); - Quaternion rot2 = Quaternion.Euler(333.4003f, 346.5612f, 333.1861f); - - MMesh shape1 = Primitives.Torus(0, Vector3.zero, scale1, 1); - shape1.offset = offset1; - shape1.rotation = rot1; - MMesh shape2 = Primitives.AxisAlignedIcosphere(1, Vector3.zero, scale2, 2); - shape2.offset = offset2; - shape2.rotation = rot2; - - Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); - SpatialIndex spatialIndex = new SpatialIndex(bounds); - spatialIndex.AddMesh(shape1); - Model m = new Model(bounds); - m.AddMesh(shape1); - - NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); - } - - [Test] - public void TestTen() { - Vector3 scale1 = new Vector3(0.13f, 0.13f, 0.13f); - Vector3 offset1 = new Vector3(-0.3696883f, 0.8180166f, 0.09428416f); - Quaternion rot1 = Quaternion.Euler(329.1212f, 310.4952f, 342.6105f); - - Vector3 scale2 = new Vector3(0.2f, 0.2f, 0.2f); - Vector3 offset2 = new Vector3(-0.3002505f, 0.8771363f, 0.1747152f); - Quaternion rot2 = Quaternion.Euler(327.2445f, 318.5047f, 26.39776f); - - MMesh shape1 = Primitives.AxisAlignedIcosphere(0, Vector3.zero, scale1, 1); - shape1.offset = offset1; - shape1.rotation = rot1; - MMesh shape2 = Primitives.Torus(1, Vector3.zero, scale2, 2); - shape2.offset = offset2; - shape2.rotation = rot2; - - Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 10.0f); - SpatialIndex spatialIndex = new SpatialIndex(bounds); - spatialIndex.AddMesh(shape1); - Model m = new Model(bounds); - m.AddMesh(shape1); - - NUnit.Framework.Assert.IsTrue(CsgOperations.SubtractMeshFromModel(m, spatialIndex, shape2)); - } - } } \ No newline at end of file diff --git a/Assets/Editor/tests/model/csg/CsgMathTest.cs b/Assets/Editor/tests/model/csg/CsgMathTest.cs index a6933e68..f4c6bed6 100644 --- a/Assets/Editor/tests/model/csg/CsgMathTest.cs +++ b/Assets/Editor/tests/model/csg/CsgMathTest.cs @@ -20,80 +20,90 @@ using UnityEngine; using com.google.apps.peltzer.client.model.core; -namespace com.google.apps.peltzer.client.model.csg { - [TestFixture] - public class CsgMathTest { +namespace com.google.apps.peltzer.client.model.csg +{ + [TestFixture] + public class CsgMathTest + { - [Test] - public void TestPointOnPlane() { - Plane plane = new Plane(new Vector3(-1, 5, -1), new Vector3(1, 5, -1), new Vector3(1, 5, 1)); - Vector3 p = CsgMath.PointOnPlane(plane); - NUnit.Framework.Assert.AreEqual(p.y, 5f, 0.001f); - } + [Test] + public void TestPointOnPlane() + { + Plane plane = new Plane(new Vector3(-1, 5, -1), new Vector3(1, 5, -1), new Vector3(1, 5, 1)); + Vector3 p = CsgMath.PointOnPlane(plane); + NUnit.Framework.Assert.AreEqual(p.y, 5f, 0.001f); + } - [Test] - public void TestRayPlaneIntersection() { - Plane plane = new Plane(new Vector3(-1, 5, -1), new Vector3(1, 5, -1), new Vector3(1, 5, 1)); - Vector3 intersection; + [Test] + public void TestRayPlaneIntersection() + { + Plane plane = new Plane(new Vector3(-1, 5, -1), new Vector3(1, 5, -1), new Vector3(1, 5, 1)); + Vector3 intersection; - // Plane in front of Ray: - CsgMath.RayPlaneIntersection(out intersection, Vector3.zero, new Vector3(0, 1, 0), plane); - NUnit.Framework.Assert.AreEqual(0, Vector3.Distance(intersection, new Vector3(0, 5, 0)), 0.001f); + // Plane in front of Ray: + CsgMath.RayPlaneIntersection(out intersection, Vector3.zero, new Vector3(0, 1, 0), plane); + NUnit.Framework.Assert.AreEqual(0, Vector3.Distance(intersection, new Vector3(0, 5, 0)), 0.001f); - // Plane behind Ray: - CsgMath.RayPlaneIntersection(out intersection, Vector3.zero, new Vector3(0, -1, 0), plane); - NUnit.Framework.Assert.AreEqual(0, Vector3.Distance(intersection, new Vector3(0, 5, 0)), 0.001f); + // Plane behind Ray: + CsgMath.RayPlaneIntersection(out intersection, Vector3.zero, new Vector3(0, -1, 0), plane); + NUnit.Framework.Assert.AreEqual(0, Vector3.Distance(intersection, new Vector3(0, 5, 0)), 0.001f); - // On ray start: - CsgMath.RayPlaneIntersection(out intersection, new Vector3(0, 5, 0), new Vector3(0, -1, 0), plane); - NUnit.Framework.Assert.AreEqual(0, Vector3.Distance(intersection, new Vector3(0, 5, 0)), 0.001f); - } + // On ray start: + CsgMath.RayPlaneIntersection(out intersection, new Vector3(0, 5, 0), new Vector3(0, -1, 0), plane); + NUnit.Framework.Assert.AreEqual(0, Vector3.Distance(intersection, new Vector3(0, 5, 0)), 0.001f); + } - [Test] - public void TestIsInside() { - AssertInside(Vector3.zero, - new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); - AssertInside(new Vector3(0.9f, 0, 0.9f), - new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); - AssertInside(new Vector3(-0.9f, 0, 0.9f), - new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); + [Test] + public void TestIsInside() + { + AssertInside(Vector3.zero, + new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); + AssertInside(new Vector3(0.9f, 0, 0.9f), + new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); + AssertInside(new Vector3(-0.9f, 0, 0.9f), + new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); - AssertNotInside(new Vector3(-1.5f, 0, 0), - new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); - AssertNotInside(new Vector3(-1.5f, 0, -1.1f), - new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); - AssertNotInside(new Vector3(-1.5f, 0, 0.9f), - new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); + AssertNotInside(new Vector3(-1.5f, 0, 0), + new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); + AssertNotInside(new Vector3(-1.5f, 0, -1.1f), + new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); + AssertNotInside(new Vector3(-1.5f, 0, 0.9f), + new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); - AssertOnBorder(new Vector3(-1, 0, 0), - new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); - AssertOnBorder(new Vector3(1, 0, 0), - new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); - AssertOnBorder(new Vector3(-1, 0, -1), - new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); - } + AssertOnBorder(new Vector3(-1, 0, 0), + new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); + AssertOnBorder(new Vector3(1, 0, 0), + new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); + AssertOnBorder(new Vector3(-1, 0, -1), + new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); + } - private void AssertInside(Vector3 point, params Vector3[] verts) { - NUnit.Framework.Assert.AreEqual(1, CsgMath.IsInside(toPoly(verts), point), - "Point should be inside polygon"); - } + private void AssertInside(Vector3 point, params Vector3[] verts) + { + NUnit.Framework.Assert.AreEqual(1, CsgMath.IsInside(toPoly(verts), point), + "Point should be inside polygon"); + } - private void AssertNotInside(Vector3 point, params Vector3[] verts) { - NUnit.Framework.Assert.AreEqual(-1, CsgMath.IsInside(toPoly(verts), point), - "Point should not be inside polygon"); - } + private void AssertNotInside(Vector3 point, params Vector3[] verts) + { + NUnit.Framework.Assert.AreEqual(-1, CsgMath.IsInside(toPoly(verts), point), + "Point should not be inside polygon"); + } - private void AssertOnBorder(Vector3 point, params Vector3[] verts) { - NUnit.Framework.Assert.AreEqual(0, CsgMath.IsInside(toPoly(verts), point), - "Point should be on polygon border"); - } + private void AssertOnBorder(Vector3 point, params Vector3[] verts) + { + NUnit.Framework.Assert.AreEqual(0, CsgMath.IsInside(toPoly(verts), point), + "Point should be on polygon border"); + } - private CsgPolygon toPoly(params Vector3[] verts) { - List poly = new List(); - foreach (Vector3 vert in verts) { - poly.Add(new CsgVertex(vert)); - } - return new CsgPolygon(poly, new core.FaceProperties()); + private CsgPolygon toPoly(params Vector3[] verts) + { + List poly = new List(); + foreach (Vector3 vert in verts) + { + poly.Add(new CsgVertex(vert)); + } + return new CsgPolygon(poly, new core.FaceProperties()); + } } - } } diff --git a/Assets/Editor/tests/model/csg/CsgOperationsTest.cs b/Assets/Editor/tests/model/csg/CsgOperationsTest.cs index 97b54aa4..82141eac 100644 --- a/Assets/Editor/tests/model/csg/CsgOperationsTest.cs +++ b/Assets/Editor/tests/model/csg/CsgOperationsTest.cs @@ -21,322 +21,359 @@ using com.google.apps.peltzer.client.model.core; using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.model.csg { - [TestFixture] - public class CsgOperationsTest { - - [Test] - public void TestSubtractFromModel() { - Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 5); - Model model = new Model(bounds); - - SpatialIndex spatialIndex = new SpatialIndex(model, bounds); - - int toErase = 1; - int toIntersect = 2; - int toIgnore = 3; - - // Add a small cube at the center: - MMesh meshToAdd = Primitives.AxisAlignedBox(toErase, Vector3.zero, Vector3.one * 0.5f, 1); - model.AddMesh(meshToAdd); - spatialIndex.AddMesh(meshToAdd); - - - // Add one nearby: - meshToAdd = Primitives.AxisAlignedBox(toIntersect, Vector3.one, Vector3.one, 1); - model.AddMesh(meshToAdd); - spatialIndex.AddMesh(meshToAdd); - - // Add another further away: - meshToAdd = Primitives.AxisAlignedBox(toIgnore, Vector3.one * 2, Vector3.one * 0.5f, 1); - model.AddMesh(meshToAdd); - spatialIndex.AddMesh(meshToAdd); - - // Now subtract a big cube from the model: - bool subtracted = CsgOperations.SubtractMeshFromModel( - model, spatialIndex, Primitives.AxisAlignedBox(7, Vector3.zero, Vector3.one, 1)); - NUnit.Framework.Assert.IsTrue(subtracted); - - // Should have completely erased the small one in the center: - NUnit.Framework.Assert.IsFalse(model.HasMesh(toErase)); - - // Other two should still be there: - NUnit.Framework.Assert.IsTrue(model.HasMesh(toIntersect)); - NUnit.Framework.Assert.IsTrue(model.HasMesh(toIgnore)); - - // The one that intersected will end up with more faces. - NUnit.Framework.Assert.AreEqual(15, model.GetMesh(toIntersect).faceCount); - - // Subtract away from the scene to make sure the method returns false. - NUnit.Framework.Assert.IsFalse(CsgOperations.SubtractMeshFromModel(model, spatialIndex, - Primitives.AxisAlignedBox(7, Vector3.one * -3, Vector3.one, 1))); - } - - [Test] - public void RaycastTest() { - CsgPolygon unitSquareNegativeYAtZero = toPoly( - new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); - CsgPolygon unitSquarePositiveYAtZero = toPoly( - new Vector3(-1, 0, 1), new Vector3(1, 0, 1), new Vector3(1, 0, -1), new Vector3(-1, 0, -1)); - CsgPolygon unitSquarePositiveYAtMinusOne = toPoly( - new Vector3(-1, -1, 1), new Vector3(1, -1, 1), new Vector3(1, -1, -1), new Vector3(-1, -1, -1)); - CsgPolygon unitSquareNegativeYAtMinusOne = toPoly( - new Vector3(-1, -1, -1), new Vector3(1, -1, -1), new Vector3(1, -1, 1), new Vector3(-1, -1, 1)); - CsgPolygon unitSquareShiftedOneNegativeYAtZero = toPoly( - new Vector3(0, 0, -1), new Vector3(2, 0, -1), new Vector3(2, 0, 1), new Vector3(0, 0, 1)); - - AssertPolyStatus(PolygonStatus.INSIDE, unitSquareNegativeYAtZero, toObj(unitSquareNegativeYAtMinusOne)); - AssertPolyStatus(PolygonStatus.OUTSIDE, unitSquareNegativeYAtZero, toObj(unitSquarePositiveYAtMinusOne)); - AssertPolyStatus(PolygonStatus.SAME, unitSquareNegativeYAtZero, toObj(unitSquareNegativeYAtZero)); - AssertPolyStatus(PolygonStatus.OPPOSITE, unitSquareNegativeYAtZero, toObj(unitSquarePositiveYAtZero)); - AssertPolyStatus(PolygonStatus.SAME, unitSquareNegativeYAtZero, - toObj(unitSquareNegativeYAtZero, unitSquareShiftedOneNegativeYAtZero)); - AssertPolyStatus(PolygonStatus.SAME, unitSquareNegativeYAtZero, - toObj(unitSquareShiftedOneNegativeYAtZero, unitSquareNegativeYAtZero)); - } - - [Test] - public void RaycastCubeWithinCube() { - CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); - // Small cube inside of a larger cube, both at the origin. - CsgObject smallCube = CsgOperations.ToCsg(ctx, Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one * 0.5f, 1)); - CsgObject largeCube = CsgOperations.ToCsg(ctx, Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, 1)); - - // All faces of small cube should be INSIDE the large cube: - foreach (CsgPolygon poly in smallCube.polygons) { - AssertPolyStatus(PolygonStatus.INSIDE, poly, largeCube); - } - - // All faces of large cube should be OUTSIDE small cube. - foreach (CsgPolygon poly in largeCube.polygons) { - AssertPolyStatus(PolygonStatus.OUTSIDE, poly, smallCube); - } - } - - [Test] - public void RaycastCubeTouchingCube() { - CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 10f)); - // Two cubes next to each other, touching. - CsgObject leftCube = CsgOperations.ToCsg(ctx, Primitives.AxisAlignedBox(1, new Vector3(-2, 0, 0), Vector3.one, 1)); - CsgObject rightCube = CsgOperations.ToCsg(ctx, Primitives.AxisAlignedBox(2, Vector3.zero, Vector3.one, 1)); - - // Classify all faces in both cubes. - foreach (CsgPolygon poly in leftCube.polygons) { - CsgOperations.ClassifyPolygonUsingRaycast(poly, rightCube); - } - foreach (CsgPolygon poly in rightCube.polygons) { - CsgOperations.ClassifyPolygonUsingRaycast(poly, leftCube); - } - - // We expect all faces to be OUTSIDE except one from each cube. Those should bother be OPPOSITE. - CsgPolygon leftPoly = null; - CsgPolygon rightPoly = null; - foreach (CsgPolygon poly in leftCube.polygons) { - if (poly.status == PolygonStatus.OPPOSITE) { - NUnit.Framework.Assert.Null(leftPoly, "Should be only one poly that is OPPOSITE"); - leftPoly = poly; - } else { - NUnit.Framework.Assert.AreEqual(PolygonStatus.OUTSIDE, poly.status); - } - } - foreach (CsgPolygon poly in rightCube.polygons) { - if (poly.status == PolygonStatus.OPPOSITE) { - NUnit.Framework.Assert.Null(rightPoly, "Should be only one poly that is OPPOSITE"); - rightPoly = poly; - } else { - NUnit.Framework.Assert.AreEqual(PolygonStatus.OUTSIDE, poly.status); +namespace com.google.apps.peltzer.client.model.csg +{ + [TestFixture] + public class CsgOperationsTest + { + + [Test] + public void TestSubtractFromModel() + { + Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 5); + Model model = new Model(bounds); + + SpatialIndex spatialIndex = new SpatialIndex(model, bounds); + + int toErase = 1; + int toIntersect = 2; + int toIgnore = 3; + + // Add a small cube at the center: + MMesh meshToAdd = Primitives.AxisAlignedBox(toErase, Vector3.zero, Vector3.one * 0.5f, 1); + model.AddMesh(meshToAdd); + spatialIndex.AddMesh(meshToAdd); + + + // Add one nearby: + meshToAdd = Primitives.AxisAlignedBox(toIntersect, Vector3.one, Vector3.one, 1); + model.AddMesh(meshToAdd); + spatialIndex.AddMesh(meshToAdd); + + // Add another further away: + meshToAdd = Primitives.AxisAlignedBox(toIgnore, Vector3.one * 2, Vector3.one * 0.5f, 1); + model.AddMesh(meshToAdd); + spatialIndex.AddMesh(meshToAdd); + + // Now subtract a big cube from the model: + bool subtracted = CsgOperations.SubtractMeshFromModel( + model, spatialIndex, Primitives.AxisAlignedBox(7, Vector3.zero, Vector3.one, 1)); + NUnit.Framework.Assert.IsTrue(subtracted); + + // Should have completely erased the small one in the center: + NUnit.Framework.Assert.IsFalse(model.HasMesh(toErase)); + + // Other two should still be there: + NUnit.Framework.Assert.IsTrue(model.HasMesh(toIntersect)); + NUnit.Framework.Assert.IsTrue(model.HasMesh(toIgnore)); + + // The one that intersected will end up with more faces. + NUnit.Framework.Assert.AreEqual(15, model.GetMesh(toIntersect).faceCount); + + // Subtract away from the scene to make sure the method returns false. + NUnit.Framework.Assert.IsFalse(CsgOperations.SubtractMeshFromModel(model, spatialIndex, + Primitives.AxisAlignedBox(7, Vector3.one * -3, Vector3.one, 1))); } - } - NUnit.Framework.Assert.NotNull(leftPoly); - NUnit.Framework.Assert.NotNull(rightPoly); + [Test] + public void RaycastTest() + { + CsgPolygon unitSquareNegativeYAtZero = toPoly( + new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); + CsgPolygon unitSquarePositiveYAtZero = toPoly( + new Vector3(-1, 0, 1), new Vector3(1, 0, 1), new Vector3(1, 0, -1), new Vector3(-1, 0, -1)); + CsgPolygon unitSquarePositiveYAtMinusOne = toPoly( + new Vector3(-1, -1, 1), new Vector3(1, -1, 1), new Vector3(1, -1, -1), new Vector3(-1, -1, -1)); + CsgPolygon unitSquareNegativeYAtMinusOne = toPoly( + new Vector3(-1, -1, -1), new Vector3(1, -1, -1), new Vector3(1, -1, 1), new Vector3(-1, -1, 1)); + CsgPolygon unitSquareShiftedOneNegativeYAtZero = toPoly( + new Vector3(0, 0, -1), new Vector3(2, 0, -1), new Vector3(2, 0, 1), new Vector3(0, 0, 1)); + + AssertPolyStatus(PolygonStatus.INSIDE, unitSquareNegativeYAtZero, toObj(unitSquareNegativeYAtMinusOne)); + AssertPolyStatus(PolygonStatus.OUTSIDE, unitSquareNegativeYAtZero, toObj(unitSquarePositiveYAtMinusOne)); + AssertPolyStatus(PolygonStatus.SAME, unitSquareNegativeYAtZero, toObj(unitSquareNegativeYAtZero)); + AssertPolyStatus(PolygonStatus.OPPOSITE, unitSquareNegativeYAtZero, toObj(unitSquarePositiveYAtZero)); + AssertPolyStatus(PolygonStatus.SAME, unitSquareNegativeYAtZero, + toObj(unitSquareNegativeYAtZero, unitSquareShiftedOneNegativeYAtZero)); + AssertPolyStatus(PolygonStatus.SAME, unitSquareNegativeYAtZero, + toObj(unitSquareShiftedOneNegativeYAtZero, unitSquareNegativeYAtZero)); + } - // Barycenters should be the same, normals should be opposite - NUnit.Framework.Assert.AreEqual(0f, Vector3.Distance(leftPoly.baryCenter, rightPoly.baryCenter), 0.001f); - NUnit.Framework.Assert.AreEqual(-1f, Vector3.Dot(leftPoly.plane.normal, rightPoly.plane.normal), 0.001f); - } + [Test] + public void RaycastCubeWithinCube() + { + CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); + // Small cube inside of a larger cube, both at the origin. + CsgObject smallCube = CsgOperations.ToCsg(ctx, Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one * 0.5f, 1)); + CsgObject largeCube = CsgOperations.ToCsg(ctx, Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, 1)); + + // All faces of small cube should be INSIDE the large cube: + foreach (CsgPolygon poly in smallCube.polygons) + { + AssertPolyStatus(PolygonStatus.INSIDE, poly, largeCube); + } + + // All faces of large cube should be OUTSIDE small cube. + foreach (CsgPolygon poly in largeCube.polygons) + { + AssertPolyStatus(PolygonStatus.OUTSIDE, poly, smallCube); + } + } - [Test] - public void RaycastCubeOverlappingCube() { - CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); - - // Two cubes next to each other, overlapping. - CsgObject leftCube = CsgOperations.ToCsg(ctx, - Primitives.AxisAlignedBox(1, new Vector3(-1, 0, 0), Vector3.one, 1)); - CsgObject rightCube = CsgOperations.ToCsg(ctx, Primitives.AxisAlignedBox(2, Vector3.zero, Vector3.one, 1)); - - // Classify all faces in both cubes. - // Each obj should have 4 polys SAME, 1 OUTSIDE and 1 INSIDE - int numInside = 0; - int numOutside = 0; - int numSame = 0; - foreach (CsgPolygon poly in leftCube.polygons) { - CsgOperations.ClassifyPolygonUsingRaycast(poly, rightCube); - switch (poly.status) { - case PolygonStatus.INSIDE: - numInside++; - break; - case PolygonStatus.OUTSIDE: - numOutside++; - break; - case PolygonStatus.SAME: - numSame++; - break; - default: - NUnit.Framework.Assert.Fail("Didn't expect status: " + poly.status); - break; + [Test] + public void RaycastCubeTouchingCube() + { + CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 10f)); + // Two cubes next to each other, touching. + CsgObject leftCube = CsgOperations.ToCsg(ctx, Primitives.AxisAlignedBox(1, new Vector3(-2, 0, 0), Vector3.one, 1)); + CsgObject rightCube = CsgOperations.ToCsg(ctx, Primitives.AxisAlignedBox(2, Vector3.zero, Vector3.one, 1)); + + // Classify all faces in both cubes. + foreach (CsgPolygon poly in leftCube.polygons) + { + CsgOperations.ClassifyPolygonUsingRaycast(poly, rightCube); + } + foreach (CsgPolygon poly in rightCube.polygons) + { + CsgOperations.ClassifyPolygonUsingRaycast(poly, leftCube); + } + + // We expect all faces to be OUTSIDE except one from each cube. Those should bother be OPPOSITE. + CsgPolygon leftPoly = null; + CsgPolygon rightPoly = null; + foreach (CsgPolygon poly in leftCube.polygons) + { + if (poly.status == PolygonStatus.OPPOSITE) + { + NUnit.Framework.Assert.Null(leftPoly, "Should be only one poly that is OPPOSITE"); + leftPoly = poly; + } + else + { + NUnit.Framework.Assert.AreEqual(PolygonStatus.OUTSIDE, poly.status); + } + } + foreach (CsgPolygon poly in rightCube.polygons) + { + if (poly.status == PolygonStatus.OPPOSITE) + { + NUnit.Framework.Assert.Null(rightPoly, "Should be only one poly that is OPPOSITE"); + rightPoly = poly; + } + else + { + NUnit.Framework.Assert.AreEqual(PolygonStatus.OUTSIDE, poly.status); + } + } + + NUnit.Framework.Assert.NotNull(leftPoly); + NUnit.Framework.Assert.NotNull(rightPoly); + + // Barycenters should be the same, normals should be opposite + NUnit.Framework.Assert.AreEqual(0f, Vector3.Distance(leftPoly.baryCenter, rightPoly.baryCenter), 0.001f); + NUnit.Framework.Assert.AreEqual(-1f, Vector3.Dot(leftPoly.plane.normal, rightPoly.plane.normal), 0.001f); } - } - NUnit.Framework.Assert.AreEqual(1, numInside); - NUnit.Framework.Assert.AreEqual(1, numOutside); - NUnit.Framework.Assert.AreEqual(4, numSame); - - numInside = 0; - numOutside = 0; - numSame = 0; - foreach (CsgPolygon poly in rightCube.polygons) { - CsgOperations.ClassifyPolygonUsingRaycast(poly, leftCube); - switch (poly.status) { - case PolygonStatus.INSIDE: - numInside++; - break; - case PolygonStatus.OUTSIDE: - numOutside++; - break; - case PolygonStatus.SAME: - numSame++; - break; - default: - NUnit.Framework.Assert.Fail("Didn't expect status: " + poly.status); - break; + + [Test] + public void RaycastCubeOverlappingCube() + { + CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); + + // Two cubes next to each other, overlapping. + CsgObject leftCube = CsgOperations.ToCsg(ctx, + Primitives.AxisAlignedBox(1, new Vector3(-1, 0, 0), Vector3.one, 1)); + CsgObject rightCube = CsgOperations.ToCsg(ctx, Primitives.AxisAlignedBox(2, Vector3.zero, Vector3.one, 1)); + + // Classify all faces in both cubes. + // Each obj should have 4 polys SAME, 1 OUTSIDE and 1 INSIDE + int numInside = 0; + int numOutside = 0; + int numSame = 0; + foreach (CsgPolygon poly in leftCube.polygons) + { + CsgOperations.ClassifyPolygonUsingRaycast(poly, rightCube); + switch (poly.status) + { + case PolygonStatus.INSIDE: + numInside++; + break; + case PolygonStatus.OUTSIDE: + numOutside++; + break; + case PolygonStatus.SAME: + numSame++; + break; + default: + NUnit.Framework.Assert.Fail("Didn't expect status: " + poly.status); + break; + } + } + NUnit.Framework.Assert.AreEqual(1, numInside); + NUnit.Framework.Assert.AreEqual(1, numOutside); + NUnit.Framework.Assert.AreEqual(4, numSame); + + numInside = 0; + numOutside = 0; + numSame = 0; + foreach (CsgPolygon poly in rightCube.polygons) + { + CsgOperations.ClassifyPolygonUsingRaycast(poly, leftCube); + switch (poly.status) + { + case PolygonStatus.INSIDE: + numInside++; + break; + case PolygonStatus.OUTSIDE: + numOutside++; + break; + case PolygonStatus.SAME: + numSame++; + break; + default: + NUnit.Framework.Assert.Fail("Didn't expect status: " + poly.status); + break; + } + } + NUnit.Framework.Assert.AreEqual(1, numInside); + NUnit.Framework.Assert.AreEqual(1, numOutside); + NUnit.Framework.Assert.AreEqual(4, numSame); } - } - NUnit.Framework.Assert.AreEqual(1, numInside); - NUnit.Framework.Assert.AreEqual(1, numOutside); - NUnit.Framework.Assert.AreEqual(4, numSame); - } - private void AssertPolyStatus(PolygonStatus status, CsgPolygon poly, CsgObject obj) { - CsgOperations.ClassifyPolygonUsingRaycast(poly, obj); - NUnit.Framework.Assert.AreEqual(status, poly.status); - } + private void AssertPolyStatus(PolygonStatus status, CsgPolygon poly, CsgObject obj) + { + CsgOperations.ClassifyPolygonUsingRaycast(poly, obj); + NUnit.Framework.Assert.AreEqual(status, poly.status); + } - private CsgPolygon toPoly(params Vector3[] verts) { - List poly = new List(); - foreach (Vector3 vert in verts) { - poly.Add(new CsgVertex(vert)); - } - return new CsgPolygon(poly, new core.FaceProperties()); - } + private CsgPolygon toPoly(params Vector3[] verts) + { + List poly = new List(); + foreach (Vector3 vert in verts) + { + poly.Add(new CsgVertex(vert)); + } + return new CsgPolygon(poly, new core.FaceProperties()); + } - private CsgObject toObj(params CsgPolygon[] polys) { - HashSet verts = new HashSet(); - foreach (CsgPolygon poly in polys) { - foreach (CsgVertex vert in poly.vertices) { - verts.Add(vert); + private CsgObject toObj(params CsgPolygon[] polys) + { + HashSet verts = new HashSet(); + foreach (CsgPolygon poly in polys) + { + foreach (CsgVertex vert in poly.vertices) + { + verts.Add(vert); + } + } + return new CsgObject(new List(polys), new List(verts)); } - } - return new CsgObject(new List(polys), new List(verts)); - } - [Test] - public void SubtractCubeWithinCube() { - // Small cube inside of a larger cube, both at the origin. - MMesh smallCube = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one * 0.5f, 1); - MMesh largeCube = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, 1); + [Test] + public void SubtractCubeWithinCube() + { + // Small cube inside of a larger cube, both at the origin. + MMesh smallCube = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one * 0.5f, 1); + MMesh largeCube = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, 1); - // Subtracting large cube from small cube should result in empty space. - NUnit.Framework.Assert.IsNull(CsgOperations.Subtract(smallCube, largeCube)); + // Subtracting large cube from small cube should result in empty space. + NUnit.Framework.Assert.IsNull(CsgOperations.Subtract(smallCube, largeCube)); - // Subtracting small cube from large cube should result in just the large cube with an invisible hole. - MMesh results = CsgOperations.Subtract(largeCube, smallCube); - NUnit.Framework.Assert.AreEqual(12, results.faceCount); + // Subtracting small cube from large cube should result in just the large cube with an invisible hole. + MMesh results = CsgOperations.Subtract(largeCube, smallCube); + NUnit.Framework.Assert.AreEqual(12, results.faceCount); - // Mesh should still be valid: - NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(results)); - } + // Mesh should still be valid: + NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(results)); + } - [Test] - public void SubtractCubeOverlappingCube() { - // Two cubes next to each other, overlapping. - MMesh result = CsgOperations.Subtract( - Primitives.AxisAlignedBox(1, new Vector3(-1, 0, 0), Vector3.one, 1), - Primitives.AxisAlignedBox(2, Vector3.zero, Vector3.one, 1)); + [Test] + public void SubtractCubeOverlappingCube() + { + // Two cubes next to each other, overlapping. + MMesh result = CsgOperations.Subtract( + Primitives.AxisAlignedBox(1, new Vector3(-1, 0, 0), Vector3.one, 1), + Primitives.AxisAlignedBox(2, Vector3.zero, Vector3.one, 1)); - NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(result, true)); - } + NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(result, true)); + } - //[Test] TODO: Uncomment when works :/ - public void SubtractSphereOverlappingCube() { - // A cube and a sphere, overlapping. - MMesh result = CsgOperations.Subtract( - Primitives.AxisAlignedBox(1, new Vector3(-1, -0.7f, -0.3f), Vector3.one, 2), - Primitives.AxisAlignedIcosphere(2, new Vector3(-.2f, 0, 0), Vector3.one, 1)); + //[Test] TODO: Uncomment when works :/ + public void SubtractSphereOverlappingCube() + { + // A cube and a sphere, overlapping. + MMesh result = CsgOperations.Subtract( + Primitives.AxisAlignedBox(1, new Vector3(-1, -0.7f, -0.3f), Vector3.one, 2), + Primitives.AxisAlignedIcosphere(2, new Vector3(-.2f, 0, 0), Vector3.one, 1)); - NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(result, true)); - } + NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(result, true)); + } - [Test] - public void TestOverlappingCubesSplitCorrectly() { - CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); - CsgObject leftCube = CsgOperations.ToCsg( - ctx, Primitives.AxisAlignedBox(1, new Vector3(-1, 0, 0), Vector3.one, 1)); - CsgObject rightCube = CsgOperations.ToCsg( - ctx, Primitives.AxisAlignedBox(2, Vector3.zero, Vector3.one, 1)); - TestObjectSplitMaintainsValidObject(ctx, leftCube, rightCube); - } + [Test] + public void TestOverlappingCubesSplitCorrectly() + { + CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); + CsgObject leftCube = CsgOperations.ToCsg( + ctx, Primitives.AxisAlignedBox(1, new Vector3(-1, 0, 0), Vector3.one, 1)); + CsgObject rightCube = CsgOperations.ToCsg( + ctx, Primitives.AxisAlignedBox(2, Vector3.zero, Vector3.one, 1)); + TestObjectSplitMaintainsValidObject(ctx, leftCube, rightCube); + } - [Test] - public void TestDiagonalOverlappingCubesSplitCorrectly() { - CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); - CsgObject leftCube = CsgOperations.ToCsg( - ctx, Primitives.AxisAlignedBox(1, new Vector3(-1, -0.7f, -0.3f), Vector3.one, 1)); - CsgObject rightCube = CsgOperations.ToCsg( - ctx, Primitives.AxisAlignedBox(2, Vector3.zero, Vector3.one, 1)); - TestObjectSplitMaintainsValidObject(ctx, leftCube, rightCube); - } + [Test] + public void TestDiagonalOverlappingCubesSplitCorrectly() + { + CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); + CsgObject leftCube = CsgOperations.ToCsg( + ctx, Primitives.AxisAlignedBox(1, new Vector3(-1, -0.7f, -0.3f), Vector3.one, 1)); + CsgObject rightCube = CsgOperations.ToCsg( + ctx, Primitives.AxisAlignedBox(2, Vector3.zero, Vector3.one, 1)); + TestObjectSplitMaintainsValidObject(ctx, leftCube, rightCube); + } - [Test] - public void TestDiagonalOverlappingCubeAndSphereSplitCorrectly() { - CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); - CsgObject leftCube = CsgOperations.ToCsg( - ctx, Primitives.AxisAlignedBox(1, new Vector3(-1, -0.7f, -0.3f), Vector3.one, 1)); - CsgObject rightSphere = CsgOperations.ToCsg( - ctx, Primitives.AxisAlignedIcosphere(2, new Vector3(-.2f, 0, 0), Vector3.one, 1)); - TestObjectSplitMaintainsValidObject(ctx, leftCube, rightSphere); - } + [Test] + public void TestDiagonalOverlappingCubeAndSphereSplitCorrectly() + { + CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); + CsgObject leftCube = CsgOperations.ToCsg( + ctx, Primitives.AxisAlignedBox(1, new Vector3(-1, -0.7f, -0.3f), Vector3.one, 1)); + CsgObject rightSphere = CsgOperations.ToCsg( + ctx, Primitives.AxisAlignedIcosphere(2, new Vector3(-.2f, 0, 0), Vector3.one, 1)); + TestObjectSplitMaintainsValidObject(ctx, leftCube, rightSphere); + } - // Same as above, but moved around a little. - // [Test] TODO: Uncomment when works :/ - public void TestDiagonalOverlappingCubeAndSphereSplitCorrectly2() { - CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); - CsgObject leftCube = CsgOperations.ToCsg( - ctx, Primitives.AxisAlignedBox(1, new Vector3(-1, -0.7f, -0.3f), Vector3.one, 1)); - CsgObject rightSphere = CsgOperations.ToCsg( - ctx, Primitives.AxisAlignedIcosphere(2, new Vector3(-.23f, 0.01f, -0.23f), Vector3.one, 1)); - TestObjectSplitMaintainsValidObject(ctx, leftCube, rightSphere); - } + // Same as above, but moved around a little. + // [Test] TODO: Uncomment when works :/ + public void TestDiagonalOverlappingCubeAndSphereSplitCorrectly2() + { + CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); + CsgObject leftCube = CsgOperations.ToCsg( + ctx, Primitives.AxisAlignedBox(1, new Vector3(-1, -0.7f, -0.3f), Vector3.one, 1)); + CsgObject rightSphere = CsgOperations.ToCsg( + ctx, Primitives.AxisAlignedIcosphere(2, new Vector3(-.23f, 0.01f, -0.23f), Vector3.one, 1)); + TestObjectSplitMaintainsValidObject(ctx, leftCube, rightSphere); + } - public void TestObjectSplitMaintainsValidObject(CsgContext ctx, CsgObject left, CsgObject right) { - // Make sure they start with valid topology: - NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology( - CsgOperations.FromPolys(1, Vector3.zero, Quaternion.identity, left.polygons), true)); - NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology( - CsgOperations.FromPolys(1, Vector3.zero, Quaternion.identity, right.polygons), true)); - - // Now split against each other and ensure they are both still valid. - CsgOperations.SplitObject(ctx, left, right); - CsgOperations.SplitObject(ctx, right, left); - - NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology( - CsgOperations.FromPolys(1, Vector3.zero, Quaternion.identity, left.polygons), true)); - NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology( - CsgOperations.FromPolys(1, Vector3.zero, Quaternion.identity, right.polygons), true)); - - // Split one last time, because the paper says so :) - CsgOperations.SplitObject(ctx, left, right); - NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology( - CsgOperations.FromPolys(1, Vector3.zero, Quaternion.identity, left.polygons), true)); + public void TestObjectSplitMaintainsValidObject(CsgContext ctx, CsgObject left, CsgObject right) + { + // Make sure they start with valid topology: + NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology( + CsgOperations.FromPolys(1, Vector3.zero, Quaternion.identity, left.polygons), true)); + NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology( + CsgOperations.FromPolys(1, Vector3.zero, Quaternion.identity, right.polygons), true)); + + // Now split against each other and ensure they are both still valid. + CsgOperations.SplitObject(ctx, left, right); + CsgOperations.SplitObject(ctx, right, left); + + NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology( + CsgOperations.FromPolys(1, Vector3.zero, Quaternion.identity, left.polygons), true)); + NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology( + CsgOperations.FromPolys(1, Vector3.zero, Quaternion.identity, right.polygons), true)); + + // Split one last time, because the paper says so :) + CsgOperations.SplitObject(ctx, left, right); + NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology( + CsgOperations.FromPolys(1, Vector3.zero, Quaternion.identity, left.polygons), true)); + } } - } } diff --git a/Assets/Editor/tests/model/csg/CsgPolygonTest.cs b/Assets/Editor/tests/model/csg/CsgPolygonTest.cs index 66c43f9d..0b1cdbcb 100644 --- a/Assets/Editor/tests/model/csg/CsgPolygonTest.cs +++ b/Assets/Editor/tests/model/csg/CsgPolygonTest.cs @@ -19,44 +19,50 @@ using NUnit.Framework; using UnityEngine; -namespace com.google.apps.peltzer.client.model.csg { - [TestFixture] - public class CsgPolygonTest { - [Test] - public void TestBaryCenter() { - CsgPolygon poly = toPoly( - new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); - NUnit.Framework.Assert.AreEqual(0f, Vector3.Distance(poly.baryCenter, Vector3.zero), 0.001f); - - poly = toPoly( - new Vector3(-1, 3, -1), new Vector3(1, 3, -1), new Vector3(1, 3, 1), new Vector3(-1, 3, 1)); - NUnit.Framework.Assert.AreEqual(0f, Vector3.Distance(poly.baryCenter, new Vector3(0, 3, 0)), 0.001f); - - poly = toPoly(new Vector3(-1, 1, -1), new Vector3(1, 1, -1), new Vector3(1, 1, 1)); - NUnit.Framework.Assert.AreEqual(0f, - Vector3.Distance(poly.baryCenter, new Vector3(0.3333f, 1.0f, -0.333f)), 0.001f); - } +namespace com.google.apps.peltzer.client.model.csg +{ + [TestFixture] + public class CsgPolygonTest + { + [Test] + public void TestBaryCenter() + { + CsgPolygon poly = toPoly( + new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); + NUnit.Framework.Assert.AreEqual(0f, Vector3.Distance(poly.baryCenter, Vector3.zero), 0.001f); - [Test] - public void TestNormal() { - CsgPolygon poly = toPoly( - new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); - NUnit.Framework.Assert.AreEqual(0, Vector3.Distance(poly.plane.normal, new Vector3(0, -1, 0)), 0.001f); + poly = toPoly( + new Vector3(-1, 3, -1), new Vector3(1, 3, -1), new Vector3(1, 3, 1), new Vector3(-1, 3, 1)); + NUnit.Framework.Assert.AreEqual(0f, Vector3.Distance(poly.baryCenter, new Vector3(0, 3, 0)), 0.001f); - poly = toPoly(new Vector3(-1, 10, -1), new Vector3(1, 10, -1), new Vector3(1, 10, 1)); - NUnit.Framework.Assert.AreEqual(0, Vector3.Distance(poly.plane.normal, new Vector3(0, -1, 0)), 0.001f); + poly = toPoly(new Vector3(-1, 1, -1), new Vector3(1, 1, -1), new Vector3(1, 1, 1)); + NUnit.Framework.Assert.AreEqual(0f, + Vector3.Distance(poly.baryCenter, new Vector3(0.3333f, 1.0f, -0.333f)), 0.001f); + } - poly = toPoly(new Vector3(0, -1, -1), new Vector3(0, 1, -1), new Vector3(0, 0, 1)); - NUnit.Framework.Assert.AreEqual(0, Vector3.Distance(poly.plane.normal, new Vector3(1, 0, 0)), 0.001f); - } + [Test] + public void TestNormal() + { + CsgPolygon poly = toPoly( + new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1)); + NUnit.Framework.Assert.AreEqual(0, Vector3.Distance(poly.plane.normal, new Vector3(0, -1, 0)), 0.001f); - private CsgPolygon toPoly(params Vector3[] verts) { - List poly = new List(); - foreach (Vector3 vert in verts) { - poly.Add(new CsgVertex(vert)); - } - return new CsgPolygon(poly, new core.FaceProperties()); - } + poly = toPoly(new Vector3(-1, 10, -1), new Vector3(1, 10, -1), new Vector3(1, 10, 1)); + NUnit.Framework.Assert.AreEqual(0, Vector3.Distance(poly.plane.normal, new Vector3(0, -1, 0)), 0.001f); - } + poly = toPoly(new Vector3(0, -1, -1), new Vector3(0, 1, -1), new Vector3(0, 0, 1)); + NUnit.Framework.Assert.AreEqual(0, Vector3.Distance(poly.plane.normal, new Vector3(1, 0, 0)), 0.001f); + } + + private CsgPolygon toPoly(params Vector3[] verts) + { + List poly = new List(); + foreach (Vector3 vert in verts) + { + poly.Add(new CsgVertex(vert)); + } + return new CsgPolygon(poly, new core.FaceProperties()); + } + + } } diff --git a/Assets/Editor/tests/model/csg/FaceRecomposerTest.cs b/Assets/Editor/tests/model/csg/FaceRecomposerTest.cs index c4cef5b7..cddcb41d 100644 --- a/Assets/Editor/tests/model/csg/FaceRecomposerTest.cs +++ b/Assets/Editor/tests/model/csg/FaceRecomposerTest.cs @@ -19,254 +19,267 @@ using UnityEngine; using NUnit.Framework; -namespace com.google.apps.peltzer.client.model.csg { - [TestFixture] - public class FaceRecomposerTest { - - /// - /// For the JoinAtSegment tests, this is a diagram of the points: - /// - /// 1-----2-----3 - /// | | | - /// | | | - /// 4-----5-----6 - /// - /// - [Test] - public void TestJoinAtSegment() { - SolidVertex one = new SolidVertex(1, new Vector3(0, -1, 1), Vector3.one); - SolidVertex two = new SolidVertex(2, new Vector3(0, 0, 1), Vector3.one); - SolidVertex three = new SolidVertex(3, new Vector3(0, 1, 1), Vector3.one); - SolidVertex four = new SolidVertex(4, new Vector3(0, -1, -1), Vector3.one); - SolidVertex five = new SolidVertex(5, new Vector3(0, 0, -1), Vector3.one); - SolidVertex six = new SolidVertex(6, new Vector3(0, 1, -1), Vector3.one); - - List> pieces; - - pieces = new List>() { +namespace com.google.apps.peltzer.client.model.csg +{ + [TestFixture] + public class FaceRecomposerTest + { + + /// + /// For the JoinAtSegment tests, this is a diagram of the points: + /// + /// 1-----2-----3 + /// | | | + /// | | | + /// 4-----5-----6 + /// + /// + [Test] + public void TestJoinAtSegment() + { + SolidVertex one = new SolidVertex(1, new Vector3(0, -1, 1), Vector3.one); + SolidVertex two = new SolidVertex(2, new Vector3(0, 0, 1), Vector3.one); + SolidVertex three = new SolidVertex(3, new Vector3(0, 1, 1), Vector3.one); + SolidVertex four = new SolidVertex(4, new Vector3(0, -1, -1), Vector3.one); + SolidVertex five = new SolidVertex(5, new Vector3(0, 0, -1), Vector3.one); + SolidVertex six = new SolidVertex(6, new Vector3(0, 1, -1), Vector3.one); + + List> pieces; + + pieces = new List>() { new List() { one, two, five }, new List() { five, two, three } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { one, two, three, five }); + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { one, two, three, five }); - pieces = new List>() { + pieces = new List>() { new List() { two, five, one }, new List() { five, two, three } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { two, three, five, one }); + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { two, three, five, one }); - pieces = new List>() { + pieces = new List>() { new List() { two, one, five }, new List() { five, three, two } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { three, two, one, five }); + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { three, two, one, five }); - pieces = new List>() { + pieces = new List>() { new List() { two, one, four, five }, new List() { five, six, three, two } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { six, three, two, one, four, five }); + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { six, three, two, one, four, five }); - } + } - /// - /// For the JoinAtPoint tests, this is a diagram of the vertices: - /// - /// 3 - /// /| - /// / 2 - /// / |\ - /// / | \ - /// 4____1____5 - /// - /// 1 -> Shared - /// 2 -> toInsert - /// - [Test] - public void TestJoinAtPointAlt1() { - SolidVertex shared = new SolidVertex(1, new Vector3(0, 0, 0), Vector3.one); - SolidVertex toInsert = new SolidVertex(2, new Vector3(0, 1, 0), Vector3.one); - SolidVertex three = new SolidVertex(3, new Vector3(0, 2, 0), Vector3.one); - SolidVertex four = new SolidVertex(4, new Vector3(-1, 0, 0), Vector3.one); - SolidVertex five = new SolidVertex(5, new Vector3(1, 0, 0), Vector3.one); - - List> pieces; - - pieces = new List>() { + /// + /// For the JoinAtPoint tests, this is a diagram of the vertices: + /// + /// 3 + /// /| + /// / 2 + /// / |\ + /// / | \ + /// 4____1____5 + /// + /// 1 -> Shared + /// 2 -> toInsert + /// + [Test] + public void TestJoinAtPointAlt1() + { + SolidVertex shared = new SolidVertex(1, new Vector3(0, 0, 0), Vector3.one); + SolidVertex toInsert = new SolidVertex(2, new Vector3(0, 1, 0), Vector3.one); + SolidVertex three = new SolidVertex(3, new Vector3(0, 2, 0), Vector3.one); + SolidVertex four = new SolidVertex(4, new Vector3(-1, 0, 0), Vector3.one); + SolidVertex five = new SolidVertex(5, new Vector3(1, 0, 0), Vector3.one); + + List> pieces; + + pieces = new List>() { new List() { shared, four, three }, new List() { shared, toInsert, five } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { toInsert, five, four, three }); + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { toInsert, five, four, three }); - pieces = new List>() { + pieces = new List>() { new List() { three, shared, four }, new List() { shared, toInsert, five } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { three, toInsert, five, four }); + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { three, toInsert, five, four }); - pieces = new List>() { + pieces = new List>() { new List() { four, three, shared }, new List() { shared, toInsert, five } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { four, three, toInsert, five }); + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { four, three, toInsert, five }); - pieces = new List>() { + pieces = new List>() { new List() { shared, four, three }, new List() { toInsert, five, shared } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { toInsert, five, four, three }); - } + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { toInsert, five, four, three }); + } - [Test] - public void TestJoinAtPointAlt2() { - SolidVertex shared = new SolidVertex(1, new Vector3(0, 0, 0), Vector3.one); - SolidVertex toInsert = new SolidVertex(2, new Vector3(0, 1, 0), Vector3.one); - SolidVertex three = new SolidVertex(3, new Vector3(0, 2, 0), Vector3.one); - SolidVertex four = new SolidVertex(4, new Vector3(-1, 0, 0), Vector3.one); - SolidVertex five = new SolidVertex(5, new Vector3(1, 0, 0), Vector3.one); + [Test] + public void TestJoinAtPointAlt2() + { + SolidVertex shared = new SolidVertex(1, new Vector3(0, 0, 0), Vector3.one); + SolidVertex toInsert = new SolidVertex(2, new Vector3(0, 1, 0), Vector3.one); + SolidVertex three = new SolidVertex(3, new Vector3(0, 2, 0), Vector3.one); + SolidVertex four = new SolidVertex(4, new Vector3(-1, 0, 0), Vector3.one); + SolidVertex five = new SolidVertex(5, new Vector3(1, 0, 0), Vector3.one); - List> pieces; + List> pieces; - pieces = new List>() { + pieces = new List>() { new List() { shared, three, four }, new List() { toInsert, shared, five } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { five, toInsert, three, four }); + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { five, toInsert, three, four }); - pieces = new List>() { + pieces = new List>() { new List() { shared, three, four }, new List() { five, toInsert, shared } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - //(1, 5, 2, 3, 4) - CheckPoly(pieces[0], new List { five, toInsert, three, four }); + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + //(1, 5, 2, 3, 4) + CheckPoly(pieces[0], new List { five, toInsert, three, four }); - pieces = new List>() { + pieces = new List>() { new List() { three, four, shared }, new List() { toInsert, shared, five } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { three, four, five, toInsert }); + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { three, four, five, toInsert }); - pieces = new List>() { + pieces = new List>() { new List() { shared, three, four }, new List() { shared, five, toInsert } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { five, toInsert, three, four }); - } + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { five, toInsert, three, four }); + } - [Test] - public void TestJoinAtPointAlt3() { - SolidVertex shared = new SolidVertex(1, new Vector3(0, 0, 0), Vector3.one); - SolidVertex toInsert = new SolidVertex(2, new Vector3(0, 1, 0), Vector3.one); - SolidVertex three = new SolidVertex(3, new Vector3(0, 2, 0), Vector3.one); - SolidVertex four = new SolidVertex(4, new Vector3(-1, 0, 0), Vector3.one); - SolidVertex five = new SolidVertex(5, new Vector3(1, 0, 0), Vector3.one); + [Test] + public void TestJoinAtPointAlt3() + { + SolidVertex shared = new SolidVertex(1, new Vector3(0, 0, 0), Vector3.one); + SolidVertex toInsert = new SolidVertex(2, new Vector3(0, 1, 0), Vector3.one); + SolidVertex three = new SolidVertex(3, new Vector3(0, 2, 0), Vector3.one); + SolidVertex four = new SolidVertex(4, new Vector3(-1, 0, 0), Vector3.one); + SolidVertex five = new SolidVertex(5, new Vector3(1, 0, 0), Vector3.one); - List> pieces; + List> pieces; - pieces = new List>() { + pieces = new List>() { new List() { shared, toInsert, five }, new List() { shared, four, three } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { four, three, toInsert, five }); + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { four, three, toInsert, five }); - pieces = new List>() { + pieces = new List>() { new List() { shared, toInsert, five }, new List() { four, three, shared } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { four, three, toInsert, five }); + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { four, three, toInsert, five }); - pieces = new List>() { + pieces = new List>() { new List() { five, shared, toInsert }, new List() { three, shared, four } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { five, four, three, toInsert }); + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { five, four, three, toInsert }); - pieces = new List>() { + pieces = new List>() { new List() { toInsert, five, shared }, new List() { four, three, shared } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { toInsert, five, four, three }); - } + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { toInsert, five, four, three }); + } - [Test] - public void TestJoinAtPointAlt4() { - SolidVertex shared = new SolidVertex(1, new Vector3(0, 0, 0), Vector3.one); - SolidVertex toInsert = new SolidVertex(2, new Vector3(0, 1, 0), Vector3.one); - SolidVertex three = new SolidVertex(3, new Vector3(0, 2, 0), Vector3.one); - SolidVertex four = new SolidVertex(4, new Vector3(-1, 0, 0), Vector3.one); - SolidVertex five = new SolidVertex(5, new Vector3(1, 0, 0), Vector3.one); + [Test] + public void TestJoinAtPointAlt4() + { + SolidVertex shared = new SolidVertex(1, new Vector3(0, 0, 0), Vector3.one); + SolidVertex toInsert = new SolidVertex(2, new Vector3(0, 1, 0), Vector3.one); + SolidVertex three = new SolidVertex(3, new Vector3(0, 2, 0), Vector3.one); + SolidVertex four = new SolidVertex(4, new Vector3(-1, 0, 0), Vector3.one); + SolidVertex five = new SolidVertex(5, new Vector3(1, 0, 0), Vector3.one); - List> pieces; + List> pieces; - pieces = new List>() { + pieces = new List>() { new List() { toInsert, shared, five }, new List() { shared, three, four } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { toInsert, three, four, five }); + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { toInsert, three, four, five }); - pieces = new List>() { + pieces = new List>() { new List() { shared, five, toInsert }, new List() { shared, three, four } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { three, four, five, toInsert }); + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { three, four, five, toInsert }); - pieces = new List>() { + pieces = new List>() { new List() { toInsert, shared, five }, new List() { shared, three, four } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { toInsert, three, four, five }); + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { toInsert, three, four, five }); - pieces = new List>() { + pieces = new List>() { new List() { five, toInsert, shared }, new List() { three, four, shared } }; - pieces = FaceRecomposer.RecomposeFace(pieces); - NUnit.Framework.Assert.AreEqual(1, pieces.Count); - CheckPoly(pieces[0], new List { five, toInsert, three, four }); - } + pieces = FaceRecomposer.RecomposeFace(pieces); + NUnit.Framework.Assert.AreEqual(1, pieces.Count); + CheckPoly(pieces[0], new List { five, toInsert, three, four }); + } - private void CheckPoly(List list1, List list2) { - string failMsg = GetMessage(list1, list2); - NUnit.Framework.Assert.AreEqual(list1.Count, list2.Count, failMsg); + private void CheckPoly(List list1, List list2) + { + string failMsg = GetMessage(list1, list2); + NUnit.Framework.Assert.AreEqual(list1.Count, list2.Count, failMsg); - for (int i = 0; i < list1.Count; i++) { - NUnit.Framework.Assert.AreEqual(list1[i].vertexId, list2[i].vertexId, failMsg); - } - } + for (int i = 0; i < list1.Count; i++) + { + NUnit.Framework.Assert.AreEqual(list1[i].vertexId, list2[i].vertexId, failMsg); + } + } - private string GetMessage(List list1, List list2) { - return "Expected: " + ToStr(list1) + " but was: " + ToStr(list2); - } + private string GetMessage(List list1, List list2) + { + return "Expected: " + ToStr(list1) + " but was: " + ToStr(list2); + } - private string ToStr(List list) { - string s = "("; - for (int i = 0; i < list.Count; i++) { - if (i > 0) { - s += ", "; + private string ToStr(List list) + { + string s = "("; + for (int i = 0; i < list.Count; i++) + { + if (i > 0) + { + s += ", "; + } + s += list[i].vertexId; + } + return s + ")"; } - s += list[i].vertexId; - } - return s + ")"; } - } } diff --git a/Assets/Editor/tests/model/csg/PolygonSplitterTest.cs b/Assets/Editor/tests/model/csg/PolygonSplitterTest.cs index b819496f..57f22ed8 100644 --- a/Assets/Editor/tests/model/csg/PolygonSplitterTest.cs +++ b/Assets/Editor/tests/model/csg/PolygonSplitterTest.cs @@ -21,604 +21,631 @@ using com.google.apps.peltzer.client.model.core; using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.model.csg { - - [TestFixture] - public class PolygonSplitterTest { - [Test] - public void TestSegmentDescriptor() { - SegmentDescriptor descriptor; - CsgPolygon poly = toPoly( - new Vector3(-1, -1, 0), new Vector3(1, -1, 0), new Vector3(1, 1, 0), new Vector3(-1, 1, 0)); - CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); - - descriptor = PolygonSplitter.CalcSegmentDescriptor( - ctx, Vector3.zero, new Vector3(1, 0, 0), - new float[] { 0, 0, 1, 1 }, - poly); - AssertDescriptor(descriptor, Endpoint.VERTEX, Endpoint.EDGE, Endpoint.VERTEX); - - // TODO: is this case right? - //descriptor = PolygonSplitter.CalcSegmentDescriptor( - // ctx, Vector3.zero, new Vector3(1, 0, 0), - // new float[] { -1, 1, -1, 1 }, - // poly); - //AssertDescriptor(descriptor, Endpoint.EDGE, Endpoint.EDGE, Endpoint.EDGE); - - descriptor = PolygonSplitter.CalcSegmentDescriptor( - ctx, Vector3.zero, new Vector3(1, 0, 0), - new float[] { -1, 1, 1, -1 }, - poly); - AssertDescriptor(descriptor, Endpoint.EDGE, Endpoint.FACE, Endpoint.EDGE); - - descriptor = PolygonSplitter.CalcSegmentDescriptor( - ctx, Vector3.zero, new Vector3(1, 0, 0), - new float[] { 0, 1, 1, 0 }, - poly); - AssertDescriptor(descriptor, Endpoint.VERTEX, Endpoint.EDGE, Endpoint.VERTEX); - - descriptor = PolygonSplitter.CalcSegmentDescriptor( - ctx, Vector3.zero, new Vector3(1, 0, 0), - new float[] { 0, 1, 0, 1 }, - poly); - AssertDescriptor(descriptor, Endpoint.VERTEX, Endpoint.FACE, Endpoint.VERTEX); - } - - private void AssertDescriptor(SegmentDescriptor descriptor, int start, int middle, int end) { - NUnit.Framework.Assert.AreEqual(start, descriptor.start); - NUnit.Framework.Assert.AreEqual(middle, descriptor.middle); - NUnit.Framework.Assert.AreEqual(end, descriptor.end); - } - - [Test] - public void TestSplitAlignedPerpendicularFaces() { - CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); - - CsgObject plane1 = toObj(toPoly( - new Vector3(-1, -1, 0), new Vector3(1, -1, 0), new Vector3(1, 1, 0), new Vector3(-1, 1, 0))); - CsgObject plane2 = toObj(toPoly( - new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1))); - - CsgPolygon original = plane1.polygons[0]; - CsgPolygon splitBy = plane2.polygons[0]; - PolygonSplitter.SplitPolys(ctx, plane1, original, splitBy); +namespace com.google.apps.peltzer.client.model.csg +{ + + [TestFixture] + public class PolygonSplitterTest + { + [Test] + public void TestSegmentDescriptor() + { + SegmentDescriptor descriptor; + CsgPolygon poly = toPoly( + new Vector3(-1, -1, 0), new Vector3(1, -1, 0), new Vector3(1, 1, 0), new Vector3(-1, 1, 0)); + CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); + + descriptor = PolygonSplitter.CalcSegmentDescriptor( + ctx, Vector3.zero, new Vector3(1, 0, 0), + new float[] { 0, 0, 1, 1 }, + poly); + AssertDescriptor(descriptor, Endpoint.VERTEX, Endpoint.EDGE, Endpoint.VERTEX); + + // TODO: is this case right? + //descriptor = PolygonSplitter.CalcSegmentDescriptor( + // ctx, Vector3.zero, new Vector3(1, 0, 0), + // new float[] { -1, 1, -1, 1 }, + // poly); + //AssertDescriptor(descriptor, Endpoint.EDGE, Endpoint.EDGE, Endpoint.EDGE); + + descriptor = PolygonSplitter.CalcSegmentDescriptor( + ctx, Vector3.zero, new Vector3(1, 0, 0), + new float[] { -1, 1, 1, -1 }, + poly); + AssertDescriptor(descriptor, Endpoint.EDGE, Endpoint.FACE, Endpoint.EDGE); + + descriptor = PolygonSplitter.CalcSegmentDescriptor( + ctx, Vector3.zero, new Vector3(1, 0, 0), + new float[] { 0, 1, 1, 0 }, + poly); + AssertDescriptor(descriptor, Endpoint.VERTEX, Endpoint.EDGE, Endpoint.VERTEX); + + descriptor = PolygonSplitter.CalcSegmentDescriptor( + ctx, Vector3.zero, new Vector3(1, 0, 0), + new float[] { 0, 1, 0, 1 }, + poly); + AssertDescriptor(descriptor, Endpoint.VERTEX, Endpoint.FACE, Endpoint.VERTEX); + } - // Should be split into exactly 2 polys. - NUnit.Framework.Assert.AreEqual(2, plane1.polygons.Count); + private void AssertDescriptor(SegmentDescriptor descriptor, int start, int middle, int end) + { + NUnit.Framework.Assert.AreEqual(start, descriptor.start); + NUnit.Framework.Assert.AreEqual(middle, descriptor.middle); + NUnit.Framework.Assert.AreEqual(end, descriptor.end); + } - NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(original, plane1.polygons, 2)); + [Test] + public void TestSplitAlignedPerpendicularFaces() + { + CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); - // Resplitting should do nothing: - foreach (CsgPolygon poly in plane1.polygons) { - PolygonSplitter.SplitPolys(ctx, plane1, poly, splitBy); - NUnit.Framework.Assert.AreEqual(2, plane1.polygons.Count); - } - } + CsgObject plane1 = toObj(toPoly( + new Vector3(-1, -1, 0), new Vector3(1, -1, 0), new Vector3(1, 1, 0), new Vector3(-1, 1, 0))); + CsgObject plane2 = toObj(toPoly( + new Vector3(-1, 0, -1), new Vector3(1, 0, -1), new Vector3(1, 0, 1), new Vector3(-1, 0, 1))); - [Test] - public void TestSplitUnalignedPerpendicularFaces() { - CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); + CsgPolygon original = plane1.polygons[0]; + CsgPolygon splitBy = plane2.polygons[0]; + PolygonSplitter.SplitPolys(ctx, plane1, original, splitBy); - CsgObject plane1 = toObj(toPoly( - new Vector3(-1, -1, 0), new Vector3(1, -1, 0), new Vector3(1, 1, 0), new Vector3(-1, 1, 0))); - CsgObject plane2 = toObj(toPoly( - new Vector3(-0.5f, 0, -0.5f), new Vector3(1, 0, -0.5f), new Vector3(1, 0, 1), new Vector3(-1, 0, 1))); + // Should be split into exactly 2 polys. + NUnit.Framework.Assert.AreEqual(2, plane1.polygons.Count); - CsgPolygon original = plane1.polygons[0]; - CsgPolygon splitBy = plane2.polygons[0]; - PolygonSplitter.SplitPolys(ctx, plane1, original, splitBy); + NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(original, plane1.polygons, 2)); - // Should be split into exactly 3 polys. - NUnit.Framework.Assert.AreEqual(3, plane1.polygons.Count); - NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(original, plane1.polygons, 1)); + // Resplitting should do nothing: + foreach (CsgPolygon poly in plane1.polygons) + { + PolygonSplitter.SplitPolys(ctx, plane1, poly, splitBy); + NUnit.Framework.Assert.AreEqual(2, plane1.polygons.Count); + } + } - // Resplitting should do nothing: - foreach (CsgPolygon poly in plane1.polygons) { - PolygonSplitter.SplitPolys(ctx, plane1, poly, splitBy); - NUnit.Framework.Assert.AreEqual(3, plane1.polygons.Count); - } - } + [Test] + public void TestSplitUnalignedPerpendicularFaces() + { + CsgContext ctx = new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)); + + CsgObject plane1 = toObj(toPoly( + new Vector3(-1, -1, 0), new Vector3(1, -1, 0), new Vector3(1, 1, 0), new Vector3(-1, 1, 0))); + CsgObject plane2 = toObj(toPoly( + new Vector3(-0.5f, 0, -0.5f), new Vector3(1, 0, -0.5f), new Vector3(1, 0, 1), new Vector3(-1, 0, 1))); + + CsgPolygon original = plane1.polygons[0]; + CsgPolygon splitBy = plane2.polygons[0]; + PolygonSplitter.SplitPolys(ctx, plane1, original, splitBy); + + // Should be split into exactly 3 polys. + NUnit.Framework.Assert.AreEqual(3, plane1.polygons.Count); + NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(original, plane1.polygons, 1)); + + // Resplitting should do nothing: + foreach (CsgPolygon poly in plane1.polygons) + { + PolygonSplitter.SplitPolys(ctx, plane1, poly, splitBy); + NUnit.Framework.Assert.AreEqual(3, plane1.polygons.Count); + } + } - /// - /// Diagram for this test. The polygon is split at E to X. For the first case, - /// the split happens between edge B-D. In the second the split happens on vertex Y. - /// - /// A B - /// +-------------------------+ - /// | | - /// + E + X | (Y) - /// | | - /// +-------------------------+ - /// C D - /// - [Test] - public void TestSplit_VertexFaceFace() { - CsgVertex vertA = new CsgVertex(new Vector3(-1, -1, 0)); - CsgVertex vertB = new CsgVertex(new Vector3(1, -1, 0)); - CsgVertex vertC = new CsgVertex(new Vector3(-1, 1, 0)); - CsgVertex vertD = new CsgVertex(new Vector3(1, 1, 0)); - CsgVertex vertE = new CsgVertex(new Vector3(-1, 0, 0)); - CsgVertex vertX = new CsgVertex(new Vector3(0, 0, 0)); - CsgVertex vertY = new CsgVertex(new Vector3(1, 0, 0)); - - // Case 6.3h - CsgPolygon poly = toPoly(vertE, vertA, vertB, vertD, vertC); - CsgObject obj = toObj(poly); - - SegmentDescriptor descriptor = new SegmentDescriptor(); - descriptor.start = descriptor.finalStart = Endpoint.VERTEX; - descriptor.middle = descriptor.finalMiddle = Endpoint.FACE; - descriptor.end = Endpoint.EDGE; - descriptor.finalEnd = Endpoint.FACE; - descriptor.startVertex = descriptor.finalStartVertex = vertE; - descriptor.endVertex = vertB; - descriptor.finalEndVertex = vertX; - descriptor.startVertIdx = 0; - descriptor.endVertIdx = 2; - - PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); - - NUnit.Framework.Assert.AreEqual(3, obj.polygons.Count); - NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 0)); - - // Case 6.3g - poly = toPoly(vertE, vertA, vertB, vertY, vertD, vertC); - obj = toObj(poly); - - descriptor.start = descriptor.finalStart = Endpoint.VERTEX; - descriptor.middle = descriptor.finalMiddle = Endpoint.FACE; - descriptor.end = Endpoint.VERTEX; - descriptor.finalEnd = Endpoint.FACE; - descriptor.startVertex = descriptor.finalStartVertex = vertE; - descriptor.endVertex = vertY; - descriptor.finalEndVertex = vertX; - descriptor.startVertIdx = 0; - descriptor.endVertIdx = 3; - - PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); - NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 0)); - } + /// + /// Diagram for this test. The polygon is split at E to X. For the first case, + /// the split happens between edge B-D. In the second the split happens on vertex Y. + /// + /// A B + /// +-------------------------+ + /// | | + /// + E + X | (Y) + /// | | + /// +-------------------------+ + /// C D + /// + [Test] + public void TestSplit_VertexFaceFace() + { + CsgVertex vertA = new CsgVertex(new Vector3(-1, -1, 0)); + CsgVertex vertB = new CsgVertex(new Vector3(1, -1, 0)); + CsgVertex vertC = new CsgVertex(new Vector3(-1, 1, 0)); + CsgVertex vertD = new CsgVertex(new Vector3(1, 1, 0)); + CsgVertex vertE = new CsgVertex(new Vector3(-1, 0, 0)); + CsgVertex vertX = new CsgVertex(new Vector3(0, 0, 0)); + CsgVertex vertY = new CsgVertex(new Vector3(1, 0, 0)); + + // Case 6.3h + CsgPolygon poly = toPoly(vertE, vertA, vertB, vertD, vertC); + CsgObject obj = toObj(poly); + + SegmentDescriptor descriptor = new SegmentDescriptor(); + descriptor.start = descriptor.finalStart = Endpoint.VERTEX; + descriptor.middle = descriptor.finalMiddle = Endpoint.FACE; + descriptor.end = Endpoint.EDGE; + descriptor.finalEnd = Endpoint.FACE; + descriptor.startVertex = descriptor.finalStartVertex = vertE; + descriptor.endVertex = vertB; + descriptor.finalEndVertex = vertX; + descriptor.startVertIdx = 0; + descriptor.endVertIdx = 2; + + PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); + + NUnit.Framework.Assert.AreEqual(3, obj.polygons.Count); + NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 0)); + + // Case 6.3g + poly = toPoly(vertE, vertA, vertB, vertY, vertD, vertC); + obj = toObj(poly); + + descriptor.start = descriptor.finalStart = Endpoint.VERTEX; + descriptor.middle = descriptor.finalMiddle = Endpoint.FACE; + descriptor.end = Endpoint.VERTEX; + descriptor.finalEnd = Endpoint.FACE; + descriptor.startVertex = descriptor.finalStartVertex = vertE; + descriptor.endVertex = vertY; + descriptor.finalEndVertex = vertX; + descriptor.startVertIdx = 0; + descriptor.endVertIdx = 3; + + PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); + NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 0)); + } - [Test] - public void TestSplit_EdgeEdgeVertex() { - CsgVertex vertA = new CsgVertex(new Vector3(-2, -1, -1)); - CsgVertex vertB = new CsgVertex(new Vector3(0, -1, -1)); - CsgVertex vertC = new CsgVertex(new Vector3(0, -1, 1)); - CsgVertex vertD = new CsgVertex(new Vector3(-2, -1, 1)); - CsgVertex vertX = new CsgVertex(new Vector3(-1, -1, -1)); - - CsgPolygon poly = toPoly(vertA, vertB, vertC, vertD); - CsgObject obj = toObj(poly); - - SegmentDescriptor descriptor = new SegmentDescriptor(); - descriptor.start = Endpoint.VERTEX; - descriptor.middle = Endpoint.EDGE; - descriptor.end = Endpoint.VERTEX; - descriptor.finalStart = Endpoint.EDGE; - descriptor.finalMiddle = Endpoint.EDGE; - descriptor.finalEnd = Endpoint.VERTEX; - descriptor.startVertex = vertA; - descriptor.endVertex = vertB; - descriptor.finalStartVertex = vertX; - descriptor.finalEndVertex = vertB; - descriptor.startVertIdx = 0; - descriptor.endVertIdx = 1; - - PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); - NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 1)); - } + [Test] + public void TestSplit_EdgeEdgeVertex() + { + CsgVertex vertA = new CsgVertex(new Vector3(-2, -1, -1)); + CsgVertex vertB = new CsgVertex(new Vector3(0, -1, -1)); + CsgVertex vertC = new CsgVertex(new Vector3(0, -1, 1)); + CsgVertex vertD = new CsgVertex(new Vector3(-2, -1, 1)); + CsgVertex vertX = new CsgVertex(new Vector3(-1, -1, -1)); + + CsgPolygon poly = toPoly(vertA, vertB, vertC, vertD); + CsgObject obj = toObj(poly); + + SegmentDescriptor descriptor = new SegmentDescriptor(); + descriptor.start = Endpoint.VERTEX; + descriptor.middle = Endpoint.EDGE; + descriptor.end = Endpoint.VERTEX; + descriptor.finalStart = Endpoint.EDGE; + descriptor.finalMiddle = Endpoint.EDGE; + descriptor.finalEnd = Endpoint.VERTEX; + descriptor.startVertex = vertA; + descriptor.endVertex = vertB; + descriptor.finalStartVertex = vertX; + descriptor.finalEndVertex = vertB; + descriptor.startVertIdx = 0; + descriptor.endVertIdx = 1; + + PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); + NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 1)); + } - [Test] - public void TestSplit_VertexEdgeEdge() { - CsgVertex vertA = new CsgVertex(new Vector3(0, 0.3f, -1.3f)); - CsgVertex vertB = new CsgVertex(new Vector3(0, 0.3f, 0.7f)); - CsgVertex vertC = new CsgVertex(new Vector3(0, -1, 0.7f)); - CsgVertex vertD = new CsgVertex(new Vector3(0, -1, -1)); - CsgVertex vertX = new CsgVertex(new Vector3(0, 0.3f, -1)); - - CsgPolygon poly = toPoly(vertA, vertB, vertC, vertD); - CsgObject obj = toObj(poly); - - SegmentDescriptor descriptor = new SegmentDescriptor(); - - descriptor.start = Endpoint.VERTEX; - descriptor.middle = Endpoint.EDGE; - descriptor.end = Endpoint.EDGE; - descriptor.finalStart = Endpoint.VERTEX; - descriptor.finalMiddle = Endpoint.EDGE; - descriptor.finalEnd = Endpoint.EDGE; - descriptor.startVertex = vertD; - descriptor.endVertex = vertX; - descriptor.finalStartVertex = vertD; - descriptor.finalEndVertex = vertX; - descriptor.startVertIdx = 3; - descriptor.endVertIdx = 0; - - PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); - NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 1)); - } + [Test] + public void TestSplit_VertexEdgeEdge() + { + CsgVertex vertA = new CsgVertex(new Vector3(0, 0.3f, -1.3f)); + CsgVertex vertB = new CsgVertex(new Vector3(0, 0.3f, 0.7f)); + CsgVertex vertC = new CsgVertex(new Vector3(0, -1, 0.7f)); + CsgVertex vertD = new CsgVertex(new Vector3(0, -1, -1)); + CsgVertex vertX = new CsgVertex(new Vector3(0, 0.3f, -1)); + + CsgPolygon poly = toPoly(vertA, vertB, vertC, vertD); + CsgObject obj = toObj(poly); + + SegmentDescriptor descriptor = new SegmentDescriptor(); + + descriptor.start = Endpoint.VERTEX; + descriptor.middle = Endpoint.EDGE; + descriptor.end = Endpoint.EDGE; + descriptor.finalStart = Endpoint.VERTEX; + descriptor.finalMiddle = Endpoint.EDGE; + descriptor.finalEnd = Endpoint.EDGE; + descriptor.startVertex = vertD; + descriptor.endVertex = vertX; + descriptor.finalStartVertex = vertD; + descriptor.finalEndVertex = vertX; + descriptor.startVertIdx = 3; + descriptor.endVertIdx = 0; + + PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); + NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 1)); + } - /// - /// Tests case for a split which occurs along an edge. - /// - [Test] - public void TestSplit_EdgeEdgeEdge_Fig_i() { - CsgVertex vertA = new CsgVertex(new Vector3(-1, -1, 0)); - CsgVertex vertB = new CsgVertex(new Vector3(-1.2f, 0, 0)); - CsgVertex vertC = new CsgVertex(new Vector3(-1, 1, 0)); - CsgVertex vertD = new CsgVertex(new Vector3(1, 1, 0)); - CsgVertex vertE = new CsgVertex(new Vector3(1.2f, 0, 0)); - CsgVertex vertF = new CsgVertex(new Vector3(1, -1, 0)); - - CsgVertex vertN = new CsgVertex(new Vector3(-.2f, -1, 0)); - CsgVertex vertM = new CsgVertex(new Vector3(-.1f, -1, 0)); - - // Case 6.3i - CsgPolygon poly = toPoly(vertA, vertB, vertC, vertD, vertE, vertF); - CsgObject obj = toObj(poly); - - SegmentDescriptor descriptor = new SegmentDescriptor(); - descriptor.start = Endpoint.EDGE; - descriptor.middle = Endpoint.EDGE; - descriptor.end = Endpoint.EDGE; - - descriptor.finalStart = Endpoint.EDGE; - descriptor.finalMiddle = Endpoint.EDGE; - descriptor.finalEnd = Endpoint.EDGE; - - descriptor.startVertex = vertF; - descriptor.finalStartVertex = vertM; - descriptor.endVertex = vertA; - descriptor.finalEndVertex = vertN; - descriptor.startVertIdx = 5; - descriptor.endVertIdx = 0; - - PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); - - NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 1)); - } + /// + /// Tests case for a split which occurs along an edge. + /// + [Test] + public void TestSplit_EdgeEdgeEdge_Fig_i() + { + CsgVertex vertA = new CsgVertex(new Vector3(-1, -1, 0)); + CsgVertex vertB = new CsgVertex(new Vector3(-1.2f, 0, 0)); + CsgVertex vertC = new CsgVertex(new Vector3(-1, 1, 0)); + CsgVertex vertD = new CsgVertex(new Vector3(1, 1, 0)); + CsgVertex vertE = new CsgVertex(new Vector3(1.2f, 0, 0)); + CsgVertex vertF = new CsgVertex(new Vector3(1, -1, 0)); + + CsgVertex vertN = new CsgVertex(new Vector3(-.2f, -1, 0)); + CsgVertex vertM = new CsgVertex(new Vector3(-.1f, -1, 0)); + + // Case 6.3i + CsgPolygon poly = toPoly(vertA, vertB, vertC, vertD, vertE, vertF); + CsgObject obj = toObj(poly); + + SegmentDescriptor descriptor = new SegmentDescriptor(); + descriptor.start = Endpoint.EDGE; + descriptor.middle = Endpoint.EDGE; + descriptor.end = Endpoint.EDGE; + + descriptor.finalStart = Endpoint.EDGE; + descriptor.finalMiddle = Endpoint.EDGE; + descriptor.finalEnd = Endpoint.EDGE; + + descriptor.startVertex = vertF; + descriptor.finalStartVertex = vertM; + descriptor.endVertex = vertA; + descriptor.finalEndVertex = vertN; + descriptor.startVertIdx = 5; + descriptor.endVertIdx = 0; + + PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); + + NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 1)); + } - /// - /// Diagram for this test. Fig 6.3(n) - /// - /// +----------+ - /// / \ / \ - /// / \ / \ - /// +B +N +M +E - /// \ / \ / - /// \ / \ / - /// +----------+ - /// - /// - [Test] - public void TestSplit_FaceFaceFace() { - CsgVertex vertA = new CsgVertex(new Vector3(-1, -1, 0)); - CsgVertex vertB = new CsgVertex(new Vector3(-1.2f, 0, 0)); - CsgVertex vertC = new CsgVertex(new Vector3(-1, 1, 0)); - CsgVertex vertD = new CsgVertex(new Vector3(1, 1, 0)); - CsgVertex vertE = new CsgVertex(new Vector3(1.2f, 0, 0)); - CsgVertex vertF = new CsgVertex(new Vector3(1, -1, 0)); - - CsgVertex vertN = new CsgVertex(new Vector3(0, 0, 0)); - CsgVertex vertM = new CsgVertex(new Vector3(1, 0, 0)); - - // Case 6.3n - CsgPolygon poly = toPoly(vertA, vertB, vertC, vertD, vertE, vertF); - CsgObject obj = toObj(poly); - - SegmentDescriptor descriptor = new SegmentDescriptor(); - descriptor.start = Endpoint.VERTEX; - descriptor.middle = Endpoint.FACE; - descriptor.end = Endpoint.VERTEX; - - descriptor.finalStart = Endpoint.FACE; - descriptor.finalMiddle = Endpoint.FACE; - descriptor.finalEnd = Endpoint.FACE; - - descriptor.startVertex = vertB; - descriptor.finalStartVertex = vertN; - descriptor.endVertex = vertE; - descriptor.finalEndVertex = vertM; - descriptor.startVertIdx = 1; - descriptor.endVertIdx = 4; - - PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); - - NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 0)); - } + /// + /// Diagram for this test. Fig 6.3(n) + /// + /// +----------+ + /// / \ / \ + /// / \ / \ + /// +B +N +M +E + /// \ / \ / + /// \ / \ / + /// +----------+ + /// + /// + [Test] + public void TestSplit_FaceFaceFace() + { + CsgVertex vertA = new CsgVertex(new Vector3(-1, -1, 0)); + CsgVertex vertB = new CsgVertex(new Vector3(-1.2f, 0, 0)); + CsgVertex vertC = new CsgVertex(new Vector3(-1, 1, 0)); + CsgVertex vertD = new CsgVertex(new Vector3(1, 1, 0)); + CsgVertex vertE = new CsgVertex(new Vector3(1.2f, 0, 0)); + CsgVertex vertF = new CsgVertex(new Vector3(1, -1, 0)); + + CsgVertex vertN = new CsgVertex(new Vector3(0, 0, 0)); + CsgVertex vertM = new CsgVertex(new Vector3(1, 0, 0)); + + // Case 6.3n + CsgPolygon poly = toPoly(vertA, vertB, vertC, vertD, vertE, vertF); + CsgObject obj = toObj(poly); + + SegmentDescriptor descriptor = new SegmentDescriptor(); + descriptor.start = Endpoint.VERTEX; + descriptor.middle = Endpoint.FACE; + descriptor.end = Endpoint.VERTEX; + + descriptor.finalStart = Endpoint.FACE; + descriptor.finalMiddle = Endpoint.FACE; + descriptor.finalEnd = Endpoint.FACE; + + descriptor.startVertex = vertB; + descriptor.finalStartVertex = vertN; + descriptor.endVertex = vertE; + descriptor.finalEndVertex = vertM; + descriptor.startVertIdx = 1; + descriptor.endVertIdx = 4; + + PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); + + NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 0)); + } - /// - /// Diagram for this test. Fig 6.3(n) - /// - /// +----------+ - /// / \ /| - /// / \ / | - /// +B +N +M | - /// \ / \ | - /// \ / \| - /// +----------+ - /// - /// - [Test] - public void TestSplit_FaceFaceFace_Fig_o() { - CsgVertex vertA = new CsgVertex(new Vector3(-1, -1, 0)); - CsgVertex vertB = new CsgVertex(new Vector3(-1.2f, 0, 0)); - CsgVertex vertC = new CsgVertex(new Vector3(-1, 1, 0)); - CsgVertex vertD = new CsgVertex(new Vector3(1, 1, 0)); - CsgVertex vertF = new CsgVertex(new Vector3(1, -1, 0)); - - CsgVertex vertN = new CsgVertex(new Vector3(0, 0, 0)); - CsgVertex vertM = new CsgVertex(new Vector3(.3f, 0, 0)); - CsgVertex endVertexOnEdge = new CsgVertex(new Vector3(1, 0, 0)); - - // Case 6.3n - CsgPolygon poly = toPoly(vertA, vertB, vertC, vertD, vertF); - CsgObject obj = toObj(poly); - - SegmentDescriptor descriptor = new SegmentDescriptor(); - descriptor.start = Endpoint.VERTEX; - descriptor.middle = Endpoint.FACE; - descriptor.end = Endpoint.EDGE; - - descriptor.finalStart = Endpoint.FACE; - descriptor.finalMiddle = Endpoint.FACE; - descriptor.finalEnd = Endpoint.FACE; - - descriptor.startVertex = vertB; - descriptor.finalStartVertex = vertN; - descriptor.endVertex = endVertexOnEdge; - descriptor.finalEndVertex = vertM; - descriptor.startVertIdx = 1; - descriptor.endVertIdx = 3; - - PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); - - NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 0)); - } + /// + /// Diagram for this test. Fig 6.3(n) + /// + /// +----------+ + /// / \ /| + /// / \ / | + /// +B +N +M | + /// \ / \ | + /// \ / \| + /// +----------+ + /// + /// + [Test] + public void TestSplit_FaceFaceFace_Fig_o() + { + CsgVertex vertA = new CsgVertex(new Vector3(-1, -1, 0)); + CsgVertex vertB = new CsgVertex(new Vector3(-1.2f, 0, 0)); + CsgVertex vertC = new CsgVertex(new Vector3(-1, 1, 0)); + CsgVertex vertD = new CsgVertex(new Vector3(1, 1, 0)); + CsgVertex vertF = new CsgVertex(new Vector3(1, -1, 0)); + + CsgVertex vertN = new CsgVertex(new Vector3(0, 0, 0)); + CsgVertex vertM = new CsgVertex(new Vector3(.3f, 0, 0)); + CsgVertex endVertexOnEdge = new CsgVertex(new Vector3(1, 0, 0)); + + // Case 6.3n + CsgPolygon poly = toPoly(vertA, vertB, vertC, vertD, vertF); + CsgObject obj = toObj(poly); + + SegmentDescriptor descriptor = new SegmentDescriptor(); + descriptor.start = Endpoint.VERTEX; + descriptor.middle = Endpoint.FACE; + descriptor.end = Endpoint.EDGE; + + descriptor.finalStart = Endpoint.FACE; + descriptor.finalMiddle = Endpoint.FACE; + descriptor.finalEnd = Endpoint.FACE; + + descriptor.startVertex = vertB; + descriptor.finalStartVertex = vertN; + descriptor.endVertex = endVertexOnEdge; + descriptor.finalEndVertex = vertM; + descriptor.startVertIdx = 1; + descriptor.endVertIdx = 3; + + PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); + + NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 0)); + } - /// - /// Diagram for this test. Fig 6.3(n) - /// - /// +----------+ - /// |\ / \ - /// | \ / \ - /// | +N +M +E - /// | / \ / - /// |/ \ / - /// +----------+ - /// - /// - [Test] - public void TestSplit_FaceFaceFace_Fig_p() { - CsgVertex vertA = new CsgVertex(new Vector3(-1, -1, 0)); - CsgVertex vertC = new CsgVertex(new Vector3(-1, 1, 0)); - CsgVertex vertD = new CsgVertex(new Vector3(1, 1, 0)); - CsgVertex vertE = new CsgVertex(new Vector3(1.2f, 0, 0)); - CsgVertex vertF = new CsgVertex(new Vector3(1, -1, 0)); - - CsgVertex vertN = new CsgVertex(new Vector3(0, 0, 0)); - CsgVertex vertM = new CsgVertex(new Vector3(1, 0, 0)); - - // Case 6.3n - CsgPolygon poly = toPoly(vertA, vertC, vertD, vertE, vertF); - CsgObject obj = toObj(poly); - - SegmentDescriptor descriptor = new SegmentDescriptor(); - descriptor.start = Endpoint.EDGE; - descriptor.middle = Endpoint.FACE; - descriptor.end = Endpoint.VERTEX; - - descriptor.finalStart = Endpoint.FACE; - descriptor.finalMiddle = Endpoint.FACE; - descriptor.finalEnd = Endpoint.FACE; - - descriptor.startVertex = new CsgVertex(new Vector3(-1, 0, 0)); - descriptor.finalStartVertex = vertN; - descriptor.endVertex = vertE; - descriptor.finalEndVertex = vertM; - descriptor.startVertIdx = 0; - descriptor.endVertIdx = 3; - - PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); - - NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 0)); - } + /// + /// Diagram for this test. Fig 6.3(n) + /// + /// +----------+ + /// |\ / \ + /// | \ / \ + /// | +N +M +E + /// | / \ / + /// |/ \ / + /// +----------+ + /// + /// + [Test] + public void TestSplit_FaceFaceFace_Fig_p() + { + CsgVertex vertA = new CsgVertex(new Vector3(-1, -1, 0)); + CsgVertex vertC = new CsgVertex(new Vector3(-1, 1, 0)); + CsgVertex vertD = new CsgVertex(new Vector3(1, 1, 0)); + CsgVertex vertE = new CsgVertex(new Vector3(1.2f, 0, 0)); + CsgVertex vertF = new CsgVertex(new Vector3(1, -1, 0)); + + CsgVertex vertN = new CsgVertex(new Vector3(0, 0, 0)); + CsgVertex vertM = new CsgVertex(new Vector3(1, 0, 0)); + + // Case 6.3n + CsgPolygon poly = toPoly(vertA, vertC, vertD, vertE, vertF); + CsgObject obj = toObj(poly); + + SegmentDescriptor descriptor = new SegmentDescriptor(); + descriptor.start = Endpoint.EDGE; + descriptor.middle = Endpoint.FACE; + descriptor.end = Endpoint.VERTEX; + + descriptor.finalStart = Endpoint.FACE; + descriptor.finalMiddle = Endpoint.FACE; + descriptor.finalEnd = Endpoint.FACE; + + descriptor.startVertex = new CsgVertex(new Vector3(-1, 0, 0)); + descriptor.finalStartVertex = vertN; + descriptor.endVertex = vertE; + descriptor.finalEndVertex = vertM; + descriptor.startVertIdx = 0; + descriptor.endVertIdx = 3; + + PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); + + NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 0)); + } - [Test] - public void TestSplit_FaceFaceFace_ExtraCases_FFF() { - CsgVertex vertA = new CsgVertex(new Vector3(-1, -1, 0)); - CsgVertex vertB = new CsgVertex(new Vector3(-1, 1, 0)); - CsgVertex vertC = new CsgVertex(new Vector3(1, 1, 0)); - CsgVertex vertD = new CsgVertex(new Vector3(1, -1, 0)); + [Test] + public void TestSplit_FaceFaceFace_ExtraCases_FFF() + { + CsgVertex vertA = new CsgVertex(new Vector3(-1, -1, 0)); + CsgVertex vertB = new CsgVertex(new Vector3(-1, 1, 0)); + CsgVertex vertC = new CsgVertex(new Vector3(1, 1, 0)); + CsgVertex vertD = new CsgVertex(new Vector3(1, -1, 0)); - CsgVertex vertN = new CsgVertex(new Vector3(-.9f, .1f, 0)); - CsgVertex vertM = new CsgVertex(new Vector3(-.8f, .2f, 0)); + CsgVertex vertN = new CsgVertex(new Vector3(-.9f, .1f, 0)); + CsgVertex vertM = new CsgVertex(new Vector3(-.8f, .2f, 0)); - // Case 6.3n - CsgPolygon poly = toPoly(vertA, vertB, vertC, vertD); - CsgObject obj = toObj(poly); + // Case 6.3n + CsgPolygon poly = toPoly(vertA, vertB, vertC, vertD); + CsgObject obj = toObj(poly); - SegmentDescriptor descriptor = new SegmentDescriptor(); - descriptor.start = Endpoint.FACE; - descriptor.middle = Endpoint.FACE; - descriptor.end = Endpoint.FACE; + SegmentDescriptor descriptor = new SegmentDescriptor(); + descriptor.start = Endpoint.FACE; + descriptor.middle = Endpoint.FACE; + descriptor.end = Endpoint.FACE; - descriptor.finalStart = Endpoint.FACE; - descriptor.finalMiddle = Endpoint.FACE; - descriptor.finalEnd = Endpoint.FACE; + descriptor.finalStart = Endpoint.FACE; + descriptor.finalMiddle = Endpoint.FACE; + descriptor.finalEnd = Endpoint.FACE; - descriptor.startVertex = new CsgVertex(new Vector3(-1, 0, 0)); - descriptor.finalStartVertex = vertN; - descriptor.endVertex = new CsgVertex(new Vector3(0, 1, 0)); - descriptor.finalEndVertex = vertM; - descriptor.startVertIdx = 0; - descriptor.endVertIdx = 1; + descriptor.startVertex = new CsgVertex(new Vector3(-1, 0, 0)); + descriptor.finalStartVertex = vertN; + descriptor.endVertex = new CsgVertex(new Vector3(0, 1, 0)); + descriptor.finalEndVertex = vertM; + descriptor.startVertIdx = 0; + descriptor.endVertIdx = 1; - PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); + PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); - NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 0)); - } + NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 0)); + } - [Test] - public void TestSplit_FaceFaceFace_ExtraCases_FFF2() { - CsgVertex vertA = new CsgVertex(new Vector3(-1, -1, 0)); - CsgVertex vertB = new CsgVertex(new Vector3(-1, 1, 0)); - CsgVertex vertC = new CsgVertex(new Vector3(1, 1, 0)); - CsgVertex vertD = new CsgVertex(new Vector3(1, -1, 0)); + [Test] + public void TestSplit_FaceFaceFace_ExtraCases_FFF2() + { + CsgVertex vertA = new CsgVertex(new Vector3(-1, -1, 0)); + CsgVertex vertB = new CsgVertex(new Vector3(-1, 1, 0)); + CsgVertex vertC = new CsgVertex(new Vector3(1, 1, 0)); + CsgVertex vertD = new CsgVertex(new Vector3(1, -1, 0)); - CsgVertex vertN = new CsgVertex(new Vector3(-.8f, -.2f, 0)); - CsgVertex vertM = new CsgVertex(new Vector3(-.9f, -.1f, 0)); + CsgVertex vertN = new CsgVertex(new Vector3(-.8f, -.2f, 0)); + CsgVertex vertM = new CsgVertex(new Vector3(-.9f, -.1f, 0)); - // Case 6.3n - CsgPolygon poly = toPoly(vertA, vertB, vertC, vertD); - CsgObject obj = toObj(poly); + // Case 6.3n + CsgPolygon poly = toPoly(vertA, vertB, vertC, vertD); + CsgObject obj = toObj(poly); - SegmentDescriptor descriptor = new SegmentDescriptor(); - descriptor.start = Endpoint.FACE; - descriptor.middle = Endpoint.FACE; - descriptor.end = Endpoint.FACE; + SegmentDescriptor descriptor = new SegmentDescriptor(); + descriptor.start = Endpoint.FACE; + descriptor.middle = Endpoint.FACE; + descriptor.end = Endpoint.FACE; - descriptor.finalStart = Endpoint.FACE; - descriptor.finalMiddle = Endpoint.FACE; - descriptor.finalEnd = Endpoint.FACE; + descriptor.finalStart = Endpoint.FACE; + descriptor.finalMiddle = Endpoint.FACE; + descriptor.finalEnd = Endpoint.FACE; - descriptor.startVertex = new CsgVertex(new Vector3(0, -1, 0)); - descriptor.finalStartVertex = vertN; - descriptor.endVertex = new CsgVertex(new Vector3(-1, 0, 0)); - descriptor.finalEndVertex = vertM; - descriptor.startVertIdx = 3; - descriptor.endVertIdx = 0; + descriptor.startVertex = new CsgVertex(new Vector3(0, -1, 0)); + descriptor.finalStartVertex = vertN; + descriptor.endVertex = new CsgVertex(new Vector3(-1, 0, 0)); + descriptor.finalEndVertex = vertM; + descriptor.startVertIdx = 3; + descriptor.endVertIdx = 0; - PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); + PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); - NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 0)); - } + NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 0)); + } - [Test] - public void TestSplit_FaceFaceFace_ExtraCases_VFF() { - CsgVertex vertA = new CsgVertex(new Vector3(-0.8349411f, 0.1900979f, 0.7f)); - CsgVertex vertB = new CsgVertex(new Vector3(-2f, -1.7f, 0.7f)); - CsgVertex vertC = new CsgVertex(new Vector3(0f, -1.7f, 0.7f)); + [Test] + public void TestSplit_FaceFaceFace_ExtraCases_VFF() + { + CsgVertex vertA = new CsgVertex(new Vector3(-0.8349411f, 0.1900979f, 0.7f)); + CsgVertex vertB = new CsgVertex(new Vector3(-2f, -1.7f, 0.7f)); + CsgVertex vertC = new CsgVertex(new Vector3(0f, -1.7f, 0.7f)); - CsgVertex vertN = new CsgVertex(new Vector3(-0.8349411f, 0.1900979f, 0.7f)); - CsgVertex vertM = new CsgVertex(new Vector3(-0.8349411f, -0.1900979f, 0.7f)); + CsgVertex vertN = new CsgVertex(new Vector3(-0.8349411f, 0.1900979f, 0.7f)); + CsgVertex vertM = new CsgVertex(new Vector3(-0.8349411f, -0.1900979f, 0.7f)); - // Case 6.3n - CsgPolygon poly = toPoly(vertA, vertB, vertC); - CsgObject obj = toObj(poly); + // Case 6.3n + CsgPolygon poly = toPoly(vertA, vertB, vertC); + CsgObject obj = toObj(poly); - SegmentDescriptor descriptor = new SegmentDescriptor(); - descriptor.start = Endpoint.VERTEX; - descriptor.middle = Endpoint.FACE; - descriptor.end = Endpoint.EDGE; + SegmentDescriptor descriptor = new SegmentDescriptor(); + descriptor.start = Endpoint.VERTEX; + descriptor.middle = Endpoint.FACE; + descriptor.end = Endpoint.EDGE; - descriptor.finalStart = Endpoint.VERTEX; - descriptor.finalMiddle = Endpoint.FACE; - descriptor.finalEnd = Endpoint.FACE; + descriptor.finalStart = Endpoint.VERTEX; + descriptor.finalMiddle = Endpoint.FACE; + descriptor.finalEnd = Endpoint.FACE; - descriptor.startVertex = new CsgVertex(new Vector3(-0.8349411f, 0.1900979f, 0.7f)); - descriptor.finalStartVertex = vertN; - descriptor.endVertex = new CsgVertex(new Vector3(-0.834941f, -1.7f, 0.7f)); - descriptor.finalEndVertex = vertM; - descriptor.startVertIdx = 0; - descriptor.endVertIdx = 1; + descriptor.startVertex = new CsgVertex(new Vector3(-0.8349411f, 0.1900979f, 0.7f)); + descriptor.finalStartVertex = vertN; + descriptor.endVertex = new CsgVertex(new Vector3(-0.834941f, -1.7f, 0.7f)); + descriptor.finalEndVertex = vertM; + descriptor.startVertIdx = 0; + descriptor.endVertIdx = 1; - PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); + PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); - NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 0)); - } + NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 0)); + } - [Test] - public void TestSplit_VertexEdgeEdge_NarrowPoly() { - - CsgVertex vertA = new CsgVertex(new Vector3(-0.6395259f, 0.3f, -1.063093f)); - CsgVertex vertB = new CsgVertex(new Vector3(-0.6942508f, 0.3f, -1.051798f)); - CsgVertex vertC = new CsgVertex(new Vector3(-0.73f, 0.3f, -1.039017f)); - CsgVertex vertD = new CsgVertex(new Vector3(-0.7515792f, 0.3f, -1.021583f)); - CsgVertex vertX = new CsgVertex(new Vector3(-0.6992299f, 0.3f, -1.05077f)); - - CsgPolygon poly = toPoly(vertA, vertB, vertC, vertD); - CsgObject obj = toObj(poly); - - SegmentDescriptor descriptor = new SegmentDescriptor(); - descriptor.start = 1; - descriptor.middle = 3; - descriptor.end = 1; - descriptor.finalStart = 1; - descriptor.finalMiddle = 3; - descriptor.finalEnd = 3; - descriptor.startVertex = vertC; - descriptor.endVertex = vertB; - descriptor.finalStartVertex = vertC; - descriptor.finalEndVertex = vertX; - descriptor.startVertIdx = 2; - descriptor.endVertIdx = 1; - PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); - - NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 1)); - } + [Test] + public void TestSplit_VertexEdgeEdge_NarrowPoly() + { + + CsgVertex vertA = new CsgVertex(new Vector3(-0.6395259f, 0.3f, -1.063093f)); + CsgVertex vertB = new CsgVertex(new Vector3(-0.6942508f, 0.3f, -1.051798f)); + CsgVertex vertC = new CsgVertex(new Vector3(-0.73f, 0.3f, -1.039017f)); + CsgVertex vertD = new CsgVertex(new Vector3(-0.7515792f, 0.3f, -1.021583f)); + CsgVertex vertX = new CsgVertex(new Vector3(-0.6992299f, 0.3f, -1.05077f)); + + CsgPolygon poly = toPoly(vertA, vertB, vertC, vertD); + CsgObject obj = toObj(poly); + + SegmentDescriptor descriptor = new SegmentDescriptor(); + descriptor.start = 1; + descriptor.middle = 3; + descriptor.end = 1; + descriptor.finalStart = 1; + descriptor.finalMiddle = 3; + descriptor.finalEnd = 3; + descriptor.startVertex = vertC; + descriptor.endVertex = vertB; + descriptor.finalStartVertex = vertC; + descriptor.finalEndVertex = vertX; + descriptor.startVertIdx = 2; + descriptor.endVertIdx = 1; + PolygonSplitter.SplitPolyOnSegment(obj, poly, descriptor); + + NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly, obj.polygons, 1)); + } - [Test] - public void TestTwoPolygons() { - CsgPolygon poly1 = toPoly( - new CsgVertex(new Vector3(-2f, 0.3f, -1.3f)), - new CsgVertex(new Vector3(-2f, 0.3f, 0.7f)), - new CsgVertex(new Vector3(0f, 0.3f, 0.7f)), - new CsgVertex(new Vector3(0f, 0.3f, -1.3f)) - ); - CsgPolygon poly2 = toPoly( - new CsgVertex(new Vector3(-1.050651f, 0f, 0.5257311f)), - new CsgVertex(new Vector3(-0.7f, 0.309017f, 0.809017f)), - new CsgVertex(new Vector3(-1.009017f, 0.5f, 0.309017f)) - ); - - CsgObject obj = toObj(poly1); - PolygonSplitter.SplitPolys(new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)), obj, poly1, poly2); - - NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly1, obj.polygons, 1)); - } + [Test] + public void TestTwoPolygons() + { + CsgPolygon poly1 = toPoly( + new CsgVertex(new Vector3(-2f, 0.3f, -1.3f)), + new CsgVertex(new Vector3(-2f, 0.3f, 0.7f)), + new CsgVertex(new Vector3(0f, 0.3f, 0.7f)), + new CsgVertex(new Vector3(0f, 0.3f, -1.3f)) + ); + CsgPolygon poly2 = toPoly( + new CsgVertex(new Vector3(-1.050651f, 0f, 0.5257311f)), + new CsgVertex(new Vector3(-0.7f, 0.309017f, 0.809017f)), + new CsgVertex(new Vector3(-1.009017f, 0.5f, 0.309017f)) + ); + + CsgObject obj = toObj(poly1); + PolygonSplitter.SplitPolys(new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)), obj, poly1, poly2); + + NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly1, obj.polygons, 1)); + } - //[TEST] - public void TestTwoPolygons_EFF() { - CsgPolygon poly1 = toPoly( - new CsgVertex(new Vector3(0.1f, 0.9f, 0.4f)), - new CsgVertex(new Vector3(0.1f, 1.1f, 0.4f)), - new CsgVertex(new Vector3(0.1f, 1.1f, 0.6f)), - new CsgVertex(new Vector3(0.1f, 0.9f, 0.6f)) - ); - CsgPolygon poly2 = toPoly( - new CsgVertex(new Vector3(0.0812017f, 1.131002f, 0.5002f)), - new CsgVertex(new Vector3(0.1312017f, 1.1001f, 0.5192983f)), - new CsgVertex(new Vector3(0.1003f, 1.081002f, 0.4692983f)) - ); - - CsgObject obj = toObj(poly1); - PolygonSplitter.SplitPolys(new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)), obj, poly1, poly2); - - NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly1, obj.polygons, 1)); - } + //[TEST] + public void TestTwoPolygons_EFF() + { + CsgPolygon poly1 = toPoly( + new CsgVertex(new Vector3(0.1f, 0.9f, 0.4f)), + new CsgVertex(new Vector3(0.1f, 1.1f, 0.4f)), + new CsgVertex(new Vector3(0.1f, 1.1f, 0.6f)), + new CsgVertex(new Vector3(0.1f, 0.9f, 0.6f)) + ); + CsgPolygon poly2 = toPoly( + new CsgVertex(new Vector3(0.0812017f, 1.131002f, 0.5002f)), + new CsgVertex(new Vector3(0.1312017f, 1.1001f, 0.5192983f)), + new CsgVertex(new Vector3(0.1003f, 1.081002f, 0.4692983f)) + ); + + CsgObject obj = toObj(poly1); + PolygonSplitter.SplitPolys(new CsgContext(new Bounds(Vector3.zero, Vector3.one * 5f)), obj, poly1, poly2); + + NUnit.Framework.Assert.IsTrue(CsgUtil.IsValidPolygonSplit(poly1, obj.polygons, 1)); + } - private CsgPolygon toPoly(params CsgVertex[] verts) { - return new CsgPolygon(new List(verts), new core.FaceProperties()); - } + private CsgPolygon toPoly(params CsgVertex[] verts) + { + return new CsgPolygon(new List(verts), new core.FaceProperties()); + } - private CsgPolygon toPoly(params Vector3[] verts) { - List poly = new List(); - foreach (Vector3 vert in verts) { - poly.Add(new CsgVertex(vert)); - } - return new CsgPolygon(poly, new core.FaceProperties()); - } + private CsgPolygon toPoly(params Vector3[] verts) + { + List poly = new List(); + foreach (Vector3 vert in verts) + { + poly.Add(new CsgVertex(vert)); + } + return new CsgPolygon(poly, new core.FaceProperties()); + } - private CsgObject toObj(params CsgPolygon[] polys) { - HashSet verts = new HashSet(); - foreach (CsgPolygon poly in polys) { - foreach (CsgVertex vert in poly.vertices) { - verts.Add(vert); + private CsgObject toObj(params CsgPolygon[] polys) + { + HashSet verts = new HashSet(); + foreach (CsgPolygon poly in polys) + { + foreach (CsgVertex vert in poly.vertices) + { + verts.Add(vert); + } + } + return new CsgObject(new List(polys), new List(verts)); } - } - return new CsgObject(new List(polys), new List(verts)); } - } } diff --git a/Assets/Editor/tests/model/render/FaceTriangulatorTest.cs b/Assets/Editor/tests/model/render/FaceTriangulatorTest.cs index d2041e03..894e9e6d 100644 --- a/Assets/Editor/tests/model/render/FaceTriangulatorTest.cs +++ b/Assets/Editor/tests/model/render/FaceTriangulatorTest.cs @@ -20,247 +20,261 @@ using com.google.apps.peltzer.client.model.core; using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.model.render { - [TestFixture] - public class FaceTriangulatorTest { - [Test] - public void TriangleTest() { - // Clockwise winded triangle. - Vertex v1 = new Vertex(1, new Vector3(5, 0, 0)); - Vertex v2 = new Vertex(2, Vector3.zero); - Vertex v3 = new Vertex(3, new Vector3(0, 3, 4)); - List border = new List(); - border.Add(v1); - border.Add(v2); - border.Add(v3); - List triangles = FaceTriangulator.Triangulate(border); - List expected = new List(); - expected.Add(new Triangle(v1.id, v2.id, v3.id)); +namespace com.google.apps.peltzer.client.model.render +{ + [TestFixture] + public class FaceTriangulatorTest + { + [Test] + public void TriangleTest() + { + // Clockwise winded triangle. + Vertex v1 = new Vertex(1, new Vector3(5, 0, 0)); + Vertex v2 = new Vertex(2, Vector3.zero); + Vertex v3 = new Vertex(3, new Vector3(0, 3, 4)); + List border = new List(); + border.Add(v1); + border.Add(v2); + border.Add(v3); + List triangles = FaceTriangulator.Triangulate(border); + List expected = new List(); + expected.Add(new Triangle(v1.id, v2.id, v3.id)); - // Simple triangle should triangulate to itself. - Assert.AreEqual(expected, triangles); - } + // Simple triangle should triangulate to itself. + Assert.AreEqual(expected, triangles); + } - [Test] - public void ConvexTest() { - // Simple convex shape. - Vertex v1 = new Vertex(0, new Vector3(0, 0, 0)); - Vertex v2 = new Vertex(1, new Vector3(0, 3, 5)); - Vertex v3 = new Vertex(2, new Vector3(0, 5, 3)); - Vertex v4 = new Vertex(3, new Vector3(0, 4, 1)); - Vertex v5 = new Vertex(4, new Vector3(0, 2, 0)); - List border = new List(); - border.Add(v1); - border.Add(v2); - border.Add(v3); - border.Add(v4); - border.Add(v5); - List triangles = FaceTriangulator.Triangulate(border); - HashSet usedVertices = new HashSet(); + [Test] + public void ConvexTest() + { + // Simple convex shape. + Vertex v1 = new Vertex(0, new Vector3(0, 0, 0)); + Vertex v2 = new Vertex(1, new Vector3(0, 3, 5)); + Vertex v3 = new Vertex(2, new Vector3(0, 5, 3)); + Vertex v4 = new Vertex(3, new Vector3(0, 4, 1)); + Vertex v5 = new Vertex(4, new Vector3(0, 2, 0)); + List border = new List(); + border.Add(v1); + border.Add(v2); + border.Add(v3); + border.Add(v4); + border.Add(v5); + List triangles = FaceTriangulator.Triangulate(border); + HashSet usedVertices = new HashSet(); - // There should be 3 triangles, each with a normal facing in negative x. - Assert.AreEqual(3, triangles.Count); - foreach (Triangle t in triangles) { - usedVertices.Add(t.vertId0); - usedVertices.Add(t.vertId1); - usedVertices.Add(t.vertId2); + // There should be 3 triangles, each with a normal facing in negative x. + Assert.AreEqual(3, triangles.Count); + foreach (Triangle t in triangles) + { + usedVertices.Add(t.vertId0); + usedVertices.Add(t.vertId1); + usedVertices.Add(t.vertId2); - Assert.Less(new Plane(border[t.vertId0].loc,border[t.vertId1].loc,border[t.vertId2].loc).normal.x,0); - } + Assert.Less(new Plane(border[t.vertId0].loc, border[t.vertId1].loc, border[t.vertId2].loc).normal.x, 0); + } - // All 5 vertices should be used. - Assert.AreEqual(5, usedVertices.Count); - } + // All 5 vertices should be used. + Assert.AreEqual(5, usedVertices.Count); + } - [Test] - public void ConcaveTest() { - Vertex v1 = new Vertex(1, new Vector3(0, 0, 0)); - Vertex v2 = new Vertex(2, new Vector3(0, 3, 5)); - Vertex v3 = new Vertex(3, new Vector3(0, 5, 3)); - Vertex v4 = new Vertex(4, new Vector3(0, 2, 2)); - Vertex v5 = new Vertex(5, new Vector3(0, 2, 0)); - List border = new List(); - border.Add(v1); - border.Add(v2); - border.Add(v3); - border.Add(v4); - border.Add(v5); - List triangles = FaceTriangulator.Triangulate(border); - // The polygon is restrictive enough to enforce only one possible triangulation. - List expected = new List(); - expected.Add(new Triangle(v1.id, v2.id, v4.id)); - expected.Add(new Triangle(v2.id, v3.id, v4.id)); - expected.Add(new Triangle(v1.id, v4.id, v5.id)); - Assert.AreEqual(3, triangles.Count); - Assert.Contains(expected[0], triangles); - Assert.Contains(expected[1], triangles); - Assert.Contains(expected[2], triangles); - } + [Test] + public void ConcaveTest() + { + Vertex v1 = new Vertex(1, new Vector3(0, 0, 0)); + Vertex v2 = new Vertex(2, new Vector3(0, 3, 5)); + Vertex v3 = new Vertex(3, new Vector3(0, 5, 3)); + Vertex v4 = new Vertex(4, new Vector3(0, 2, 2)); + Vertex v5 = new Vertex(5, new Vector3(0, 2, 0)); + List border = new List(); + border.Add(v1); + border.Add(v2); + border.Add(v3); + border.Add(v4); + border.Add(v5); + List triangles = FaceTriangulator.Triangulate(border); + // The polygon is restrictive enough to enforce only one possible triangulation. + List expected = new List(); + expected.Add(new Triangle(v1.id, v2.id, v4.id)); + expected.Add(new Triangle(v2.id, v3.id, v4.id)); + expected.Add(new Triangle(v1.id, v4.id, v5.id)); + Assert.AreEqual(3, triangles.Count); + Assert.Contains(expected[0], triangles); + Assert.Contains(expected[1], triangles); + Assert.Contains(expected[2], triangles); + } - [Test] - public void HoleTest() { - // Simple triangle with square inside. - Vertex v1 = new Vertex(0, new Vector3(0, 0, 0)); - Vertex v2 = new Vertex(1, new Vector3(0, 6, 8)); - Vertex v3 = new Vertex(2, new Vector3(0, 10, 0)); - Vertex v4 = new Vertex(3, new Vector3(0, 6, 1)); - Vertex v5 = new Vertex(4, new Vector3(0, 7, 2)); - Vertex v6 = new Vertex(5, new Vector3(0, 5, 5)); - Vertex v7 = new Vertex(6, new Vector3(0, 3.5f, 3)); - List border = new List(); - border.Add(v1); - border.Add(v2); - border.Add(v3); - List hole = new List(); - hole.Add(v4); - hole.Add(v5); - hole.Add(v6); - hole.Add(v7); - List all = new List(border); - all.AddRange(hole); - List> holes = new List>(); - holes.Add(hole); - List triangles = FaceTriangulator.Triangulate(border); - // Verify point in hole is not contained in any of the faces - foreach (Triangle t in triangles) { - Console.Write(t); - Assert.False(Math3d.TriangleContainsPoint( - all[t.vertId0].loc, all[t.vertId1].loc, - all[t.vertId2].loc,new Vector3(0,5,3))); - } - } + [Test] + public void HoleTest() + { + // Simple triangle with square inside. + Vertex v1 = new Vertex(0, new Vector3(0, 0, 0)); + Vertex v2 = new Vertex(1, new Vector3(0, 6, 8)); + Vertex v3 = new Vertex(2, new Vector3(0, 10, 0)); + Vertex v4 = new Vertex(3, new Vector3(0, 6, 1)); + Vertex v5 = new Vertex(4, new Vector3(0, 7, 2)); + Vertex v6 = new Vertex(5, new Vector3(0, 5, 5)); + Vertex v7 = new Vertex(6, new Vector3(0, 3.5f, 3)); + List border = new List(); + border.Add(v1); + border.Add(v2); + border.Add(v3); + List hole = new List(); + hole.Add(v4); + hole.Add(v5); + hole.Add(v6); + hole.Add(v7); + List all = new List(border); + all.AddRange(hole); + List> holes = new List>(); + holes.Add(hole); + List triangles = FaceTriangulator.Triangulate(border); + // Verify point in hole is not contained in any of the faces + foreach (Triangle t in triangles) + { + Console.Write(t); + Assert.False(Math3d.TriangleContainsPoint( + all[t.vertId0].loc, all[t.vertId1].loc, + all[t.vertId2].loc, new Vector3(0, 5, 3))); + } + } - [Test] - public void BadIntersectionTest() { - // Simple triangle with square inside. - Vertex v1 = new Vertex(0, new Vector3(0, -1, 1)); - Vertex v2 = new Vertex(1, new Vector3(-1, -1, 0)); - Vertex v3 = new Vertex(2, new Vector3(0, -1, -1)); - Vertex v4 = new Vertex(3, new Vector3(1, -1, 0)); - Vertex v5 = new Vertex(4, new Vector3(0.5f, -1, 0)); - Vertex v6 = new Vertex(5, new Vector3(0, -1, -0.5f)); - Vertex v7 = new Vertex(6, new Vector3(-0.5f, -1, 0)); - Vertex v8 = new Vertex(7, new Vector3(0, -1, 0.5f)); - List border = new List(); - border.Add(v1); - border.Add(v2); - border.Add(v3); - border.Add(v4); - List hole = new List(); - hole.Add(v5); - hole.Add(v6); - hole.Add(v7); - hole.Add(v8); - List all = new List(border); - all.AddRange(hole); - List> holes = new List>(); - holes.Add(hole); - List triangles = FaceTriangulator.Triangulate(border); - // Verify point in hole is not contained in any of the faces - foreach (Triangle t in triangles) { - Console.Write(t); - Assert.False(Math3d.TriangleContainsPoint( - all[t.vertId0].loc, all[t.vertId1].loc, - all[t.vertId2].loc, new Vector3(0, -1, 0))); - } - } + [Test] + public void BadIntersectionTest() + { + // Simple triangle with square inside. + Vertex v1 = new Vertex(0, new Vector3(0, -1, 1)); + Vertex v2 = new Vertex(1, new Vector3(-1, -1, 0)); + Vertex v3 = new Vertex(2, new Vector3(0, -1, -1)); + Vertex v4 = new Vertex(3, new Vector3(1, -1, 0)); + Vertex v5 = new Vertex(4, new Vector3(0.5f, -1, 0)); + Vertex v6 = new Vertex(5, new Vector3(0, -1, -0.5f)); + Vertex v7 = new Vertex(6, new Vector3(-0.5f, -1, 0)); + Vertex v8 = new Vertex(7, new Vector3(0, -1, 0.5f)); + List border = new List(); + border.Add(v1); + border.Add(v2); + border.Add(v3); + border.Add(v4); + List hole = new List(); + hole.Add(v5); + hole.Add(v6); + hole.Add(v7); + hole.Add(v8); + List all = new List(border); + all.AddRange(hole); + List> holes = new List>(); + holes.Add(hole); + List triangles = FaceTriangulator.Triangulate(border); + // Verify point in hole is not contained in any of the faces + foreach (Triangle t in triangles) + { + Console.Write(t); + Assert.False(Math3d.TriangleContainsPoint( + all[t.vertId0].loc, all[t.vertId1].loc, + all[t.vertId2].loc, new Vector3(0, -1, 0))); + } + } - [Test] - public void HoleWithOcclusionTest() { - // Outside is square with occluding indentation. - Vertex v1 = new Vertex(0, new Vector3(0, 0, 0)); - Vertex v2 = new Vertex(1, new Vector3(0, 0, 5)); - Vertex v3 = new Vertex(2, new Vector3(0, 8, 8)); - Vertex v4 = new Vertex(3, new Vector3(0, 0, 10)); - Vertex v5 = new Vertex(4, new Vector3(0, 10, 10)); - Vertex v6 = new Vertex(5, new Vector3(0, 10, 0)); - List border = new List(); - border.Add(v1); - border.Add(v2); - border.Add(v3); - border.Add(v4); - border.Add(v5); - border.Add(v6); + [Test] + public void HoleWithOcclusionTest() + { + // Outside is square with occluding indentation. + Vertex v1 = new Vertex(0, new Vector3(0, 0, 0)); + Vertex v2 = new Vertex(1, new Vector3(0, 0, 5)); + Vertex v3 = new Vertex(2, new Vector3(0, 8, 8)); + Vertex v4 = new Vertex(3, new Vector3(0, 0, 10)); + Vertex v5 = new Vertex(4, new Vector3(0, 10, 10)); + Vertex v6 = new Vertex(5, new Vector3(0, 10, 0)); + List border = new List(); + border.Add(v1); + border.Add(v2); + border.Add(v3); + border.Add(v4); + border.Add(v5); + border.Add(v6); - // Hole is simple triangle. - Vertex v7 = new Vertex(6, new Vector3(0, 4, 4)); - Vertex v8 = new Vertex(7, new Vector3(0, 3.5f, 3)); - Vertex v9 = new Vertex(8, new Vector3(0, 5, 3)); - List hole = new List(); - hole.Add(v7); - hole.Add(v8); - hole.Add(v9); - List all = new List(border); - all.AddRange(hole); - List> holes = new List>(); - holes.Add(hole); + // Hole is simple triangle. + Vertex v7 = new Vertex(6, new Vector3(0, 4, 4)); + Vertex v8 = new Vertex(7, new Vector3(0, 3.5f, 3)); + Vertex v9 = new Vertex(8, new Vector3(0, 5, 3)); + List hole = new List(); + hole.Add(v7); + hole.Add(v8); + hole.Add(v9); + List all = new List(border); + all.AddRange(hole); + List> holes = new List>(); + holes.Add(hole); - // If no exception is thrown triangulation was successful. - List triangles = FaceTriangulator.Triangulate(border); + // If no exception is thrown triangulation was successful. + List triangles = FaceTriangulator.Triangulate(border); - // Verify point in hole is not contained in any of the faces - foreach (Triangle t in triangles) { - Console.Write(t); - Assert.False(Math3d.TriangleContainsPoint( - all[t.vertId0].loc, all[t.vertId1].loc, - all[t.vertId2].loc, new Vector3(0,4,3.5f))); - } - } + // Verify point in hole is not contained in any of the faces + foreach (Triangle t in triangles) + { + Console.Write(t); + Assert.False(Math3d.TriangleContainsPoint( + all[t.vertId0].loc, all[t.vertId1].loc, + all[t.vertId2].loc, new Vector3(0, 4, 3.5f))); + } + } - [Test] - public void MultipleHoleTest() { - // Border is triangle. - Vertex v1 = new Vertex(0, new Vector3(0, 0, 0)); - Vertex v2 = new Vertex(1, new Vector3(0, 6, 8)); - Vertex v3 = new Vertex(2, new Vector3(0, 15, 0)); - List border = new List(); - border.Add(v1); - border.Add(v2); - border.Add(v3); - List all = new List(border); + [Test] + public void MultipleHoleTest() + { + // Border is triangle. + Vertex v1 = new Vertex(0, new Vector3(0, 0, 0)); + Vertex v2 = new Vertex(1, new Vector3(0, 6, 8)); + Vertex v3 = new Vertex(2, new Vector3(0, 15, 0)); + List border = new List(); + border.Add(v1); + border.Add(v2); + border.Add(v3); + List all = new List(border); - // Smaller hole is also a triangle. - Vertex v4 = new Vertex(3, new Vector3(0, 5, 3.75f)); - Vertex v5 = new Vertex(4, new Vector3(0, 4.5f, 3.5f)); - Vertex v6 = new Vertex(5, new Vector3(0, 5, 3)); - List hole = new List(); - hole.Add(v4); - hole.Add(v5); - hole.Add(v6); - all.AddRange(hole); - List> holes = new List>(); - holes.Add(hole); + // Smaller hole is also a triangle. + Vertex v4 = new Vertex(3, new Vector3(0, 5, 3.75f)); + Vertex v5 = new Vertex(4, new Vector3(0, 4.5f, 3.5f)); + Vertex v6 = new Vertex(5, new Vector3(0, 5, 3)); + List hole = new List(); + hole.Add(v4); + hole.Add(v5); + hole.Add(v6); + all.AddRange(hole); + List> holes = new List>(); + holes.Add(hole); - // Larger hole is encompassing smaller one, preventing visibility. - Vertex v7 = new Vertex(6, new Vector3(0, 7, 2)); - Vertex v8 = new Vertex(7, new Vector3(0, 5, 5)); - Vertex v9 = new Vertex(8, new Vector3(0, 3.25f, 3)); - Vertex v10 = new Vertex(9, new Vector3(0, 6.75f, 2)); - Vertex v11 = new Vertex(10, new Vector3(0, 3.75f, 3.25f)); - Vertex v12 = new Vertex(11, new Vector3(0, 5, 4.5f)); - hole = new List(); - hole.Add(v7); - hole.Add(v8); - hole.Add(v9); - hole.Add(v10); - hole.Add(v11); - hole.Add(v12); - all.AddRange(hole); - holes.Add(hole); + // Larger hole is encompassing smaller one, preventing visibility. + Vertex v7 = new Vertex(6, new Vector3(0, 7, 2)); + Vertex v8 = new Vertex(7, new Vector3(0, 5, 5)); + Vertex v9 = new Vertex(8, new Vector3(0, 3.25f, 3)); + Vertex v10 = new Vertex(9, new Vector3(0, 6.75f, 2)); + Vertex v11 = new Vertex(10, new Vector3(0, 3.75f, 3.25f)); + Vertex v12 = new Vertex(11, new Vector3(0, 5, 4.5f)); + hole = new List(); + hole.Add(v7); + hole.Add(v8); + hole.Add(v9); + hole.Add(v10); + hole.Add(v11); + hole.Add(v12); + all.AddRange(hole); + holes.Add(hole); - // If no exception is thrown triangulation was successful. - List triangles = FaceTriangulator.Triangulate(border); + // If no exception is thrown triangulation was successful. + List triangles = FaceTriangulator.Triangulate(border); - // Verify point in hole is not contained in any of the faces - foreach (Triangle t in triangles) { - Console.Write(t); - Assert.False(Math3d.TriangleContainsPoint( - all[t.vertId0].loc, all[t.vertId1].loc, - all[t.vertId2].loc, new Vector3(0,4.875f,3.25f))); - Assert.False(Math3d.TriangleContainsPoint( - all[t.vertId0].loc, all[t.vertId1].loc, - all[t.vertId2].loc, new Vector3(0,4,3))); - } + // Verify point in hole is not contained in any of the faces + foreach (Triangle t in triangles) + { + Console.Write(t); + Assert.False(Math3d.TriangleContainsPoint( + all[t.vertId0].loc, all[t.vertId1].loc, + all[t.vertId2].loc, new Vector3(0, 4.875f, 3.25f))); + Assert.False(Math3d.TriangleContainsPoint( + all[t.vertId0].loc, all[t.vertId1].loc, + all[t.vertId2].loc, new Vector3(0, 4, 3))); + } + } } - } } diff --git a/Assets/Editor/tests/model/render/MeshFixerTest.cs b/Assets/Editor/tests/model/render/MeshFixerTest.cs index 526dbdb6..26c6f04c 100644 --- a/Assets/Editor/tests/model/render/MeshFixerTest.cs +++ b/Assets/Editor/tests/model/render/MeshFixerTest.cs @@ -20,163 +20,176 @@ using UnityEngine; using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.model.core { - public class MeshFixerTest { - - [Test] - public void TestJoinCorners() { - MMesh mesh = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, /* materialId */ 1); - - // Join a coner on the box with one diagonal across the face. - List moves = new List(); - moves.Add(new Vertex(0, mesh.VertexPositionInMeshCoords(3))); - - MeshFixer.MoveVerticesAndMutateMeshAndFix(mesh.Clone(), mesh, moves, /* forPreview */ false); - - // Should have removed one vertex and added one face. - NUnit.Framework.Assert.AreEqual(7, mesh.vertexCount); - NUnit.Framework.Assert.AreEqual(7, mesh.faceCount); - - NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); - } - - [Test] - public void TestJoinEdges() { - MMesh mesh = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, /* materialId */ 1); - - // Join an edge on the box with another across the face. - List moves = new List(); - moves.Add(new Vertex(1, mesh.VertexPositionInMeshCoords(3))); - moves.Add(new Vertex(0, mesh.VertexPositionInMeshCoords(2))); - - MeshFixer.MoveVerticesAndMutateMeshAndFix(mesh.Clone(), mesh, moves, /* forPreview */ false); - - // Should have removed two vertices and one face. - NUnit.Framework.Assert.AreEqual(6, mesh.vertexCount); - NUnit.Framework.Assert.AreEqual(5, mesh.faceCount); - - NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); - } - - [Test] - public void TestJoinFaces() { - MMesh mesh = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, /* materialId */ 1); - - // Join the top face with the bottom face. - List moves = new List(); - moves.Add(new Vertex(0, mesh.VertexPositionInMeshCoords(2))); - moves.Add(new Vertex(1, mesh.VertexPositionInMeshCoords(3))); - moves.Add(new Vertex(5, mesh.VertexPositionInMeshCoords(7))); - moves.Add(new Vertex(4, mesh.VertexPositionInMeshCoords(6))); - - MeshFixer.MoveVerticesAndMutateMeshAndFix(mesh.Clone(), mesh, moves, /* forPreview */ false); - - // Should have removed four vertices and four faces. - NUnit.Framework.Assert.AreEqual(4, mesh.vertexCount); - NUnit.Framework.Assert.AreEqual(2, mesh.faceCount); - - NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); - } - - [Test] - public void TestJoinDuplicateVertices() { - int vertToDelete = 0; - - MMesh mesh = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, /* materialId */ 1); - - // Should start with 8 verts. - NUnit.Framework.Assert.AreEqual(8, mesh.vertexCount); - - // Move vertex 0 to where vertex 3 is - Vector3 loc = mesh.VertexPositionInMeshCoords(3); - Vertex updated = new Vertex(vertToDelete, loc); - MMesh.GeometryOperation operation = mesh.StartOperation(); - operation.ModifyVertex(updated); - operation.Commit(); - - HashSet moves = new HashSet() { vertToDelete }; - - MeshFixer.JoinDuplicateVertices(mesh, moves); - - // Should be down to 7 verts - NUnit.Framework.Assert.AreEqual(7, mesh.vertexCount); - - // Make sure vert 0 is not anywhere in the mesh. - NUnit.Framework.Assert.IsFalse(mesh.HasVertex(vertToDelete)); - - foreach (Face face in mesh.GetFaces()) { - NUnit.Framework.Assert.IsFalse(face.vertexIds.Contains(vertToDelete)); - } - } - - [Test] - public void TestRemoveZeroLengthSegments() { - CheckRemoveZeroLengthSegments(new List { 1, 2, 3, 4 }, new List { 1, 2, 3, 4 }); - CheckRemoveZeroLengthSegments(new List { 1, 1, 3, 4 }, new List { 1, 3, 4 }); - CheckRemoveZeroLengthSegments(new List { 1, 1, 1, 4 }, new List { 1, 4 }); - CheckRemoveZeroLengthSegments(new List { 4, 2, 3, 4 }, new List { 4, 2, 3 }); - CheckRemoveZeroLengthSegments(new List { 1, 2, 2, 4 }, new List { 1, 2, 4 }); - CheckRemoveZeroLengthSegments(new List { 1, 2, 2, 4, 5, 6, 6, 6, 9, 1 }, new List { 1, 2, 4, 5, 6, 9 }); - } - - private void CheckRemoveZeroLengthSegments(List before, List expected) { - Face face = new Face(1, before.AsReadOnly(), Vector3.zero, new FaceProperties()); - Dictionary faces = new Dictionary(); - faces[1] = face; - MMesh mesh = new MMesh(2, Vector3.zero, Quaternion.identity, new Dictionary(), faces); - HashSet candidateFaces = new HashSet(); - candidateFaces.UnionWith(mesh.GetFaceIds()); - MeshFixer.RemoveZeroLengthSegments(mesh, candidateFaces); - - NUnit.Framework.CollectionAssert.AreEqual( - new List(mesh.GetFace(1).vertexIds), - expected); - } - - [Test] - public void TestRemoveZeroAreaSegments() { - CheckRemoveZeroAreaSegments(new List { 1, 2, 3, 4 }, new List { 1, 2, 3, 4 }); - CheckRemoveZeroAreaSegments(new List { 1, 2, 3, 2, 5, 6, 5 }, new List { 1, 2, 5 }); - CheckRemoveZeroAreaSegments(new List { 1, 2, 3, 1, 7 }, new List { 2, 3, 1 }); - CheckRemoveZeroAreaSegments(new List { 1, 2, 3, 1, 2, 7 }, new List { 1, 2, 3, 1, 2, 7 }); - CheckRemoveZeroAreaSegments(new List { 1, 2, 3, 2 }, new List { 1, 2 }); - } - - private void CheckRemoveZeroAreaSegments(List before, List expected) { - Face face = new Face(1, before.AsReadOnly(), Vector3.zero, new FaceProperties()); - Dictionary faces = new Dictionary(); - faces[1] = face; - MMesh mesh = new MMesh(2, Vector3.zero, Quaternion.identity, new Dictionary(), faces); - mesh.RecalcReverseTable(); - HashSet allFaces = new HashSet(); - allFaces.UnionWith(mesh.GetFaceIds()); - - MeshFixer.RemoveZeroAreaSegments(mesh, allFaces); - - NUnit.Framework.CollectionAssert.AreEqual( - new List(mesh.GetFace(1).vertexIds), - expected); - } - - [Test] - public void TestRemoveInvalidFaces() { - CheckRemoveInvalidFaces(new List { 1, 2, 3, 4 }, false); - CheckRemoveInvalidFaces(new List { 1, 2, 3, 4, 5 }, false); - CheckRemoveInvalidFaces(new List { 1, 2 }, true); - CheckRemoveInvalidFaces(new List { 1 }, true); - CheckRemoveInvalidFaces(new List { }, true); - } - - private void CheckRemoveInvalidFaces(List before, bool shouldRemove) { - Face face = new Face(1, before.AsReadOnly(), Vector3.zero, new FaceProperties()); - Dictionary faces = new Dictionary(); - faces[1] = face; - MMesh mesh = new MMesh(2, Vector3.zero, Quaternion.identity, new Dictionary(), faces); - HashSet candidateFaces = new HashSet(); - candidateFaces.UnionWith(mesh.GetFaceIds()); - MeshFixer.RemoveInvalidFacesAndHoles(mesh, candidateFaces); - - NUnit.Framework.Assert.AreEqual(!mesh.HasFace(1), shouldRemove); +namespace com.google.apps.peltzer.client.model.core +{ + public class MeshFixerTest + { + + [Test] + public void TestJoinCorners() + { + MMesh mesh = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, /* materialId */ 1); + + // Join a coner on the box with one diagonal across the face. + List moves = new List(); + moves.Add(new Vertex(0, mesh.VertexPositionInMeshCoords(3))); + + MeshFixer.MoveVerticesAndMutateMeshAndFix(mesh.Clone(), mesh, moves, /* forPreview */ false); + + // Should have removed one vertex and added one face. + NUnit.Framework.Assert.AreEqual(7, mesh.vertexCount); + NUnit.Framework.Assert.AreEqual(7, mesh.faceCount); + + NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); + } + + [Test] + public void TestJoinEdges() + { + MMesh mesh = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, /* materialId */ 1); + + // Join an edge on the box with another across the face. + List moves = new List(); + moves.Add(new Vertex(1, mesh.VertexPositionInMeshCoords(3))); + moves.Add(new Vertex(0, mesh.VertexPositionInMeshCoords(2))); + + MeshFixer.MoveVerticesAndMutateMeshAndFix(mesh.Clone(), mesh, moves, /* forPreview */ false); + + // Should have removed two vertices and one face. + NUnit.Framework.Assert.AreEqual(6, mesh.vertexCount); + NUnit.Framework.Assert.AreEqual(5, mesh.faceCount); + + NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); + } + + [Test] + public void TestJoinFaces() + { + MMesh mesh = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, /* materialId */ 1); + + // Join the top face with the bottom face. + List moves = new List(); + moves.Add(new Vertex(0, mesh.VertexPositionInMeshCoords(2))); + moves.Add(new Vertex(1, mesh.VertexPositionInMeshCoords(3))); + moves.Add(new Vertex(5, mesh.VertexPositionInMeshCoords(7))); + moves.Add(new Vertex(4, mesh.VertexPositionInMeshCoords(6))); + + MeshFixer.MoveVerticesAndMutateMeshAndFix(mesh.Clone(), mesh, moves, /* forPreview */ false); + + // Should have removed four vertices and four faces. + NUnit.Framework.Assert.AreEqual(4, mesh.vertexCount); + NUnit.Framework.Assert.AreEqual(2, mesh.faceCount); + + NUnit.Framework.Assert.IsTrue(TopologyUtil.HasValidTopology(mesh, true)); + } + + [Test] + public void TestJoinDuplicateVertices() + { + int vertToDelete = 0; + + MMesh mesh = Primitives.AxisAlignedBox(1, Vector3.zero, Vector3.one, /* materialId */ 1); + + // Should start with 8 verts. + NUnit.Framework.Assert.AreEqual(8, mesh.vertexCount); + + // Move vertex 0 to where vertex 3 is + Vector3 loc = mesh.VertexPositionInMeshCoords(3); + Vertex updated = new Vertex(vertToDelete, loc); + MMesh.GeometryOperation operation = mesh.StartOperation(); + operation.ModifyVertex(updated); + operation.Commit(); + + HashSet moves = new HashSet() { vertToDelete }; + + MeshFixer.JoinDuplicateVertices(mesh, moves); + + // Should be down to 7 verts + NUnit.Framework.Assert.AreEqual(7, mesh.vertexCount); + + // Make sure vert 0 is not anywhere in the mesh. + NUnit.Framework.Assert.IsFalse(mesh.HasVertex(vertToDelete)); + + foreach (Face face in mesh.GetFaces()) + { + NUnit.Framework.Assert.IsFalse(face.vertexIds.Contains(vertToDelete)); + } + } + + [Test] + public void TestRemoveZeroLengthSegments() + { + CheckRemoveZeroLengthSegments(new List { 1, 2, 3, 4 }, new List { 1, 2, 3, 4 }); + CheckRemoveZeroLengthSegments(new List { 1, 1, 3, 4 }, new List { 1, 3, 4 }); + CheckRemoveZeroLengthSegments(new List { 1, 1, 1, 4 }, new List { 1, 4 }); + CheckRemoveZeroLengthSegments(new List { 4, 2, 3, 4 }, new List { 4, 2, 3 }); + CheckRemoveZeroLengthSegments(new List { 1, 2, 2, 4 }, new List { 1, 2, 4 }); + CheckRemoveZeroLengthSegments(new List { 1, 2, 2, 4, 5, 6, 6, 6, 9, 1 }, new List { 1, 2, 4, 5, 6, 9 }); + } + + private void CheckRemoveZeroLengthSegments(List before, List expected) + { + Face face = new Face(1, before.AsReadOnly(), Vector3.zero, new FaceProperties()); + Dictionary faces = new Dictionary(); + faces[1] = face; + MMesh mesh = new MMesh(2, Vector3.zero, Quaternion.identity, new Dictionary(), faces); + HashSet candidateFaces = new HashSet(); + candidateFaces.UnionWith(mesh.GetFaceIds()); + MeshFixer.RemoveZeroLengthSegments(mesh, candidateFaces); + + NUnit.Framework.CollectionAssert.AreEqual( + new List(mesh.GetFace(1).vertexIds), + expected); + } + + [Test] + public void TestRemoveZeroAreaSegments() + { + CheckRemoveZeroAreaSegments(new List { 1, 2, 3, 4 }, new List { 1, 2, 3, 4 }); + CheckRemoveZeroAreaSegments(new List { 1, 2, 3, 2, 5, 6, 5 }, new List { 1, 2, 5 }); + CheckRemoveZeroAreaSegments(new List { 1, 2, 3, 1, 7 }, new List { 2, 3, 1 }); + CheckRemoveZeroAreaSegments(new List { 1, 2, 3, 1, 2, 7 }, new List { 1, 2, 3, 1, 2, 7 }); + CheckRemoveZeroAreaSegments(new List { 1, 2, 3, 2 }, new List { 1, 2 }); + } + + private void CheckRemoveZeroAreaSegments(List before, List expected) + { + Face face = new Face(1, before.AsReadOnly(), Vector3.zero, new FaceProperties()); + Dictionary faces = new Dictionary(); + faces[1] = face; + MMesh mesh = new MMesh(2, Vector3.zero, Quaternion.identity, new Dictionary(), faces); + mesh.RecalcReverseTable(); + HashSet allFaces = new HashSet(); + allFaces.UnionWith(mesh.GetFaceIds()); + + MeshFixer.RemoveZeroAreaSegments(mesh, allFaces); + + NUnit.Framework.CollectionAssert.AreEqual( + new List(mesh.GetFace(1).vertexIds), + expected); + } + + [Test] + public void TestRemoveInvalidFaces() + { + CheckRemoveInvalidFaces(new List { 1, 2, 3, 4 }, false); + CheckRemoveInvalidFaces(new List { 1, 2, 3, 4, 5 }, false); + CheckRemoveInvalidFaces(new List { 1, 2 }, true); + CheckRemoveInvalidFaces(new List { 1 }, true); + CheckRemoveInvalidFaces(new List { }, true); + } + + private void CheckRemoveInvalidFaces(List before, bool shouldRemove) + { + Face face = new Face(1, before.AsReadOnly(), Vector3.zero, new FaceProperties()); + Dictionary faces = new Dictionary(); + faces[1] = face; + MMesh mesh = new MMesh(2, Vector3.zero, Quaternion.identity, new Dictionary(), faces); + HashSet candidateFaces = new HashSet(); + candidateFaces.UnionWith(mesh.GetFaceIds()); + MeshFixer.RemoveInvalidFacesAndHoles(mesh, candidateFaces); + + NUnit.Framework.Assert.AreEqual(!mesh.HasFace(1), shouldRemove); + } } - } } \ No newline at end of file diff --git a/Assets/Editor/tests/model/render/ReMesherTest.cs b/Assets/Editor/tests/model/render/ReMesherTest.cs index 81cc9058..ac5df02d 100644 --- a/Assets/Editor/tests/model/render/ReMesherTest.cs +++ b/Assets/Editor/tests/model/render/ReMesherTest.cs @@ -20,127 +20,153 @@ using com.google.apps.peltzer.client.model.core; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.render { - [TestFixture] - // Tests for ReMesher. - public class ReMesherTest { - [Test] - public void SanityTest() { - // Hard to test rendering is correct. But let's at least add a few items - // to make sure nothing blows up. - ReMesher remesher = new ReMesher(); - Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 5); - WorldSpace worldSpace = new WorldSpace(bounds); - Model model = new Model(bounds); - GameObject go = new GameObject(); - MeshRepresentationCache meshRepresentationCache = new MeshRepresentationCache(); - meshRepresentationCache.Setup(model, PeltzerMain.Instance.worldSpace); - remesher.AddMesh(Primitives.AxisAlignedBox( - 1, Vector3.zero, Vector3.one, /* Material id*/ 2)); - - remesher.AddMesh(Primitives.AxisAlignedBox( - 2, Vector3.one, Vector3.one, /* Material id*/ 3)); - - remesher.Render(model); - - remesher.RemoveMesh(1); - - remesher.Render(model); - - // Delete a mesh that isn't there. - try { - remesher.RemoveMesh(1); - Assert.True(false, "Expected exception."); - } catch (Exception) { - // Expected. - } - - // Add a duplicate mesh. - try { - remesher.AddMesh(Primitives.AxisAlignedBox( - 2, Vector3.one, Vector3.one, /* Material id*/ 3)); - Assert.True(false, "Expected exception."); - } catch (Exception) { - // Expected. - } - } - - [Test] - public void TestCoalescing() { - ReMesher remesher = new ReMesher(); - Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 5); - WorldSpace worldSpace = new WorldSpace(bounds); - Model model = new Model(bounds); - GameObject go = new GameObject(); - MeshRepresentationCache meshRepresentationCache = new MeshRepresentationCache(); - meshRepresentationCache.Setup(model, PeltzerMain.Instance.worldSpace); - - // Add 100 multicolor meshes: - for (int i = 0; i < 100; i++) { - remesher.AddMesh(multiColorMesh(i, i % 5, (i + 1) % 5, (i + 2) % 5)); - } - - // Make sure they are all there: - for (int i = 0; i < 100; i++) { - NUnit.Framework.Assert.IsTrue(remesher.HasMesh(i)); - // Has three colors, should be in exactly three meshInfos. - NUnit.Framework.Assert.AreEqual(3, remesher.MeshInMeshInfosCount(i)); - } - - // Now remove half of them: - for (int i = 0; i < 100; i+=2) { - remesher.RemoveMesh(i); - } - - // Make sure the ReMesher has what we think it should have: - for (int i = 0; i < 100; i++) { - if (i % 2 == 0) { - // Was deleted. Make sure it isn't anywhere: - NUnit.Framework.Assert.IsFalse(remesher.HasMesh(i)); - NUnit.Framework.Assert.AreEqual(0, remesher.MeshInMeshInfosCount(i)); - } else { - NUnit.Framework.Assert.IsTrue(remesher.HasMesh(i)); - NUnit.Framework.Assert.AreEqual(3, remesher.MeshInMeshInfosCount(i)); - } - } - - // Add some new ones back: - for (int i = 100; i < 200; i++) { - remesher.AddMesh(multiColorMesh(i, i % 5, (i + 1) % 5, (i + 2) % 5)); - } - - // Check everything again: - for (int i = 0; i < 200; i++) { - if (i < 100 && i % 2 == 0) { - // Was deleted. Make sure it isn't anywhere: - NUnit.Framework.Assert.IsFalse(remesher.HasMesh(i)); - NUnit.Framework.Assert.AreEqual(0, remesher.MeshInMeshInfosCount(i)); - } else { - NUnit.Framework.Assert.IsTrue(remesher.HasMesh(i)); - NUnit.Framework.Assert.AreEqual(3, remesher.MeshInMeshInfosCount(i)); +namespace com.google.apps.peltzer.client.model.render +{ + [TestFixture] + // Tests for ReMesher. + public class ReMesherTest + { + [Test] + public void SanityTest() + { + // Hard to test rendering is correct. But let's at least add a few items + // to make sure nothing blows up. + ReMesher remesher = new ReMesher(); + Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 5); + WorldSpace worldSpace = new WorldSpace(bounds); + Model model = new Model(bounds); + GameObject go = new GameObject(); + MeshRepresentationCache meshRepresentationCache = new MeshRepresentationCache(); + meshRepresentationCache.Setup(model, PeltzerMain.Instance.worldSpace); + remesher.AddMesh(Primitives.AxisAlignedBox( + 1, Vector3.zero, Vector3.one, /* Material id*/ 2)); + + remesher.AddMesh(Primitives.AxisAlignedBox( + 2, Vector3.one, Vector3.one, /* Material id*/ 3)); + + remesher.Render(model); + + remesher.RemoveMesh(1); + + remesher.Render(model); + + // Delete a mesh that isn't there. + try + { + remesher.RemoveMesh(1); + Assert.True(false, "Expected exception."); + } + catch (Exception) + { + // Expected. + } + + // Add a duplicate mesh. + try + { + remesher.AddMesh(Primitives.AxisAlignedBox( + 2, Vector3.one, Vector3.one, /* Material id*/ 3)); + Assert.True(false, "Expected exception."); + } + catch (Exception) + { + // Expected. + } } - } - // Now delete everything and make sure there aren't any MeshInfos: - for (int i = 0; i < 200; i++) { - if (i >= 100 || i % 2 != 0) { - remesher.RemoveMesh(i); + [Test] + public void TestCoalescing() + { + ReMesher remesher = new ReMesher(); + Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 5); + WorldSpace worldSpace = new WorldSpace(bounds); + Model model = new Model(bounds); + GameObject go = new GameObject(); + MeshRepresentationCache meshRepresentationCache = new MeshRepresentationCache(); + meshRepresentationCache.Setup(model, PeltzerMain.Instance.worldSpace); + + // Add 100 multicolor meshes: + for (int i = 0; i < 100; i++) + { + remesher.AddMesh(multiColorMesh(i, i % 5, (i + 1) % 5, (i + 2) % 5)); + } + + // Make sure they are all there: + for (int i = 0; i < 100; i++) + { + NUnit.Framework.Assert.IsTrue(remesher.HasMesh(i)); + // Has three colors, should be in exactly three meshInfos. + NUnit.Framework.Assert.AreEqual(3, remesher.MeshInMeshInfosCount(i)); + } + + // Now remove half of them: + for (int i = 0; i < 100; i += 2) + { + remesher.RemoveMesh(i); + } + + // Make sure the ReMesher has what we think it should have: + for (int i = 0; i < 100; i++) + { + if (i % 2 == 0) + { + // Was deleted. Make sure it isn't anywhere: + NUnit.Framework.Assert.IsFalse(remesher.HasMesh(i)); + NUnit.Framework.Assert.AreEqual(0, remesher.MeshInMeshInfosCount(i)); + } + else + { + NUnit.Framework.Assert.IsTrue(remesher.HasMesh(i)); + NUnit.Framework.Assert.AreEqual(3, remesher.MeshInMeshInfosCount(i)); + } + } + + // Add some new ones back: + for (int i = 100; i < 200; i++) + { + remesher.AddMesh(multiColorMesh(i, i % 5, (i + 1) % 5, (i + 2) % 5)); + } + + // Check everything again: + for (int i = 0; i < 200; i++) + { + if (i < 100 && i % 2 == 0) + { + // Was deleted. Make sure it isn't anywhere: + NUnit.Framework.Assert.IsFalse(remesher.HasMesh(i)); + NUnit.Framework.Assert.AreEqual(0, remesher.MeshInMeshInfosCount(i)); + } + else + { + NUnit.Framework.Assert.IsTrue(remesher.HasMesh(i)); + NUnit.Framework.Assert.AreEqual(3, remesher.MeshInMeshInfosCount(i)); + } + } + + // Now delete everything and make sure there aren't any MeshInfos: + for (int i = 0; i < 200; i++) + { + if (i >= 100 || i % 2 != 0) + { + remesher.RemoveMesh(i); + } + } + + for (int i = 0; i < 200; i++) + { + NUnit.Framework.Assert.IsFalse(remesher.HasMesh(i)); + NUnit.Framework.Assert.AreEqual(0, remesher.MeshInMeshInfosCount(i)); + } } - } - for (int i = 0; i < 200; i++) { - NUnit.Framework.Assert.IsFalse(remesher.HasMesh(i)); - NUnit.Framework.Assert.AreEqual(0, remesher.MeshInMeshInfosCount(i)); - } - } - - private MMesh multiColorMesh(int id, int color1, int color2, int color3) { - MMesh mesh = Primitives.AxisAlignedBox(id, Vector3.zero, Vector3.one, color1); + private MMesh multiColorMesh(int id, int color1, int color2, int color3) + { + MMesh mesh = Primitives.AxisAlignedBox(id, Vector3.zero, Vector3.one, color1); - mesh.GetFace(0).SetProperties(new FaceProperties(color2)); - mesh.GetFace(1).SetProperties(new FaceProperties(color3)); + mesh.GetFace(0).SetProperties(new FaceProperties(color2)); + mesh.GetFace(1).SetProperties(new FaceProperties(color3)); - return mesh; + return mesh; + } } - } } diff --git a/Assets/Editor/tests/model/util/ConcurrentQueueTest.cs b/Assets/Editor/tests/model/util/ConcurrentQueueTest.cs index 88591498..bdf373af 100644 --- a/Assets/Editor/tests/model/util/ConcurrentQueueTest.cs +++ b/Assets/Editor/tests/model/util/ConcurrentQueueTest.cs @@ -18,106 +18,123 @@ using NUnit.Framework; using UnityEngine; -namespace com.google.apps.peltzer.client.model.util { - [TestFixture] - // Tests for ConcurrentQueue. - public class ConcurrentQueueTest { - private const string MAIN = "Main"; - private const string BACKGROUND = "Background"; - private Thread backgroundThread; - private bool running = true; - private ConcurrentQueue forBackground = new ConcurrentQueue(); - private ConcurrentQueue forMainThread = new ConcurrentQueue(); - - public void SetupThread() { - if (Thread.CurrentThread.Name == null || !Thread.CurrentThread.Name.Equals(MAIN)) { - Thread.CurrentThread.Name = MAIN; - } - backgroundThread = new Thread(BackgroundThread); - backgroundThread.Name = BACKGROUND; - backgroundThread.Start(); - } +namespace com.google.apps.peltzer.client.model.util +{ + [TestFixture] + // Tests for ConcurrentQueue. + public class ConcurrentQueueTest + { + private const string MAIN = "Main"; + private const string BACKGROUND = "Background"; + private Thread backgroundThread; + private bool running = true; + private ConcurrentQueue forBackground = new ConcurrentQueue(); + private ConcurrentQueue forMainThread = new ConcurrentQueue(); + + public void SetupThread() + { + if (Thread.CurrentThread.Name == null || !Thread.CurrentThread.Name.Equals(MAIN)) + { + Thread.CurrentThread.Name = MAIN; + } + backgroundThread = new Thread(BackgroundThread); + backgroundThread.Name = BACKGROUND; + backgroundThread.Start(); + } - public void StopThread() { - running = false; - if (backgroundThread != null) { - backgroundThread.Abort(); - } - } + public void StopThread() + { + running = false; + if (backgroundThread != null) + { + backgroundThread.Abort(); + } + } - [Test] - public void TestBasics() { - ConcurrentQueue queue = new ConcurrentQueue(); - queue.Enqueue("foo"); - queue.Enqueue("bar"); + [Test] + public void TestBasics() + { + ConcurrentQueue queue = new ConcurrentQueue(); + queue.Enqueue("foo"); + queue.Enqueue("bar"); - string fromQueue; - NUnit.Framework.Assert.True(queue.Dequeue(out fromQueue)); - NUnit.Framework.Assert.AreEqual("foo", fromQueue); + string fromQueue; + NUnit.Framework.Assert.True(queue.Dequeue(out fromQueue)); + NUnit.Framework.Assert.AreEqual("foo", fromQueue); - NUnit.Framework.Assert.True(queue.WaitAndDequeue(10, out fromQueue)); - NUnit.Framework.Assert.AreEqual("bar", fromQueue); + NUnit.Framework.Assert.True(queue.WaitAndDequeue(10, out fromQueue)); + NUnit.Framework.Assert.AreEqual("bar", fromQueue); - // Queue should now be empty. - NUnit.Framework.Assert.False(queue.Dequeue(out fromQueue)); + // Queue should now be empty. + NUnit.Framework.Assert.False(queue.Dequeue(out fromQueue)); - float start = Time.realtimeSinceStartup; - NUnit.Framework.Assert.False(queue.WaitAndDequeue(20, out fromQueue)); - NUnit.Framework.Assert.GreaterOrEqual(Time.realtimeSinceStartup - start, 0.015f, - "Should have waited at least approximately 20ms."); - } + float start = Time.realtimeSinceStartup; + NUnit.Framework.Assert.False(queue.WaitAndDequeue(20, out fromQueue)); + NUnit.Framework.Assert.GreaterOrEqual(Time.realtimeSinceStartup - start, 0.015f, + "Should have waited at least approximately 20ms."); + } - [Test] - public void TestThreaded() { - try { - SetupThread(); - - forBackground.Enqueue("foo"); - forBackground.Enqueue("bar"); - Thread.Sleep(100); - forBackground.Enqueue("baz"); - - // Make sure work was done in right order on right thread. - WorkInfo info; - NUnit.Framework.Assert.True(forMainThread.WaitAndDequeue(/* wait time */ 1000, out info)); - NUnit.Framework.Assert.AreEqual("foo", info.workName); - NUnit.Framework.Assert.AreEqual(BACKGROUND, info.threadName); - - NUnit.Framework.Assert.True(forMainThread.WaitAndDequeue(/* wait time */ 1000, out info)); - NUnit.Framework.Assert.AreEqual("bar", info.workName); - NUnit.Framework.Assert.AreEqual(BACKGROUND, info.threadName); - - NUnit.Framework.Assert.True(forMainThread.WaitAndDequeue(/* wait time */ 1000, out info)); - NUnit.Framework.Assert.AreEqual("baz", info.workName); - NUnit.Framework.Assert.AreEqual(BACKGROUND, info.threadName); - NUnit.Framework.Assert.Greater(info.tryCount, 5, - "Should have had to try several times while waiting"); - } finally { - StopThread(); - } - } + [Test] + public void TestThreaded() + { + try + { + SetupThread(); + + forBackground.Enqueue("foo"); + forBackground.Enqueue("bar"); + Thread.Sleep(100); + forBackground.Enqueue("baz"); + + // Make sure work was done in right order on right thread. + WorkInfo info; + NUnit.Framework.Assert.True(forMainThread.WaitAndDequeue(/* wait time */ 1000, out info)); + NUnit.Framework.Assert.AreEqual("foo", info.workName); + NUnit.Framework.Assert.AreEqual(BACKGROUND, info.threadName); + + NUnit.Framework.Assert.True(forMainThread.WaitAndDequeue(/* wait time */ 1000, out info)); + NUnit.Framework.Assert.AreEqual("bar", info.workName); + NUnit.Framework.Assert.AreEqual(BACKGROUND, info.threadName); + + NUnit.Framework.Assert.True(forMainThread.WaitAndDequeue(/* wait time */ 1000, out info)); + NUnit.Framework.Assert.AreEqual("baz", info.workName); + NUnit.Framework.Assert.AreEqual(BACKGROUND, info.threadName); + NUnit.Framework.Assert.Greater(info.tryCount, 5, + "Should have had to try several times while waiting"); + } + finally + { + StopThread(); + } + } - void BackgroundThread() { - int tries = 0; - while (running) { - string workName; - if (forBackground.WaitAndDequeue(/* wait time */ 10, out workName)) { - WorkInfo info = new WorkInfo(); - info.workName = workName; - info.tryCount = tries; - info.threadName = Thread.CurrentThread.Name; - tries = 0; - forMainThread.Enqueue(info); - } else { - tries++; + void BackgroundThread() + { + int tries = 0; + while (running) + { + string workName; + if (forBackground.WaitAndDequeue(/* wait time */ 10, out workName)) + { + WorkInfo info = new WorkInfo(); + info.workName = workName; + info.tryCount = tries; + info.threadName = Thread.CurrentThread.Name; + tries = 0; + forMainThread.Enqueue(info); + } + else + { + tries++; + } + } } - } - } - class WorkInfo { - public string workName; - public string threadName; - public int tryCount; + class WorkInfo + { + public string workName; + public string threadName; + public int tryCount; + } } - } } diff --git a/Assets/Editor/tests/model/util/DisjointSetTest.cs b/Assets/Editor/tests/model/util/DisjointSetTest.cs index c7daa891..d84d8acd 100644 --- a/Assets/Editor/tests/model/util/DisjointSetTest.cs +++ b/Assets/Editor/tests/model/util/DisjointSetTest.cs @@ -14,74 +14,78 @@ using NUnit.Framework; -namespace com.google.apps.peltzer.client.model.util { - [TestFixture] - // Tests for DisjointSet. - public class DisjointSetTest { - [Test] - public void TestAdd() { - DisjointSet disjointSet = new DisjointSet(); - Assert.False(disjointSet.Contains(123)); - Assert.False(disjointSet.Contains(0)); - Assert.False(disjointSet.Contains(42)); - disjointSet.Add(42); - Assert.True(disjointSet.Contains(42)); - disjointSet.Add(600673); - disjointSet.Add(600673); // redundant. - Assert.True(disjointSet.Contains(600673)); +namespace com.google.apps.peltzer.client.model.util +{ + [TestFixture] + // Tests for DisjointSet. + public class DisjointSetTest + { + [Test] + public void TestAdd() + { + DisjointSet disjointSet = new DisjointSet(); + Assert.False(disjointSet.Contains(123)); + Assert.False(disjointSet.Contains(0)); + Assert.False(disjointSet.Contains(42)); + disjointSet.Add(42); + Assert.True(disjointSet.Contains(42)); + disjointSet.Add(600673); + disjointSet.Add(600673); // redundant. + Assert.True(disjointSet.Contains(600673)); - // All elements should be in the same set as themselves, if they are in the structure. - Assert.True(disjointSet.AreInSameSet(42, 42)); - Assert.True(disjointSet.AreInSameSet(600673, 600673)); + // All elements should be in the same set as themselves, if they are in the structure. + Assert.True(disjointSet.AreInSameSet(42, 42)); + Assert.True(disjointSet.AreInSameSet(600673, 600673)); - // But not if the are not in the structure. - Assert.False(disjointSet.AreInSameSet(391393, 391393)); - } + // But not if the are not in the structure. + Assert.False(disjointSet.AreInSameSet(391393, 391393)); + } - [Test] - public void TestJoin() { - DisjointSet disjointSet = new DisjointSet(); - disjointSet.Add(100); - disjointSet.Add(200); - disjointSet.Add(300); - disjointSet.Add(400); + [Test] + public void TestJoin() + { + DisjointSet disjointSet = new DisjointSet(); + disjointSet.Add(100); + disjointSet.Add(200); + disjointSet.Add(300); + disjointSet.Add(400); - // Should be 4 disjoint sets. - Assert.False(disjointSet.AreInSameSet(100, 200)); - Assert.False(disjointSet.AreInSameSet(100, 300)); - Assert.False(disjointSet.AreInSameSet(100, 400)); - Assert.False(disjointSet.AreInSameSet(200, 300)); - Assert.False(disjointSet.AreInSameSet(200, 400)); - Assert.False(disjointSet.AreInSameSet(300, 400)); - Assert.False(disjointSet.AreInSameSet(100, 4193193)); // non-members also allowed. - Assert.False(disjointSet.AreInSameSet(-13192, 200)); // non-members also allowed. + // Should be 4 disjoint sets. + Assert.False(disjointSet.AreInSameSet(100, 200)); + Assert.False(disjointSet.AreInSameSet(100, 300)); + Assert.False(disjointSet.AreInSameSet(100, 400)); + Assert.False(disjointSet.AreInSameSet(200, 300)); + Assert.False(disjointSet.AreInSameSet(200, 400)); + Assert.False(disjointSet.AreInSameSet(300, 400)); + Assert.False(disjointSet.AreInSameSet(100, 4193193)); // non-members also allowed. + Assert.False(disjointSet.AreInSameSet(-13192, 200)); // non-members also allowed. - // Now join 100 with 200. - disjointSet.Join(100, 200); + // Now join 100 with 200. + disjointSet.Join(100, 200); - // Now one set should be { 100, 200 }, and 300 and 400 are in separate sets. - Assert.True(disjointSet.AreInSameSet(100, 200)); - Assert.False(disjointSet.AreInSameSet(100, 300)); - Assert.False(disjointSet.AreInSameSet(200, 300)); - Assert.False(disjointSet.AreInSameSet(300, 400)); + // Now one set should be { 100, 200 }, and 300 and 400 are in separate sets. + Assert.True(disjointSet.AreInSameSet(100, 200)); + Assert.False(disjointSet.AreInSameSet(100, 300)); + Assert.False(disjointSet.AreInSameSet(200, 300)); + Assert.False(disjointSet.AreInSameSet(300, 400)); - // Now join 300 with 400. - // The new situation is { 100, 200 } and { 300, 400 }. - disjointSet.Join(300, 400); - Assert.True(disjointSet.AreInSameSet(100, 200)); - Assert.True(disjointSet.AreInSameSet(400, 300)); - Assert.False(disjointSet.AreInSameSet(200, 300)); - Assert.False(disjointSet.AreInSameSet(100, 400)); + // Now join 300 with 400. + // The new situation is { 100, 200 } and { 300, 400 }. + disjointSet.Join(300, 400); + Assert.True(disjointSet.AreInSameSet(100, 200)); + Assert.True(disjointSet.AreInSameSet(400, 300)); + Assert.False(disjointSet.AreInSameSet(200, 300)); + Assert.False(disjointSet.AreInSameSet(100, 400)); - // Now finally join 200 with 300, bringing everyone together in one big happy set: - // { 100, 200, 300, 400 }. - disjointSet.Join(200, 300); - Assert.True(disjointSet.AreInSameSet(100, 200)); - Assert.True(disjointSet.AreInSameSet(100, 300)); - Assert.True(disjointSet.AreInSameSet(100, 400)); - Assert.True(disjointSet.AreInSameSet(200, 300)); - Assert.True(disjointSet.AreInSameSet(200, 400)); - Assert.True(disjointSet.AreInSameSet(300, 400)); + // Now finally join 200 with 300, bringing everyone together in one big happy set: + // { 100, 200, 300, 400 }. + disjointSet.Join(200, 300); + Assert.True(disjointSet.AreInSameSet(100, 200)); + Assert.True(disjointSet.AreInSameSet(100, 300)); + Assert.True(disjointSet.AreInSameSet(100, 400)); + Assert.True(disjointSet.AreInSameSet(200, 300)); + Assert.True(disjointSet.AreInSameSet(200, 400)); + Assert.True(disjointSet.AreInSameSet(300, 400)); + } } - } } \ No newline at end of file diff --git a/Assets/Editor/tests/model/util/OctreeTest.cs b/Assets/Editor/tests/model/util/OctreeTest.cs index b3a82120..ba31098a 100644 --- a/Assets/Editor/tests/model/util/OctreeTest.cs +++ b/Assets/Editor/tests/model/util/OctreeTest.cs @@ -17,211 +17,213 @@ using NUnit.Framework; using UnityEngine; -namespace com.google.apps.peltzer.client.model.util { +namespace com.google.apps.peltzer.client.model.util +{ + + [TestFixture] + // Tests for Octree + public class OctreeTest + { + + /*[Test] + public void TestDepthIsZeroForSmallTree() { + Octree tree = new Octree( + new Bounds(Vector3.zero, Vector3.one * 10)); + + // Initial tree should have 0 depth. + NUnit.Framework.Assert.AreEqual(0, Depth(tree), + "Tree should have 0 depth."); + + // Small tree should have 0 depth. + tree.Add("Foo", new Bounds(new Vector3(1, 2, 3), Vector3.one)); + tree.Add("Bar", new Bounds(new Vector3(2, 1, 3), Vector3.one)); + tree.Add("Baz", new Bounds(new Vector3(3, 2, 1), Vector3.one)); + + NUnit.Framework.Assert.AreEqual(0, Depth(tree), + "Tree should have 0 depth."); + } - [TestFixture] - // Tests for Octree - public class OctreeTest { + [Test] + public void TestSplitNode() { + Octree tree = new Octree( + new Bounds(Vector3.zero, Vector3.one * 10)); - /*[Test] - public void TestDepthIsZeroForSmallTree() { - Octree tree = new Octree( - new Bounds(Vector3.zero, Vector3.one * 10)); + // Initial tree should have 0 depth. + NUnit.Framework.Assert.AreEqual(0, Depth(tree), + "Tree should have 0 depth."); - // Initial tree should have 0 depth. - NUnit.Framework.Assert.AreEqual(0, Depth(tree), - "Tree should have 0 depth."); + // Add 15 objects. Tree should split once. + for (float f = 0f; f < 1.5f; f += 0.1f) { + tree.Add("Foo: " + f, new Bounds(new Vector3(f, 2, 3), Vector3.one)); + } - // Small tree should have 0 depth. - tree.Add("Foo", new Bounds(new Vector3(1, 2, 3), Vector3.one)); - tree.Add("Bar", new Bounds(new Vector3(2, 1, 3), Vector3.one)); - tree.Add("Baz", new Bounds(new Vector3(3, 2, 1), Vector3.one)); + NUnit.Framework.Assert.AreEqual(1, Depth(tree), + "Tree should have 1 depth."); + } - NUnit.Framework.Assert.AreEqual(0, Depth(tree), - "Tree should have 0 depth."); - } + [Test] + public void TestInfiniteRecursion() { + Octree tree = new Octree( + new Bounds(Vector3.zero, Vector3.one * 10)); - [Test] - public void TestSplitNode() { - Octree tree = new Octree( - new Bounds(Vector3.zero, Vector3.one * 10)); + // Placing more than 10 items of point size at the same location could + // cause infinite recursion. + for (int i = 0; i < 20; i++) { + tree.Add("Foo: " + i, + new Bounds(new Vector3(1.5f, 2.3f, 3.1f), Vector3.zero)); + } + } - // Initial tree should have 0 depth. - NUnit.Framework.Assert.AreEqual(0, Depth(tree), - "Tree should have 0 depth."); + [Test] + public void TestContains() { + Octree tree = new Octree( + new Bounds(Vector3.zero, Vector3.one * 20)); + + tree.Add("Foo", new Bounds(new Vector3(1, 1, 1), Vector3.one * 0.1f)); + tree.Add("Bar", new Bounds(new Vector3(2, 2, 2), Vector3.one * 0.1f)); + tree.Add("NotFound", + new Bounds(new Vector3(5, 5, 5), Vector3.one * 0.1f)); + + HashSet contains; + NUnit.Framework.Assert.True( + tree.ContainedBy(new Bounds(Vector3.zero, Vector3.one * 5.0f), + out contains)); + NUnit.Framework.Assert.AreEqual(2, contains.Count); + NUnit.Framework.Assert.Contains("Foo", contains); + NUnit.Framework.Assert.Contains("Bar", contains); + + // Now move one outside of bounds: + tree.Update("Bar", new Bounds(new Vector3(6, 6, 6), Vector3.one * 0.5f)); + NUnit.Framework.Assert.True( + tree.ContainedBy(new Bounds(Vector3.zero, Vector3.one * 3.0f), + out contains)); + NUnit.Framework.Assert.AreEqual(1, contains.Count); + NUnit.Framework.Assert.Contains("Foo", contains); + } - // Add 15 objects. Tree should split once. - for (float f = 0f; f < 1.5f; f += 0.1f) { - tree.Add("Foo: " + f, new Bounds(new Vector3(f, 2, 3), Vector3.one)); - } + [Test] + public void TestIntersects() { + Octree tree = new Octree( + new Bounds(Vector3.zero, Vector3.one * 20)); + + tree.Add("Foo", new Bounds(new Vector3(1, 1, 1), Vector3.one * 2f)); + tree.Add("Bar", new Bounds(new Vector3(2, 2, 2), Vector3.one * 2f)); + tree.Add("Baz", new Bounds(new Vector3(5, 5, 5), Vector3.one * 2f)); + + HashSet intersects; + NUnit.Framework.Assert.True(tree.IntersectedBy( + new Bounds(Vector3.zero, Vector3.one * 10.0f), out intersects)); + NUnit.Framework.Assert.AreEqual(3, intersects.Count); + NUnit.Framework.Assert.Contains("Foo", intersects); + NUnit.Framework.Assert.Contains("Bar", intersects); + NUnit.Framework.Assert.Contains("Baz", intersects); + + NUnit.Framework.Assert.True(tree.IntersectedBy( + new Bounds(Vector3.zero, Vector3.one * 4.0f), out intersects)); + NUnit.Framework.Assert.AreEqual(2, intersects.Count); + NUnit.Framework.Assert.Contains("Foo", intersects); + NUnit.Framework.Assert.Contains("Bar", intersects); + + // Now move one outside of bounds: + tree.Update("Baz", new Bounds(new Vector3(6, 6, 6), Vector3.one * 0.5f)); + NUnit.Framework.Assert.True(tree.IntersectedBy( + new Bounds(Vector3.zero, Vector3.one * 10.0f), out intersects)); + NUnit.Framework.Assert.AreEqual(2, intersects.Count); + NUnit.Framework.Assert.Contains("Foo", intersects); + NUnit.Framework.Assert.Contains("Bar", intersects); + } - NUnit.Framework.Assert.AreEqual(1, Depth(tree), - "Tree should have 1 depth."); - } + [Test] + public void TestOutsideOfRange() { + Octree tree = new Octree( + new Bounds(Vector3.zero, Vector3.one * 2)); + + try { + tree.Add("Foo", new Bounds( + new Vector3(10, 10, 10), Vector3.one * 0.1f)); + NUnit.Framework.Assert.True(false, "Expected exception"); + } catch (Exception) { + // Expected + } + } - [Test] - public void TestInfiniteRecursion() { - Octree tree = new Octree( - new Bounds(Vector3.zero, Vector3.one * 10)); - - // Placing more than 10 items of point size at the same location could - // cause infinite recursion. - for (int i = 0; i < 20; i++) { - tree.Add("Foo: " + i, - new Bounds(new Vector3(1.5f, 2.3f, 3.1f), Vector3.zero)); - } - } + [Test] + public void TestMoveNonExisting() { + Octree tree = new Octree( + new Bounds(Vector3.zero, Vector3.one * 2)); - [Test] - public void TestContains() { - Octree tree = new Octree( - new Bounds(Vector3.zero, Vector3.one * 20)); - - tree.Add("Foo", new Bounds(new Vector3(1, 1, 1), Vector3.one * 0.1f)); - tree.Add("Bar", new Bounds(new Vector3(2, 2, 2), Vector3.one * 0.1f)); - tree.Add("NotFound", - new Bounds(new Vector3(5, 5, 5), Vector3.one * 0.1f)); - - HashSet contains; - NUnit.Framework.Assert.True( - tree.ContainedBy(new Bounds(Vector3.zero, Vector3.one * 5.0f), - out contains)); - NUnit.Framework.Assert.AreEqual(2, contains.Count); - NUnit.Framework.Assert.Contains("Foo", contains); - NUnit.Framework.Assert.Contains("Bar", contains); - - // Now move one outside of bounds: - tree.Update("Bar", new Bounds(new Vector3(6, 6, 6), Vector3.one * 0.5f)); - NUnit.Framework.Assert.True( - tree.ContainedBy(new Bounds(Vector3.zero, Vector3.one * 3.0f), - out contains)); - NUnit.Framework.Assert.AreEqual(1, contains.Count); - NUnit.Framework.Assert.Contains("Foo", contains); - } + try { + tree.Update("Foo", new Bounds(Vector3.zero, Vector3.one * 0.1f)); + NUnit.Framework.Assert.True(false, "Expected exception"); + } catch (Exception) { + // Expected + } - [Test] - public void TestIntersects() { - Octree tree = new Octree( - new Bounds(Vector3.zero, Vector3.one * 20)); - - tree.Add("Foo", new Bounds(new Vector3(1, 1, 1), Vector3.one * 2f)); - tree.Add("Bar", new Bounds(new Vector3(2, 2, 2), Vector3.one * 2f)); - tree.Add("Baz", new Bounds(new Vector3(5, 5, 5), Vector3.one * 2f)); - - HashSet intersects; - NUnit.Framework.Assert.True(tree.IntersectedBy( - new Bounds(Vector3.zero, Vector3.one * 10.0f), out intersects)); - NUnit.Framework.Assert.AreEqual(3, intersects.Count); - NUnit.Framework.Assert.Contains("Foo", intersects); - NUnit.Framework.Assert.Contains("Bar", intersects); - NUnit.Framework.Assert.Contains("Baz", intersects); - - NUnit.Framework.Assert.True(tree.IntersectedBy( - new Bounds(Vector3.zero, Vector3.one * 4.0f), out intersects)); - NUnit.Framework.Assert.AreEqual(2, intersects.Count); - NUnit.Framework.Assert.Contains("Foo", intersects); - NUnit.Framework.Assert.Contains("Bar", intersects); - - // Now move one outside of bounds: - tree.Update("Baz", new Bounds(new Vector3(6, 6, 6), Vector3.one * 0.5f)); - NUnit.Framework.Assert.True(tree.IntersectedBy( - new Bounds(Vector3.zero, Vector3.one * 10.0f), out intersects)); - NUnit.Framework.Assert.AreEqual(2, intersects.Count); - NUnit.Framework.Assert.Contains("Foo", intersects); - NUnit.Framework.Assert.Contains("Bar", intersects); - } + // Now add and then remove something and try again + tree.Add("Foo", new Bounds(Vector3.zero, Vector3.one * 0.1f)); + tree.Remove("Foo"); + try { + tree.Update("Foo", new Bounds(Vector3.zero, Vector3.one * 0.1f)); + NUnit.Framework.Assert.True(false, "Expected exception"); + } catch (Exception) { + // Expected + } + } - [Test] - public void TestOutsideOfRange() { - Octree tree = new Octree( - new Bounds(Vector3.zero, Vector3.one * 2)); - - try { - tree.Add("Foo", new Bounds( - new Vector3(10, 10, 10), Vector3.one * 0.1f)); - NUnit.Framework.Assert.True(false, "Expected exception"); - } catch (Exception) { - // Expected - } - } + [Test] + public void TestDeleteNonExisting() { + Octree tree = new Octree( + new Bounds(Vector3.zero, Vector3.one * 2)); - [Test] - public void TestMoveNonExisting() { - Octree tree = new Octree( - new Bounds(Vector3.zero, Vector3.one * 2)); - - try { - tree.Update("Foo", new Bounds(Vector3.zero, Vector3.one * 0.1f)); - NUnit.Framework.Assert.True(false, "Expected exception"); - } catch (Exception) { - // Expected - } - - // Now add and then remove something and try again - tree.Add("Foo", new Bounds(Vector3.zero, Vector3.one * 0.1f)); - tree.Remove("Foo"); - try { - tree.Update("Foo", new Bounds(Vector3.zero, Vector3.one * 0.1f)); - NUnit.Framework.Assert.True(false, "Expected exception"); - } catch (Exception) { - // Expected - } - } + try { + tree.Remove("Foo"); + NUnit.Framework.Assert.True(false, "Expected exception"); + } catch (Exception) { + // Expected + } - [Test] - public void TestDeleteNonExisting() { - Octree tree = new Octree( - new Bounds(Vector3.zero, Vector3.one * 2)); - - try { - tree.Remove("Foo"); - NUnit.Framework.Assert.True(false, "Expected exception"); - } catch (Exception) { - // Expected - } - - // Now add and then remove something and try again - tree.Add("Foo", new Bounds(Vector3.zero, Vector3.one * 0.1f)); - tree.Remove("Foo"); - try { - tree.Remove("Foo"); - NUnit.Framework.Assert.True(false, "Expected exception"); - } catch (Exception) { - // Expected - } - } + // Now add and then remove something and try again + tree.Add("Foo", new Bounds(Vector3.zero, Vector3.one * 0.1f)); + tree.Remove("Foo"); + try { + tree.Remove("Foo"); + NUnit.Framework.Assert.True(false, "Expected exception"); + } catch (Exception) { + // Expected + } + } - [Test] - public void TestSubBounds() { - Bounds root = new Bounds(Vector3.zero, Vector3.one * 20); - AssertBounds(Octree.SubBounds(root, 0), new Bounds(new Vector3(5, 5, 5), new Vector3(10, 10, 10))); - AssertBounds(Octree.SubBounds(root, 7), new Bounds(new Vector3(-5, -5, -5), new Vector3(10, 10, 10))); - AssertBounds(Octree.SubBounds(root, 1), new Bounds(new Vector3(-5, 5, 5), new Vector3(10, 10, 10))); - } + [Test] + public void TestSubBounds() { + Bounds root = new Bounds(Vector3.zero, Vector3.one * 20); + AssertBounds(Octree.SubBounds(root, 0), new Bounds(new Vector3(5, 5, 5), new Vector3(10, 10, 10))); + AssertBounds(Octree.SubBounds(root, 7), new Bounds(new Vector3(-5, -5, -5), new Vector3(10, 10, 10))); + AssertBounds(Octree.SubBounds(root, 1), new Bounds(new Vector3(-5, 5, 5), new Vector3(10, 10, 10))); + } - private void AssertBounds(Bounds actual, Bounds expected) { - NUnit.Framework.Assert.IsTrue(Vector3.Distance(actual.center, expected.center) < 0.01f - && Vector3.Distance(actual.size, expected.size) < 0.01f, "Expected: " + expected + " was: " + actual); - } + private void AssertBounds(Bounds actual, Bounds expected) { + NUnit.Framework.Assert.IsTrue(Vector3.Distance(actual.center, expected.center) < 0.01f + && Vector3.Distance(actual.size, expected.size) < 0.01f, "Expected: " + expected + " was: " + actual); + } - private int Depth(Octree tree) { - Octree.OTNode root = tree.GetRootNode(); - return DepthOfNode(root); - } + private int Depth(Octree tree) { + Octree.OTNode root = tree.GetRootNode(); + return DepthOfNode(root); + } - private int DepthOfNode(Octree.OTNode node) { - Octree.OTNode[] children = node.GetChildNodes(); - if (children == null) { - return 0; - } else { - int max = 0; - foreach(Octree.OTNode child in children) { - if (child != null) { - max = Math.Max(max, 1 + DepthOfNode(child)); + private int DepthOfNode(Octree.OTNode node) { + Octree.OTNode[] children = node.GetChildNodes(); + if (children == null) { + return 0; + } else { + int max = 0; + foreach(Octree.OTNode child in children) { + if (child != null) { + max = Math.Max(max, 1 + DepthOfNode(child)); + } + } + return max; } - } - return max; - } - } */ - } + } */ + } } diff --git a/Assets/Editor/tests/serialization/PeltzerFileSerializationTest.cs b/Assets/Editor/tests/serialization/PeltzerFileSerializationTest.cs index 9e828808..c9f09dac 100644 --- a/Assets/Editor/tests/serialization/PeltzerFileSerializationTest.cs +++ b/Assets/Editor/tests/serialization/PeltzerFileSerializationTest.cs @@ -19,104 +19,114 @@ using System.Collections.Generic; using UnityEngine; -namespace com.google.apps.peltzer.client.serialization { - [TestFixture] - // Tests for serializting a PeltzerFile (including MMeshes and stuff). - public class PeltzerFileSerializationTest { - private const float EPSILON = 1e-4f; +namespace com.google.apps.peltzer.client.serialization +{ + [TestFixture] + // Tests for serializting a PeltzerFile (including MMeshes and stuff). + public class PeltzerFileSerializationTest + { + private const float EPSILON = 1e-4f; - [Test] - public void TestSerializePeltzerFile() { - // Let's create a test file with a few meshes. - Metadata metadata = new Metadata("Randall Peltzer", "Jun 8, 1984", "1.0"); - List materials = new List(); - materials.Add(new PeltzerMaterial(/* materialId */ 100, /* color */ 0x101010)); - materials.Add(new PeltzerMaterial(/* materialId */ 200, /* color */ 0x202020)); - materials.Add(new PeltzerMaterial(/* materialId */ 300, /* color */ 0x303030)); - List meshes = new List(); + [Test] + public void TestSerializePeltzerFile() + { + // Let's create a test file with a few meshes. + Metadata metadata = new Metadata("Randall Peltzer", "Jun 8, 1984", "1.0"); + List materials = new List(); + materials.Add(new PeltzerMaterial(/* materialId */ 100, /* color */ 0x101010)); + materials.Add(new PeltzerMaterial(/* materialId */ 200, /* color */ 0x202020)); + materials.Add(new PeltzerMaterial(/* materialId */ 300, /* color */ 0x303030)); + List meshes = new List(); - // Add three highly exciting primitives. - MMesh box = - Primitives.AxisAlignedBox(1000, new Vector3(1.0f, 2.0f, 3.0f), new Vector3(1.0f, 20.0f, 5.0f), 100); - MMesh cylinder = - Primitives.AxisAlignedCylinder(2000, new Vector3(1.0f, 2.0f, 3.0f), new Vector3(1.0f, 20.0f, 5.0f), - /* holeRadius */null, 200); - MMesh sphere = - Primitives.AxisAlignedIcosphere(3000, new Vector3(2000.0f, -4000.0f, 6000.0f), Vector3.one, 300); - meshes.Add(box); - meshes.Add(cylinder); - meshes.Add(sphere); + // Add three highly exciting primitives. + MMesh box = + Primitives.AxisAlignedBox(1000, new Vector3(1.0f, 2.0f, 3.0f), new Vector3(1.0f, 20.0f, 5.0f), 100); + MMesh cylinder = + Primitives.AxisAlignedCylinder(2000, new Vector3(1.0f, 2.0f, 3.0f), new Vector3(1.0f, 20.0f, 5.0f), + /* holeRadius */null, 200); + MMesh sphere = + Primitives.AxisAlignedIcosphere(3000, new Vector3(2000.0f, -4000.0f, 6000.0f), Vector3.one, 300); + meshes.Add(box); + meshes.Add(cylinder); + meshes.Add(sphere); - List allCommands = new List(); - List undoStack = new List(); - List redoStack = new List(); + List allCommands = new List(); + List undoStack = new List(); + List redoStack = new List(); - PeltzerFile savedFile = new PeltzerFile(metadata, /* zoomFactor */ 1.5f, materials, meshes); - int estimate = savedFile.GetSerializedSizeEstimate(); + PeltzerFile savedFile = new PeltzerFile(metadata, /* zoomFactor */ 1.5f, materials, meshes); + int estimate = savedFile.GetSerializedSizeEstimate(); - // Serialize to a byte buffer. - PolySerializer serializer = new PolySerializer(); - serializer.SetupForWriting(16); - savedFile.Serialize(serializer); - serializer.FinishWriting(); + // Serialize to a byte buffer. + PolySerializer serializer = new PolySerializer(); + serializer.SetupForWriting(16); + savedFile.Serialize(serializer); + serializer.FinishWriting(); - byte[] output = serializer.ToByteArray(); - Assert.IsTrue(output.Length <= estimate); + byte[] output = serializer.ToByteArray(); + Assert.IsTrue(output.Length <= estimate); - // Now let's read back the buffer and check that everything looks good. - serializer.SetupForReading(output, 0, output.Length); - PeltzerFile loadedFile = new PeltzerFile(serializer); + // Now let's read back the buffer and check that everything looks good. + serializer.SetupForReading(output, 0, output.Length); + PeltzerFile loadedFile = new PeltzerFile(serializer); - Assert.AreEqual("Randall Peltzer", loadedFile.metadata.creatorName); - Assert.AreEqual("Jun 8, 1984", loadedFile.metadata.creationDate); - Assert.AreEqual("1.0", loadedFile.metadata.version); - Assert.AreEqual(3, loadedFile.materials.Count); - Assert.AreEqual(100, loadedFile.materials[0].materialId); - Assert.AreEqual(0x101010, loadedFile.materials[0].color); - Assert.AreEqual(200, loadedFile.materials[1].materialId); - Assert.AreEqual(0x202020, loadedFile.materials[1].color); - Assert.AreEqual(300, loadedFile.materials[2].materialId); - Assert.AreEqual(0x303030, loadedFile.materials[2].color); - Assert.AreEqual(3, loadedFile.meshes.Count); - Dictionary meshDict = new Dictionary(); - foreach (MMesh mesh in loadedFile.meshes) { - meshDict[mesh.id] = mesh; - } - AssertMeshesEqual(box, meshDict[1000]); - AssertMeshesEqual(cylinder, meshDict[2000]); - AssertMeshesEqual(sphere, meshDict[3000]); - } + Assert.AreEqual("Randall Peltzer", loadedFile.metadata.creatorName); + Assert.AreEqual("Jun 8, 1984", loadedFile.metadata.creationDate); + Assert.AreEqual("1.0", loadedFile.metadata.version); + Assert.AreEqual(3, loadedFile.materials.Count); + Assert.AreEqual(100, loadedFile.materials[0].materialId); + Assert.AreEqual(0x101010, loadedFile.materials[0].color); + Assert.AreEqual(200, loadedFile.materials[1].materialId); + Assert.AreEqual(0x202020, loadedFile.materials[1].color); + Assert.AreEqual(300, loadedFile.materials[2].materialId); + Assert.AreEqual(0x303030, loadedFile.materials[2].color); + Assert.AreEqual(3, loadedFile.meshes.Count); + Dictionary meshDict = new Dictionary(); + foreach (MMesh mesh in loadedFile.meshes) + { + meshDict[mesh.id] = mesh; + } + AssertMeshesEqual(box, meshDict[1000]); + AssertMeshesEqual(cylinder, meshDict[2000]); + AssertMeshesEqual(sphere, meshDict[3000]); + } + + private static void AssertMeshesEqual(MMesh a, MMesh b) + { + Assert.AreEqual(a.id, b.id); + Assert.IsTrue((a.offset - b.offset).magnitude < EPSILON); + Assert.IsTrue((Quaternion.Angle(a.rotation, b.rotation) < EPSILON)); + Assert.AreEqual(a.groupId, b.groupId); + foreach (Vertex vertexInA in a.GetVertices()) + { + Assert.IsTrue(b.HasVertex(vertexInA.id)); + Vertex vertexInB = b.GetVertex(vertexInA.id); + Assert.IsTrue((vertexInA.loc - vertexInB.loc).magnitude < EPSILON); + } + foreach (Vertex vertexInB in b.GetVertices()) + { + Assert.IsTrue(a.HasVertex(vertexInB.id)); + } + foreach (Face faceInA in a.GetFaces()) + { + Face faceInB; + Assert.IsTrue(b.HasFace(faceInA.id)); + faceInB = b.GetFace(faceInA.id); + + Assert.AreEqual(faceInA.properties.materialId, faceInB.properties.materialId); + Assert.AreEqual(faceInA.vertexIds.Count, faceInB.vertexIds.Count); + for (int i = 0; i < faceInA.vertexIds.Count; i++) + { + Assert.AreEqual(faceInA.vertexIds[i], faceInB.vertexIds[i]); + } + + Assert.IsTrue((faceInA.normal - faceInB.normal).magnitude < EPSILON); - private static void AssertMeshesEqual(MMesh a, MMesh b) { - Assert.AreEqual(a.id, b.id); - Assert.IsTrue((a.offset - b.offset).magnitude < EPSILON); - Assert.IsTrue((Quaternion.Angle(a.rotation, b.rotation) < EPSILON)); - Assert.AreEqual(a.groupId, b.groupId); - foreach (Vertex vertexInA in a.GetVertices()) { - Assert.IsTrue(b.HasVertex(vertexInA.id)); - Vertex vertexInB = b.GetVertex(vertexInA.id); - Assert.IsTrue((vertexInA.loc - vertexInB.loc).magnitude < EPSILON); - } - foreach (Vertex vertexInB in b.GetVertices()) { - Assert.IsTrue(a.HasVertex(vertexInB.id)); - } - foreach (Face faceInA in a.GetFaces()) { - Face faceInB; - Assert.IsTrue(b.HasFace(faceInA.id)); - faceInB = b.GetFace(faceInA.id); - - Assert.AreEqual(faceInA.properties.materialId, faceInB.properties.materialId); - Assert.AreEqual(faceInA.vertexIds.Count, faceInB.vertexIds.Count); - for (int i = 0; i < faceInA.vertexIds.Count; i++) { - Assert.AreEqual(faceInA.vertexIds[i], faceInB.vertexIds[i]); + } + foreach (Face faceInB in b.GetFaces()) + { + Assert.IsTrue(a.HasFace(faceInB.id)); + } } - - Assert.IsTrue((faceInA.normal - faceInB.normal).magnitude < EPSILON); - - } - foreach (Face faceInB in b.GetFaces()) { - Assert.IsTrue(a.HasFace(faceInB.id)); - } } - } } \ No newline at end of file diff --git a/Assets/Editor/tests/serialization/PolySerializationUtilsTest.cs b/Assets/Editor/tests/serialization/PolySerializationUtilsTest.cs index 3b30ca62..8874209c 100644 --- a/Assets/Editor/tests/serialization/PolySerializationUtilsTest.cs +++ b/Assets/Editor/tests/serialization/PolySerializationUtilsTest.cs @@ -17,70 +17,74 @@ using System.Collections.Generic; using UnityEngine; -namespace com.google.apps.peltzer.client.serialization { - [TestFixture] - // Tests for PolySerializationUtilsTest. - public class PolySerializationUtilsTest { - [Test] - public void TestVectorsAndQuaternions() { - PolySerializer serializer = new PolySerializer(); - serializer.SetupForWriting(16); - serializer.StartWritingChunk(123); - PolySerializationUtils.WriteVector3(serializer, Vector3.forward); - PolySerializationUtils.WriteVector3(serializer, new Vector3(1000.0f, -0.1f, 1e8f)); - PolySerializationUtils.WriteQuaternion(serializer, Quaternion.identity); - PolySerializationUtils.WriteQuaternion(serializer, Quaternion.Euler(10.0f, -20.0f, 30.0f)); - serializer.FinishWritingChunk(123); - serializer.FinishWriting(); +namespace com.google.apps.peltzer.client.serialization +{ + [TestFixture] + // Tests for PolySerializationUtilsTest. + public class PolySerializationUtilsTest + { + [Test] + public void TestVectorsAndQuaternions() + { + PolySerializer serializer = new PolySerializer(); + serializer.SetupForWriting(16); + serializer.StartWritingChunk(123); + PolySerializationUtils.WriteVector3(serializer, Vector3.forward); + PolySerializationUtils.WriteVector3(serializer, new Vector3(1000.0f, -0.1f, 1e8f)); + PolySerializationUtils.WriteQuaternion(serializer, Quaternion.identity); + PolySerializationUtils.WriteQuaternion(serializer, Quaternion.Euler(10.0f, -20.0f, 30.0f)); + serializer.FinishWritingChunk(123); + serializer.FinishWriting(); - byte[] output = serializer.ToByteArray(); + byte[] output = serializer.ToByteArray(); - serializer.SetupForReading(output, 0, output.Length); - serializer.StartReadingChunk(123); - Assert.AreEqual(Vector3.forward, PolySerializationUtils.ReadVector3(serializer)); - Assert.AreEqual(new Vector3(1000.0f, -0.1f, 1e8f), PolySerializationUtils.ReadVector3(serializer)); - Assert.AreEqual(Quaternion.identity, PolySerializationUtils.ReadQuaternion(serializer)); - Assert.AreEqual(Quaternion.Euler(10.0f, -20.0f, 30.0f), PolySerializationUtils.ReadQuaternion(serializer)); - serializer.FinishReadingChunk(123); - } + serializer.SetupForReading(output, 0, output.Length); + serializer.StartReadingChunk(123); + Assert.AreEqual(Vector3.forward, PolySerializationUtils.ReadVector3(serializer)); + Assert.AreEqual(new Vector3(1000.0f, -0.1f, 1e8f), PolySerializationUtils.ReadVector3(serializer)); + Assert.AreEqual(Quaternion.identity, PolySerializationUtils.ReadQuaternion(serializer)); + Assert.AreEqual(Quaternion.Euler(10.0f, -20.0f, 30.0f), PolySerializationUtils.ReadQuaternion(serializer)); + serializer.FinishReadingChunk(123); + } - [Test] - public void TestLists() { - PolySerializer serializer = new PolySerializer(); - serializer.SetupForWriting(16); - serializer.StartWritingChunk(123); - PolySerializationUtils.WriteIntList(serializer, new int[] { 100, 200, 300, 400 }); - PolySerializationUtils.WriteVector3List(serializer, new Vector3[] { + [Test] + public void TestLists() + { + PolySerializer serializer = new PolySerializer(); + serializer.SetupForWriting(16); + serializer.StartWritingChunk(123); + PolySerializationUtils.WriteIntList(serializer, new int[] { 100, 200, 300, 400 }); + PolySerializationUtils.WriteVector3List(serializer, new Vector3[] { Vector3.up, Vector3.down, Vector3.up, Vector3.down, Vector3.left, Vector3.right, Vector3.left, Vector3.right }); // ..., A, B, start! :-) - serializer.FinishWritingChunk(123); - serializer.FinishWriting(); + serializer.FinishWritingChunk(123); + serializer.FinishWriting(); - byte[] output = serializer.ToByteArray(); + byte[] output = serializer.ToByteArray(); - serializer.SetupForReading(output, 0, output.Length); - serializer.StartReadingChunk(123); + serializer.SetupForReading(output, 0, output.Length); + serializer.StartReadingChunk(123); - List list = PolySerializationUtils.ReadIntList(serializer); - Assert.AreEqual(4, list.Count); - Assert.AreEqual(100, list[0]); - Assert.AreEqual(200, list[1]); - Assert.AreEqual(300, list[2]); - Assert.AreEqual(400, list[3]); + List list = PolySerializationUtils.ReadIntList(serializer); + Assert.AreEqual(4, list.Count); + Assert.AreEqual(100, list[0]); + Assert.AreEqual(200, list[1]); + Assert.AreEqual(300, list[2]); + Assert.AreEqual(400, list[3]); - List vectorList = PolySerializationUtils.ReadVector3List(serializer); - Assert.AreEqual(8, vectorList.Count); - Assert.AreEqual(Vector3.up, vectorList[0]); - Assert.AreEqual(Vector3.down, vectorList[1]); - Assert.AreEqual(Vector3.up, vectorList[2]); - Assert.AreEqual(Vector3.down, vectorList[3]); - Assert.AreEqual(Vector3.left, vectorList[4]); - Assert.AreEqual(Vector3.right, vectorList[5]); - Assert.AreEqual(Vector3.left, vectorList[6]); - Assert.AreEqual(Vector3.right, vectorList[7]); + List vectorList = PolySerializationUtils.ReadVector3List(serializer); + Assert.AreEqual(8, vectorList.Count); + Assert.AreEqual(Vector3.up, vectorList[0]); + Assert.AreEqual(Vector3.down, vectorList[1]); + Assert.AreEqual(Vector3.up, vectorList[2]); + Assert.AreEqual(Vector3.down, vectorList[3]); + Assert.AreEqual(Vector3.left, vectorList[4]); + Assert.AreEqual(Vector3.right, vectorList[5]); + Assert.AreEqual(Vector3.left, vectorList[6]); + Assert.AreEqual(Vector3.right, vectorList[7]); - serializer.FinishReadingChunk(123); + serializer.FinishReadingChunk(123); + } } - } } \ No newline at end of file diff --git a/Assets/Editor/tests/serialization/PolySerializerTest.cs b/Assets/Editor/tests/serialization/PolySerializerTest.cs index abbc0e60..ba133f6c 100644 --- a/Assets/Editor/tests/serialization/PolySerializerTest.cs +++ b/Assets/Editor/tests/serialization/PolySerializerTest.cs @@ -15,257 +15,274 @@ using NUnit.Framework; using System; -namespace com.google.apps.peltzer.client.serialization { - [TestFixture] - // Tests for PolySerializer. - public class PolySerializerTest { - [Test] - public void TestBasicTypes() { - PolySerializer serializer = new PolySerializer(); - serializer.SetupForWriting(16); - Assert.IsFalse(serializer.IsChunkInProgress); +namespace com.google.apps.peltzer.client.serialization +{ + [TestFixture] + // Tests for PolySerializer. + public class PolySerializerTest + { + [Test] + public void TestBasicTypes() + { + PolySerializer serializer = new PolySerializer(); + serializer.SetupForWriting(16); + Assert.IsFalse(serializer.IsChunkInProgress); - serializer.StartWritingChunk(0xDEAD); - Assert.IsTrue(serializer.IsChunkInProgress); - serializer.WriteInt(1234); - serializer.WriteInt(-5678); - serializer.WriteInt(0); - serializer.WriteInt(int.MinValue); - serializer.WriteInt(int.MaxValue); - serializer.WriteString(""); - serializer.WriteString("The quick brown fox jumps over the lazy dog."); - serializer.WriteString("色は匂へど散りぬるを我が世誰ぞ常ならん有為の奥山今日越えて浅き夢見じ酔ひもせず"); - serializer.WriteString(null); - serializer.WriteBool(true); - serializer.WriteBool(false); - serializer.WriteByte((byte)12); - serializer.WriteByte((byte)251); - serializer.FinishWritingChunk(0xDEAD); - Assert.IsFalse(serializer.IsChunkInProgress); + serializer.StartWritingChunk(0xDEAD); + Assert.IsTrue(serializer.IsChunkInProgress); + serializer.WriteInt(1234); + serializer.WriteInt(-5678); + serializer.WriteInt(0); + serializer.WriteInt(int.MinValue); + serializer.WriteInt(int.MaxValue); + serializer.WriteString(""); + serializer.WriteString("The quick brown fox jumps over the lazy dog."); + serializer.WriteString("色は匂へど散りぬるを我が世誰ぞ常ならん有為の奥山今日越えて浅き夢見じ酔ひもせず"); + serializer.WriteString(null); + serializer.WriteBool(true); + serializer.WriteBool(false); + serializer.WriteByte((byte)12); + serializer.WriteByte((byte)251); + serializer.FinishWritingChunk(0xDEAD); + Assert.IsFalse(serializer.IsChunkInProgress); - serializer.StartWritingChunk(0xBEEF); - Assert.IsTrue(serializer.IsChunkInProgress); - serializer.WriteFloat(0.0f); - serializer.WriteFloat(1.0f); - serializer.WriteFloat((float)Math.PI); - serializer.WriteFloat(-(float)Math.PI); - serializer.WriteFloat(1e8f); - serializer.WriteFloat(-1e8f); - serializer.WriteFloat(float.PositiveInfinity); - serializer.WriteFloat(float.NegativeInfinity); - serializer.WriteFloat(float.NaN); - serializer.WriteFloat(float.Epsilon); - serializer.FinishWritingChunk(0xBEEF); - Assert.IsFalse(serializer.IsChunkInProgress); + serializer.StartWritingChunk(0xBEEF); + Assert.IsTrue(serializer.IsChunkInProgress); + serializer.WriteFloat(0.0f); + serializer.WriteFloat(1.0f); + serializer.WriteFloat((float)Math.PI); + serializer.WriteFloat(-(float)Math.PI); + serializer.WriteFloat(1e8f); + serializer.WriteFloat(-1e8f); + serializer.WriteFloat(float.PositiveInfinity); + serializer.WriteFloat(float.NegativeInfinity); + serializer.WriteFloat(float.NaN); + serializer.WriteFloat(float.Epsilon); + serializer.FinishWritingChunk(0xBEEF); + Assert.IsFalse(serializer.IsChunkInProgress); - serializer.FinishWriting(); - byte[] output = serializer.ToByteArray(); + serializer.FinishWriting(); + byte[] output = serializer.ToByteArray(); - // Just to make things more interesting (and dangerous!), let's copy that into a larger buffer - // and use an offset. - byte[] largerBuffer = new byte[output.Length + 200]; - Buffer.BlockCopy(output, 0, largerBuffer, 17, output.Length); + // Just to make things more interesting (and dangerous!), let's copy that into a larger buffer + // and use an offset. + byte[] largerBuffer = new byte[output.Length + 200]; + Buffer.BlockCopy(output, 0, largerBuffer, 17, output.Length); - // Read back. - serializer.SetupForReading(largerBuffer, /* startOffset */ 17, /* length */ output.Length); - Assert.IsFalse(serializer.IsChunkInProgress); + // Read back. + serializer.SetupForReading(largerBuffer, /* startOffset */ 17, /* length */ output.Length); + Assert.IsFalse(serializer.IsChunkInProgress); - Assert.AreEqual(0xDEAD, serializer.GetNextChunkLabel()); - serializer.StartReadingChunk(0xDEAD); - Assert.IsTrue(serializer.IsChunkInProgress); - Assert.AreEqual(1234, serializer.ReadInt()); - Assert.AreEqual(-5678, serializer.ReadInt()); - Assert.AreEqual(0, serializer.ReadInt()); - Assert.AreEqual(int.MinValue, serializer.ReadInt()); - Assert.AreEqual(int.MaxValue, serializer.ReadInt()); - Assert.AreEqual("", serializer.ReadString()); - Assert.AreEqual("The quick brown fox jumps over the lazy dog.", serializer.ReadString()); - Assert.AreEqual("色は匂へど散りぬるを我が世誰ぞ常ならん有為の奥山今日越えて浅き夢見じ酔ひもせず", - serializer.ReadString()); - Assert.AreEqual(null, serializer.ReadString()); - Assert.AreEqual(true, serializer.ReadBool()); - Assert.AreEqual(false, serializer.ReadBool()); - Assert.AreEqual((byte)12, serializer.ReadByte()); - Assert.AreEqual((byte)251, serializer.ReadByte()); - serializer.FinishReadingChunk(0xDEAD); - Assert.IsFalse(serializer.IsChunkInProgress); + Assert.AreEqual(0xDEAD, serializer.GetNextChunkLabel()); + serializer.StartReadingChunk(0xDEAD); + Assert.IsTrue(serializer.IsChunkInProgress); + Assert.AreEqual(1234, serializer.ReadInt()); + Assert.AreEqual(-5678, serializer.ReadInt()); + Assert.AreEqual(0, serializer.ReadInt()); + Assert.AreEqual(int.MinValue, serializer.ReadInt()); + Assert.AreEqual(int.MaxValue, serializer.ReadInt()); + Assert.AreEqual("", serializer.ReadString()); + Assert.AreEqual("The quick brown fox jumps over the lazy dog.", serializer.ReadString()); + Assert.AreEqual("色は匂へど散りぬるを我が世誰ぞ常ならん有為の奥山今日越えて浅き夢見じ酔ひもせず", + serializer.ReadString()); + Assert.AreEqual(null, serializer.ReadString()); + Assert.AreEqual(true, serializer.ReadBool()); + Assert.AreEqual(false, serializer.ReadBool()); + Assert.AreEqual((byte)12, serializer.ReadByte()); + Assert.AreEqual((byte)251, serializer.ReadByte()); + serializer.FinishReadingChunk(0xDEAD); + Assert.IsFalse(serializer.IsChunkInProgress); - Assert.AreEqual(0xBEEF, serializer.GetNextChunkLabel()); - serializer.StartReadingChunk(0xBEEF); - Assert.AreEqual(0.0f, serializer.ReadFloat()); - Assert.AreEqual(1.0f, serializer.ReadFloat()); - Assert.AreEqual((float)Math.PI, serializer.ReadFloat()); - Assert.AreEqual(-(float)Math.PI, serializer.ReadFloat()); - Assert.AreEqual(1e8f, serializer.ReadFloat()); - Assert.AreEqual(-1e8f, serializer.ReadFloat()); - Assert.AreEqual(float.PositiveInfinity, serializer.ReadFloat()); - Assert.AreEqual(float.NegativeInfinity, serializer.ReadFloat()); - Assert.IsNaN(serializer.ReadFloat()); - Assert.AreEqual(float.Epsilon, serializer.ReadFloat()); - serializer.FinishReadingChunk(0xBEEF); - Assert.IsFalse(serializer.IsChunkInProgress); + Assert.AreEqual(0xBEEF, serializer.GetNextChunkLabel()); + serializer.StartReadingChunk(0xBEEF); + Assert.AreEqual(0.0f, serializer.ReadFloat()); + Assert.AreEqual(1.0f, serializer.ReadFloat()); + Assert.AreEqual((float)Math.PI, serializer.ReadFloat()); + Assert.AreEqual(-(float)Math.PI, serializer.ReadFloat()); + Assert.AreEqual(1e8f, serializer.ReadFloat()); + Assert.AreEqual(-1e8f, serializer.ReadFloat()); + Assert.AreEqual(float.PositiveInfinity, serializer.ReadFloat()); + Assert.AreEqual(float.NegativeInfinity, serializer.ReadFloat()); + Assert.IsNaN(serializer.ReadFloat()); + Assert.AreEqual(float.Epsilon, serializer.ReadFloat()); + serializer.FinishReadingChunk(0xBEEF); + Assert.IsFalse(serializer.IsChunkInProgress); - Assert.AreEqual(-1, serializer.GetNextChunkLabel()); - } + Assert.AreEqual(-1, serializer.GetNextChunkLabel()); + } - [Test] - public void TestChunkSkipping() { - PolySerializer serializer = new PolySerializer(); - serializer.SetupForWriting(16); + [Test] + public void TestChunkSkipping() + { + PolySerializer serializer = new PolySerializer(); + serializer.SetupForWriting(16); - // Add 50 chunks, labeled 0 to 49. - for (int i = 0; i < 50; i++) { - serializer.StartWritingChunk(i); - serializer.WriteInt(i); - serializer.WriteBool(true); - serializer.WriteFloat(1.234f); - serializer.FinishWritingChunk(i); - } + // Add 50 chunks, labeled 0 to 49. + for (int i = 0; i < 50; i++) + { + serializer.StartWritingChunk(i); + serializer.WriteInt(i); + serializer.WriteBool(true); + serializer.WriteFloat(1.234f); + serializer.FinishWritingChunk(i); + } - serializer.FinishWriting(); - byte[] buffer = serializer.ToByteArray(); - serializer.SetupForReading(buffer, 0, buffer.Length); + serializer.FinishWriting(); + byte[] buffer = serializer.ToByteArray(); + serializer.SetupForReading(buffer, 0, buffer.Length); - // Check that we can freely skip ahead, and can also omit reading parts of chunks. - serializer.StartReadingChunk(7); - Assert.AreEqual(7, serializer.ReadInt()); - serializer.FinishReadingChunk(7); + // Check that we can freely skip ahead, and can also omit reading parts of chunks. + serializer.StartReadingChunk(7); + Assert.AreEqual(7, serializer.ReadInt()); + serializer.FinishReadingChunk(7); - serializer.StartReadingChunk(15); - Assert.AreEqual(15, serializer.ReadInt()); - Assert.True(serializer.ReadBool()); - serializer.FinishReadingChunk(15); + serializer.StartReadingChunk(15); + Assert.AreEqual(15, serializer.ReadInt()); + Assert.True(serializer.ReadBool()); + serializer.FinishReadingChunk(15); - serializer.StartReadingChunk(49); - Assert.AreEqual(49, serializer.ReadInt()); - Assert.True(serializer.ReadBool()); - serializer.FinishReadingChunk(49); - } + serializer.StartReadingChunk(49); + Assert.AreEqual(49, serializer.ReadInt()); + Assert.True(serializer.ReadBool()); + serializer.FinishReadingChunk(49); + } - [Test] - public void TestReuse() { - // First, write a long block of data. - PolySerializer serializer = new PolySerializer(); + [Test] + public void TestReuse() + { + // First, write a long block of data. + PolySerializer serializer = new PolySerializer(); - serializer.SetupForWriting(1024); - serializer.StartWritingChunk(1111); - for (int i = 0; i < 10; i++) { - serializer.WriteInt(i); - } - serializer.FinishWritingChunk(1111); - serializer.FinishWriting(); - byte[] firstOutput = serializer.ToByteArray(); + serializer.SetupForWriting(1024); + serializer.StartWritingChunk(1111); + for (int i = 0; i < 10; i++) + { + serializer.WriteInt(i); + } + serializer.FinishWritingChunk(1111); + serializer.FinishWriting(); + byte[] firstOutput = serializer.ToByteArray(); - // Now reset and write shorter data. This should re-use the same buffer as before. - serializer.SetupForWriting(512); - serializer.StartWritingChunk(1111); - serializer.WriteInt(1234); - serializer.FinishWritingChunk(1111); - serializer.FinishWriting(); - byte[] secondOutput = serializer.ToByteArray(); + // Now reset and write shorter data. This should re-use the same buffer as before. + serializer.SetupForWriting(512); + serializer.StartWritingChunk(1111); + serializer.WriteInt(1234); + serializer.FinishWritingChunk(1111); + serializer.FinishWriting(); + byte[] secondOutput = serializer.ToByteArray(); - // The second output should be smaller than the first, even though the backing - // buffer wasn't resized. - Assert.IsTrue(secondOutput.Length < firstOutput.Length); + // The second output should be smaller than the first, even though the backing + // buffer wasn't resized. + Assert.IsTrue(secondOutput.Length < firstOutput.Length); - // Let's check that the data ends where we expect it to. - serializer.SetupForReading(secondOutput, 0, secondOutput.Length); - serializer.StartReadingChunk(1111); - Assert.AreEqual(1234, serializer.ReadInt()); - // Shouldn't have any more data. - Assert.Throws(() => { serializer.ReadInt(); }); - serializer.FinishReadingChunk(1111); - } + // Let's check that the data ends where we expect it to. + serializer.SetupForReading(secondOutput, 0, secondOutput.Length); + serializer.StartReadingChunk(1111); + Assert.AreEqual(1234, serializer.ReadInt()); + // Shouldn't have any more data. + Assert.Throws(() => { serializer.ReadInt(); }); + serializer.FinishReadingChunk(1111); + } - [Test] - public void TestSanityChecks() { - // First let's make some example data. - PolySerializer s = new PolySerializer(); - s.SetupForWriting(16); - s.StartWritingChunk(1111); - s.WriteInt(1234); - s.WriteInt(5678); - s.FinishWritingChunk(1111); - s.StartWritingChunk(2222); - s.WriteFloat(1.0f); - s.WriteFloat(2.0f); - s.FinishWritingChunk(2222); - s.FinishWriting(); - byte[] exampleData = s.ToByteArray(); + [Test] + public void TestSanityChecks() + { + // First let's make some example data. + PolySerializer s = new PolySerializer(); + s.SetupForWriting(16); + s.StartWritingChunk(1111); + s.WriteInt(1234); + s.WriteInt(5678); + s.FinishWritingChunk(1111); + s.StartWritingChunk(2222); + s.WriteFloat(1.0f); + s.WriteFloat(2.0f); + s.FinishWritingChunk(2222); + s.FinishWriting(); + byte[] exampleData = s.ToByteArray(); - // Reading without setting up should fail. - Assert.Throws(() => { new PolySerializer().StartReadingChunk(1111); }); - // Writing without setting up should fail. - Assert.Throws(() => { new PolySerializer().StartWritingChunk(1111); }); + // Reading without setting up should fail. + Assert.Throws(() => { new PolySerializer().StartReadingChunk(1111); }); + // Writing without setting up should fail. + Assert.Throws(() => { new PolySerializer().StartWritingChunk(1111); }); - // Reading data without starting a chunk is wrong. - Assert.Throws(() => { - PolySerializer serializer = new PolySerializer(); - serializer.SetupForReading(exampleData, 0, exampleData.Length); - serializer.ReadInt(); - }); - // Writing data without starting a chunk is wrong. - Assert.Throws(() => { - PolySerializer serializer = new PolySerializer(); - serializer.SetupForWriting(16); - serializer.WriteInt(1234); - }); + // Reading data without starting a chunk is wrong. + Assert.Throws(() => + { + PolySerializer serializer = new PolySerializer(); + serializer.SetupForReading(exampleData, 0, exampleData.Length); + serializer.ReadInt(); + }); + // Writing data without starting a chunk is wrong. + Assert.Throws(() => + { + PolySerializer serializer = new PolySerializer(); + serializer.SetupForWriting(16); + serializer.WriteInt(1234); + }); - // Writing data when open for reading is wrong. - Assert.Throws(() => { - PolySerializer serializer = new PolySerializer(); - serializer.SetupForReading(exampleData, 0, exampleData.Length); - serializer.StartReadingChunk(1111); - serializer.WriteInt(1234); - }); - // Reading data when open for writing is wrong. - Assert.Throws(() => { - PolySerializer serializer = new PolySerializer(); - serializer.SetupForWriting(16); - serializer.ReadInt(); - }); + // Writing data when open for reading is wrong. + Assert.Throws(() => + { + PolySerializer serializer = new PolySerializer(); + serializer.SetupForReading(exampleData, 0, exampleData.Length); + serializer.StartReadingChunk(1111); + serializer.WriteInt(1234); + }); + // Reading data when open for writing is wrong. + Assert.Throws(() => + { + PolySerializer serializer = new PolySerializer(); + serializer.SetupForWriting(16); + serializer.ReadInt(); + }); - Assert.Throws(() => { - PolySerializer serializer = new PolySerializer(); - serializer.SetupForReading(exampleData, 0, exampleData.Length); - serializer.StartReadingChunk(1111); - // Starting to read another chunk without finishing the current one should fail. - serializer.StartReadingChunk(2222); - }); + Assert.Throws(() => + { + PolySerializer serializer = new PolySerializer(); + serializer.SetupForReading(exampleData, 0, exampleData.Length); + serializer.StartReadingChunk(1111); + // Starting to read another chunk without finishing the current one should fail. + serializer.StartReadingChunk(2222); + }); - Assert.Throws(() => { - PolySerializer serializer = new PolySerializer(); - serializer.SetupForWriting(); - serializer.StartWritingChunk(1111); - // Starting to write another chunk while in the middle of writing one chunk should fail, - // as we don't allow nested chunks. - serializer.StartWritingChunk(2222); - }); + Assert.Throws(() => + { + PolySerializer serializer = new PolySerializer(); + serializer.SetupForWriting(); + serializer.StartWritingChunk(1111); + // Starting to write another chunk while in the middle of writing one chunk should fail, + // as we don't allow nested chunks. + serializer.StartWritingChunk(2222); + }); - Assert.Throws(() => { - PolySerializer serializer = new PolySerializer(); - serializer.SetupForReading(exampleData, 0, exampleData.Length); - serializer.StartReadingChunk(1111); - serializer.ReadInt(); - serializer.ReadInt(); - // Reading past the end of the chunk should fail. - serializer.ReadInt(); - }); + Assert.Throws(() => + { + PolySerializer serializer = new PolySerializer(); + serializer.SetupForReading(exampleData, 0, exampleData.Length); + serializer.StartReadingChunk(1111); + serializer.ReadInt(); + serializer.ReadInt(); + // Reading past the end of the chunk should fail. + serializer.ReadInt(); + }); - Assert.Throws(() => { - PolySerializer serializer = new PolySerializer(); - serializer.SetupForReading(exampleData, 0, exampleData.Length); - // Requesting a chunk that doesn't exist should fail. - serializer.StartReadingChunk(9999); - }); + Assert.Throws(() => + { + PolySerializer serializer = new PolySerializer(); + serializer.SetupForReading(exampleData, 0, exampleData.Length); + // Requesting a chunk that doesn't exist should fail. + serializer.StartReadingChunk(9999); + }); - Assert.Throws(() => { - PolySerializer serializer = new PolySerializer(); - serializer.SetupForWriting(); - serializer.StartWritingChunk(1111); - // Trying to get the byte array without finishing writing should fail. - serializer.ToByteArray(); - }); + Assert.Throws(() => + { + PolySerializer serializer = new PolySerializer(); + serializer.SetupForWriting(); + serializer.StartWritingChunk(1111); + // Trying to get the byte array without finishing writing should fail. + serializer.ToByteArray(); + }); + } } - } } \ No newline at end of file diff --git a/Assets/Editor/tests/tools/SubdividerTest.cs b/Assets/Editor/tests/tools/SubdividerTest.cs index 97d5c365..2e155284 100644 --- a/Assets/Editor/tests/tools/SubdividerTest.cs +++ b/Assets/Editor/tests/tools/SubdividerTest.cs @@ -19,40 +19,43 @@ using com.google.apps.peltzer.client.model.core; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.tools { - [TestFixture] - public class SubdividerTest { - [Test] - public void TestInsertVert() { - Face triangle = new Face(1, - new List() { 1, 2, 3 }.AsReadOnly(), Vector3.zero, - new FaceProperties()); +namespace com.google.apps.peltzer.client.tools +{ + [TestFixture] + public class SubdividerTest + { + [Test] + public void TestInsertVert() + { + Face triangle = new Face(1, + new List() { 1, 2, 3 }.AsReadOnly(), Vector3.zero, + new FaceProperties()); - // Not part of triangle, don't split. - NUnit.Framework.Assert.AreSame(triangle, Subdivider.MaybeInsertVert(triangle, 7, 8, 23)); - NUnit.Framework.Assert.AreSame(triangle, Subdivider.MaybeInsertVert(triangle, 1, 4, 23)); - NUnit.Framework.Assert.AreSame(triangle, Subdivider.MaybeInsertVert(triangle, 3, 5, 23)); + // Not part of triangle, don't split. + NUnit.Framework.Assert.AreSame(triangle, Subdivider.MaybeInsertVert(triangle, 7, 8, 23)); + NUnit.Framework.Assert.AreSame(triangle, Subdivider.MaybeInsertVert(triangle, 1, 4, 23)); + NUnit.Framework.Assert.AreSame(triangle, Subdivider.MaybeInsertVert(triangle, 3, 5, 23)); - // Split between 1 and 2 - List indices = Subdivider.MaybeInsertVert(triangle, 1, 2, 7); - NUnit.Framework.Assert.AreEqual(1, indices[0]); - NUnit.Framework.Assert.AreEqual(7, indices[1]); - NUnit.Framework.Assert.AreEqual(2, indices[2]); - NUnit.Framework.Assert.AreEqual(3, indices[3]); + // Split between 1 and 2 + List indices = Subdivider.MaybeInsertVert(triangle, 1, 2, 7); + NUnit.Framework.Assert.AreEqual(1, indices[0]); + NUnit.Framework.Assert.AreEqual(7, indices[1]); + NUnit.Framework.Assert.AreEqual(2, indices[2]); + NUnit.Framework.Assert.AreEqual(3, indices[3]); - // Reverse order - indices = Subdivider.MaybeInsertVert(triangle, 2, 1, 7); - NUnit.Framework.Assert.AreEqual(1, indices[0]); - NUnit.Framework.Assert.AreEqual(7, indices[1]); - NUnit.Framework.Assert.AreEqual(2, indices[2]); - NUnit.Framework.Assert.AreEqual(3, indices[3]); + // Reverse order + indices = Subdivider.MaybeInsertVert(triangle, 2, 1, 7); + NUnit.Framework.Assert.AreEqual(1, indices[0]); + NUnit.Framework.Assert.AreEqual(7, indices[1]); + NUnit.Framework.Assert.AreEqual(2, indices[2]); + NUnit.Framework.Assert.AreEqual(3, indices[3]); - // At endpoint - indices = Subdivider.MaybeInsertVert(triangle, 3, 1, 7); - NUnit.Framework.Assert.AreEqual(1, indices[0]); - NUnit.Framework.Assert.AreEqual(2, indices[1]); - NUnit.Framework.Assert.AreEqual(3, indices[2]); - NUnit.Framework.Assert.AreEqual(7, indices[3]); + // At endpoint + indices = Subdivider.MaybeInsertVert(triangle, 3, 1, 7); + NUnit.Framework.Assert.AreEqual(1, indices[0]); + NUnit.Framework.Assert.AreEqual(2, indices[1]); + NUnit.Framework.Assert.AreEqual(3, indices[2]); + NUnit.Framework.Assert.AreEqual(7, indices[3]); + } } - } } diff --git a/Assets/Editor/tests/tools/utils/GridUtilsTest.cs b/Assets/Editor/tests/tools/utils/GridUtilsTest.cs index f50f7e86..7ef7d213 100644 --- a/Assets/Editor/tests/tools/utils/GridUtilsTest.cs +++ b/Assets/Editor/tests/tools/utils/GridUtilsTest.cs @@ -18,47 +18,53 @@ using System.Collections.Generic; using com.google.apps.peltzer.client.model.core; -namespace com.google.apps.peltzer.client.tools.utils { - [TestFixture] - public class GridUtilsTest { - [Test] - public void TestGridAlignedFloatsDontSnap() { - List noSnapFloats = new List { +namespace com.google.apps.peltzer.client.tools.utils +{ + [TestFixture] + public class GridUtilsTest + { + [Test] + public void TestGridAlignedFloatsDontSnap() + { + List noSnapFloats = new List { 0, GridUtils.GRID_SIZE, GridUtils.GRID_SIZE * 2 }; - foreach (float f in noSnapFloats) - NUnit.Framework.Assert.AreEqual(f, GridUtils.SnapToGrid(f)); - } + foreach (float f in noSnapFloats) + NUnit.Framework.Assert.AreEqual(f, GridUtils.SnapToGrid(f)); + } - [Test] - public void TestFloatsSnap() { - NUnit.Framework.Assert.AreEqual(0f, GridUtils.SnapToGrid(0.001f)); - NUnit.Framework.Assert.AreEqual(0f, GridUtils.SnapToGrid(0.00124f)); - NUnit.Framework.Assert.AreEqual(0.01f, GridUtils.SnapToGrid(0.0126f)); - NUnit.Framework.Assert.AreEqual(0.02f, GridUtils.SnapToGrid(0.024f)); - NUnit.Framework.Assert.AreEqual(0.03f, GridUtils.SnapToGrid(0.026f)); - NUnit.Framework.Assert.AreEqual(50.5f, GridUtils.SnapToGrid(50.495f)); - } + [Test] + public void TestFloatsSnap() + { + NUnit.Framework.Assert.AreEqual(0f, GridUtils.SnapToGrid(0.001f)); + NUnit.Framework.Assert.AreEqual(0f, GridUtils.SnapToGrid(0.00124f)); + NUnit.Framework.Assert.AreEqual(0.01f, GridUtils.SnapToGrid(0.0126f)); + NUnit.Framework.Assert.AreEqual(0.02f, GridUtils.SnapToGrid(0.024f)); + NUnit.Framework.Assert.AreEqual(0.03f, GridUtils.SnapToGrid(0.026f)); + NUnit.Framework.Assert.AreEqual(50.5f, GridUtils.SnapToGrid(50.495f)); + } - [Test] - public void TestGridAlignedVectorsDontSnap() { - List noSnapVectors = new List { + [Test] + public void TestGridAlignedVectorsDontSnap() + { + List noSnapVectors = new List { Vector3.zero, Vector3.one * GridUtils.GRID_SIZE, Vector3.one * GridUtils.GRID_SIZE * 2, }; - foreach (Vector3 v in noSnapVectors) - NUnit.Framework.Assert.AreEqual(v, GridUtils.SnapToGrid(v)); - } + foreach (Vector3 v in noSnapVectors) + NUnit.Framework.Assert.AreEqual(v, GridUtils.SnapToGrid(v)); + } - [Test] - public void TestVectorsSnap() { - NUnit.Framework.Assert.AreEqual(Vector3.zero, GridUtils.SnapToGrid(new Vector3 (0f, 0.001f, -0.001f))); - NUnit.Framework.Assert.AreEqual(new Vector3(1f,1f,-1f), GridUtils.SnapToGrid(new Vector3(1.001f, 1f, -1.001f))); - NUnit.Framework.Assert.AreEqual(new Vector3(0.25f, 0.5f, -0.25f), - GridUtils.SnapToGrid(new Vector3(0.246f, 0.5001f, -0.2499f))); + [Test] + public void TestVectorsSnap() + { + NUnit.Framework.Assert.AreEqual(Vector3.zero, GridUtils.SnapToGrid(new Vector3(0f, 0.001f, -0.001f))); + NUnit.Framework.Assert.AreEqual(new Vector3(1f, 1f, -1f), GridUtils.SnapToGrid(new Vector3(1.001f, 1f, -1.001f))); + NUnit.Framework.Assert.AreEqual(new Vector3(0.25f, 0.5f, -0.25f), + GridUtils.SnapToGrid(new Vector3(0.246f, 0.5001f, -0.2499f))); + } } - } } diff --git a/Assets/EnvironmentThemeManager.cs b/Assets/EnvironmentThemeManager.cs index 5fd24a47..b7ed3062 100644 --- a/Assets/EnvironmentThemeManager.cs +++ b/Assets/EnvironmentThemeManager.cs @@ -21,314 +21,340 @@ public delegate void EnvironmentThemeActionHandler(object sender, EnvironmentThemeManager.EnvironmentTheme theme); -public class EnvironmentThemeManager : MonoBehaviour { - - public event EnvironmentThemeActionHandler EnvironmentThemeActionHandler; - - public enum EnvironmentTheme { - DAY = 0, - SUNSET = 1, - NIGHT = 2, - PURPLE = 3, - SNOW = 4, - DESERT = 5, - GREEN = 6, - BLACK = 7, - WHITE = 8 - }; - - /// - /// Sky colors for themes. - /// - private readonly Color DAY = new Color(231f / 255f, 251f / 255f, 255f / 255f); - private readonly Color NIGHT = new Color(77f / 255f, 69f / 255f, 103f / 255f); - private readonly Color PURPLE = new Color(206f / 255f, 178f / 255f, 255f / 255f); - private readonly Color BLACK = new Color(0F / 255f, 0F / 255f, 0F / 255f); - private readonly Color WHITE = new Color(229f / 255f, 229f / 255f, 229f / 255f); - - /// - /// Horizon colors for themes. - /// - private readonly Color DAY_HORIZON = new Color(17f / 255f, 139f / 255f, 255f / 255f); - private readonly Color NIGHT_HORIZON = new Color(77f / 255f, 69f / 255f, 103f / 255f); - private readonly Color PURPLE_HORIZON = new Color(255f / 255f, 255f / 255f, 255f / 255f); - private readonly Color BLACK_HORIZON = new Color(0F / 255f, 0F / 255f, 0F / 255f); - private readonly Color WHITE_HORIZON = new Color(0f / 255f, 0f / 255f, 0f / 255f); - - /// - /// Ground color for themes. - /// - private readonly Color DAY_GROUND = new Color(126f / 255f, 109f / 255f, 91f / 255f); - private readonly Color NIGHT_GROUND = new Color(77f / 255f, 69f / 255f, 103f / 255f); - private readonly Color PURPLE_GROUND = new Color(31f / 255f, 25f / 255f, 56f / 255f); - private readonly Color BLACK_GROUND = new Color(0F / 255f, 0F / 255f, 0F / 255f); - private readonly Color WHITE_GROUND = new Color(229f / 255f, 229f / 255f, 229f / 255f); - - /// - /// Fog color for themes. - /// - private readonly Color DAY_FOG = new Color(126f / 255f, 109f / 255f, 91f / 255f); - private readonly Color NIGHT_FOG = new Color(77f / 255f, 69f / 255f, 103f / 255f); - private readonly Color PURPLE_FOG = new Color(77f / 255f, 69f / 255f, 103f / 255f); - private readonly Color BLACK_FOG = new Color(10F / 255f, 10F / 255f, 10F / 255f); - private readonly Color WHITE_FOG = new Color(229f / 255f, 229f / 255f, 229f / 255f); - - /// - /// Fog density for themes. - /// - private readonly float DAY_FOG_DENSITY = 0.00f; - private readonly float NIGHT_FOG_DENSITY = 0.001f; - private readonly float PURPLE_FOG_DENSITY = 0.0005f; - private readonly float BLACK_FOG_DENSITY = 0.0f; - private readonly float WHITE_FOG_DENSITY = 0.0f; - - /// - /// Ambient color for themes. - /// - private readonly Color DAY_AMBIENT = new Color(0.2352941f, 0.1686274f, 0.2509804f); - private readonly Color NIGHT_AMBIENT = new Color(0.2352941f, 0.1686274f, 0.2509804f); - private readonly Color PURPLE_AMBIENT = new Color(0.2352941f, 0.1686274f, 0.2509804f); - private readonly Color BLACK_AMBIENT = new Color(0F / 255f, 0F / 255f, 0F / 255f); - private readonly Color WHITE_AMBIENT = new Color(229f / 255f, 229f / 255f, 229f / 255f); - - /// - /// Sun Radius B for themes. - /// - private readonly float DAY_SUN_RADIUS_B = 0.0463f; - private readonly float NIGHT_SUN_RADIUS_B = 0f; - private readonly float PURPLE_SUN_RADIUS_B = 0f; - private readonly float BLACK_SUN_RADIUS_B = 0f; - private readonly float WHITE_SUN_RADIUS_B = 0f; - - /// - /// Cloud strength for themes. - /// - private readonly float DAY_CLOUD_STRENGTH = 0.273f; - private readonly float NIGHT_CLOUD_STRENGTH = 0.138f; - private readonly float PURPLE_CLOUD_STRENGTH = 0.138f; - private readonly float BLACK_CLOUD_STRENGTH = 0.138f; - private readonly float WHITE_CLOUD_STRENGTH = 0.138f; - - public bool loop = true; - public EnvironmentTheme currentTheme = EnvironmentTheme.PURPLE; - public float transitionLength = 7.50f; - private float transitionStartTime; - - private GameObject sky; - private Material skyMaterial; - private Material groundMaterial; - private Texture skyTexture; - private Texture daySkyTexture; - - private Color currentSkyColor; - private Color currentGroundColor; - private Color currentFogColor; - private Color currentAmbientColor; - private Color currentHorizonColor; - - private float currentSkyStrength; - private float currentSunRadiusB; - private float currentCloudStrength; - private float currentFogDensity; - private float tempFogDensity = -1f; - - private Texture currentSkyTexture; - - private RaycastHit menuHit; - private bool isHoldingSky = false; - - private GameObject purpleGroundAndPlane; - public GameObject nightEnvironment; - public GameObject dayEnvironment; - - - public void Setup() { - transitionStartTime = Time.time; - ResolveReferences(); - } - - private void ResolveReferences() { - // Purple environment. - sky = transform.Find("Sky").gameObject; - skyMaterial = sky.GetComponent().material; - skyTexture = skyMaterial.GetTexture("_Sky"); - purpleGroundAndPlane = ObjectFinder.ObjectById("ID_PurpleGroundAndPlane"); - groundMaterial = purpleGroundAndPlane.transform.Find("Ground").GetComponent().material; - - // Day environment. - GameObject daySky = transform.Find("Environment/S_SkySphere").gameObject; - daySkyTexture = daySky.GetComponent().material.GetTexture("_Sky"); - - currentSkyColor = skyMaterial.GetColor("_SkyColor"); - currentSkyStrength = skyMaterial.GetFloat("_SkyStrength"); - currentHorizonColor = skyMaterial.GetColor("_HorizonColor"); - currentSunRadiusB = skyMaterial.GetFloat("_SunRadiusB"); - currentCloudStrength = skyMaterial.GetFloat("_CloudStrength"); - currentFogDensity = RenderSettings.fogDensity; - currentSkyTexture = skyMaterial.GetTexture("_Sky"); - currentFogColor = RenderSettings.fogColor; - currentAmbientColor = RenderSettings.ambientLight; - currentGroundColor = groundMaterial.GetColor("_Color"); - } - - void Update () { - // Animate - float pctDone = (Time.time - transitionStartTime) / transitionLength; - if (pctDone <= 1.0f) { - // Set color transtion - UpdateSkyColor(pctDone); +public class EnvironmentThemeManager : MonoBehaviour +{ + + public event EnvironmentThemeActionHandler EnvironmentThemeActionHandler; + + public enum EnvironmentTheme + { + DAY = 0, + SUNSET = 1, + NIGHT = 2, + PURPLE = 3, + SNOW = 4, + DESERT = 5, + GREEN = 6, + BLACK = 7, + WHITE = 8 + }; + + /// + /// Sky colors for themes. + /// + private readonly Color DAY = new Color(231f / 255f, 251f / 255f, 255f / 255f); + private readonly Color NIGHT = new Color(77f / 255f, 69f / 255f, 103f / 255f); + private readonly Color PURPLE = new Color(206f / 255f, 178f / 255f, 255f / 255f); + private readonly Color BLACK = new Color(0F / 255f, 0F / 255f, 0F / 255f); + private readonly Color WHITE = new Color(229f / 255f, 229f / 255f, 229f / 255f); + + /// + /// Horizon colors for themes. + /// + private readonly Color DAY_HORIZON = new Color(17f / 255f, 139f / 255f, 255f / 255f); + private readonly Color NIGHT_HORIZON = new Color(77f / 255f, 69f / 255f, 103f / 255f); + private readonly Color PURPLE_HORIZON = new Color(255f / 255f, 255f / 255f, 255f / 255f); + private readonly Color BLACK_HORIZON = new Color(0F / 255f, 0F / 255f, 0F / 255f); + private readonly Color WHITE_HORIZON = new Color(0f / 255f, 0f / 255f, 0f / 255f); + + /// + /// Ground color for themes. + /// + private readonly Color DAY_GROUND = new Color(126f / 255f, 109f / 255f, 91f / 255f); + private readonly Color NIGHT_GROUND = new Color(77f / 255f, 69f / 255f, 103f / 255f); + private readonly Color PURPLE_GROUND = new Color(31f / 255f, 25f / 255f, 56f / 255f); + private readonly Color BLACK_GROUND = new Color(0F / 255f, 0F / 255f, 0F / 255f); + private readonly Color WHITE_GROUND = new Color(229f / 255f, 229f / 255f, 229f / 255f); + + /// + /// Fog color for themes. + /// + private readonly Color DAY_FOG = new Color(126f / 255f, 109f / 255f, 91f / 255f); + private readonly Color NIGHT_FOG = new Color(77f / 255f, 69f / 255f, 103f / 255f); + private readonly Color PURPLE_FOG = new Color(77f / 255f, 69f / 255f, 103f / 255f); + private readonly Color BLACK_FOG = new Color(10F / 255f, 10F / 255f, 10F / 255f); + private readonly Color WHITE_FOG = new Color(229f / 255f, 229f / 255f, 229f / 255f); + + /// + /// Fog density for themes. + /// + private readonly float DAY_FOG_DENSITY = 0.00f; + private readonly float NIGHT_FOG_DENSITY = 0.001f; + private readonly float PURPLE_FOG_DENSITY = 0.0005f; + private readonly float BLACK_FOG_DENSITY = 0.0f; + private readonly float WHITE_FOG_DENSITY = 0.0f; + + /// + /// Ambient color for themes. + /// + private readonly Color DAY_AMBIENT = new Color(0.2352941f, 0.1686274f, 0.2509804f); + private readonly Color NIGHT_AMBIENT = new Color(0.2352941f, 0.1686274f, 0.2509804f); + private readonly Color PURPLE_AMBIENT = new Color(0.2352941f, 0.1686274f, 0.2509804f); + private readonly Color BLACK_AMBIENT = new Color(0F / 255f, 0F / 255f, 0F / 255f); + private readonly Color WHITE_AMBIENT = new Color(229f / 255f, 229f / 255f, 229f / 255f); + + /// + /// Sun Radius B for themes. + /// + private readonly float DAY_SUN_RADIUS_B = 0.0463f; + private readonly float NIGHT_SUN_RADIUS_B = 0f; + private readonly float PURPLE_SUN_RADIUS_B = 0f; + private readonly float BLACK_SUN_RADIUS_B = 0f; + private readonly float WHITE_SUN_RADIUS_B = 0f; + + /// + /// Cloud strength for themes. + /// + private readonly float DAY_CLOUD_STRENGTH = 0.273f; + private readonly float NIGHT_CLOUD_STRENGTH = 0.138f; + private readonly float PURPLE_CLOUD_STRENGTH = 0.138f; + private readonly float BLACK_CLOUD_STRENGTH = 0.138f; + private readonly float WHITE_CLOUD_STRENGTH = 0.138f; + + public bool loop = true; + public EnvironmentTheme currentTheme = EnvironmentTheme.PURPLE; + public float transitionLength = 7.50f; + private float transitionStartTime; + + private GameObject sky; + private Material skyMaterial; + private Material groundMaterial; + private Texture skyTexture; + private Texture daySkyTexture; + + private Color currentSkyColor; + private Color currentGroundColor; + private Color currentFogColor; + private Color currentAmbientColor; + private Color currentHorizonColor; + + private float currentSkyStrength; + private float currentSunRadiusB; + private float currentCloudStrength; + private float currentFogDensity; + private float tempFogDensity = -1f; + + private Texture currentSkyTexture; + + private RaycastHit menuHit; + private bool isHoldingSky = false; + + private GameObject purpleGroundAndPlane; + public GameObject nightEnvironment; + public GameObject dayEnvironment; + + + public void Setup() + { + transitionStartTime = Time.time; + ResolveReferences(); } - } - - /// - /// Entry point for setting environment. Things that need only happen once and not on update loop. - /// Mainly we cache some previous values to facilitate the transitions. - /// - /// - public void SetEnvironment(EnvironmentTheme theme) { - tempFogDensity = -1f; - if (EnvironmentThemeActionHandler != null) EnvironmentThemeActionHandler(null, theme); - if (skyMaterial != null) { - currentSkyColor = skyMaterial.GetColor("_SkyColor"); - currentSkyStrength = skyMaterial.GetFloat("_SkyStrength"); - currentHorizonColor = skyMaterial.GetColor("_HorizonColor"); - currentSunRadiusB = skyMaterial.GetFloat("_SunRadiusB"); - currentCloudStrength = skyMaterial.GetFloat("_CloudStrength"); - currentSkyTexture = skyMaterial.GetTexture("_Sky"); - } - currentFogColor = RenderSettings.fogColor; - currentFogDensity = RenderSettings.fogDensity; - currentAmbientColor = RenderSettings.ambientLight; - if(groundMaterial != null) currentGroundColor = groundMaterial.GetColor("_Color"); - currentTheme = theme; - transitionStartTime = Time.time; - switch (theme) { - case EnvironmentTheme.DAY: - dayEnvironment.SetActive(true); - nightEnvironment.SetActive(false); - purpleGroundAndPlane.SetActive(true); - break; - case EnvironmentTheme.PURPLE: - dayEnvironment.SetActive(false); - nightEnvironment.SetActive(true); - purpleGroundAndPlane.SetActive(true); - break; - case EnvironmentTheme.BLACK: - dayEnvironment.SetActive(false); - nightEnvironment.SetActive(true); - purpleGroundAndPlane.SetActive(false); - break; - case EnvironmentTheme.WHITE: - dayEnvironment.SetActive(false); - nightEnvironment.SetActive(true); - purpleGroundAndPlane.SetActive(false); - break; + + private void ResolveReferences() + { + // Purple environment. + sky = transform.Find("Sky").gameObject; + skyMaterial = sky.GetComponent().material; + skyTexture = skyMaterial.GetTexture("_Sky"); + purpleGroundAndPlane = ObjectFinder.ObjectById("ID_PurpleGroundAndPlane"); + groundMaterial = purpleGroundAndPlane.transform.Find("Ground").GetComponent().material; + + // Day environment. + GameObject daySky = transform.Find("Environment/S_SkySphere").gameObject; + daySkyTexture = daySky.GetComponent().material.GetTexture("_Sky"); + + currentSkyColor = skyMaterial.GetColor("_SkyColor"); + currentSkyStrength = skyMaterial.GetFloat("_SkyStrength"); + currentHorizonColor = skyMaterial.GetColor("_HorizonColor"); + currentSunRadiusB = skyMaterial.GetFloat("_SunRadiusB"); + currentCloudStrength = skyMaterial.GetFloat("_CloudStrength"); + currentFogDensity = RenderSettings.fogDensity; + currentSkyTexture = skyMaterial.GetTexture("_Sky"); + currentFogColor = RenderSettings.fogColor; + currentAmbientColor = RenderSettings.ambientLight; + currentGroundColor = groundMaterial.GetColor("_Color"); } - } - - /// - /// Lerps the current theme parameters, updates anything that needs to be per frame. - /// - /// Percent done of transition time. - private void UpdateSkyColor(float pctDone) { - switch (currentTheme) { - case EnvironmentTheme.DAY: - // Sky - skyMaterial.SetColor("_HorizonColor", Color.Lerp(currentHorizonColor, DAY_HORIZON, pctDone)); - skyMaterial.SetFloat("_SunRadiusB", Mathf.Lerp(currentSunRadiusB, DAY_SUN_RADIUS_B, pctDone)); - skyMaterial.SetFloat("_CloudStrength", Mathf.Lerp(currentCloudStrength, DAY_CLOUD_STRENGTH, pctDone)); - skyMaterial.SetTexture("_Sky", daySkyTexture); - - skyMaterial.SetColor("_SkyColor", Color.Lerp(currentSkyColor, DAY, pctDone)); - skyMaterial.SetFloat("_SkyStrength", Mathf.Lerp(currentSkyStrength, 1.00f, pctDone)); - groundMaterial.SetColor("_Color", Color.Lerp(currentGroundColor, DAY_GROUND, pctDone)); - RenderSettings.fogColor = Color.Lerp(currentFogColor, DAY_FOG, pctDone); - if (pctDone < 0.5f) { - RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, 1.0f, pctDone * 2.0f); - } else { - if (tempFogDensity < 0) currentFogDensity = RenderSettings.fogDensity; - RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, DAY_FOG_DENSITY, (pctDone - 0.5f) / 0.5f); - } - RenderSettings.ambientLight = Color.Lerp(currentAmbientColor, DAY_AMBIENT, pctDone); - break; - case EnvironmentTheme.NIGHT: - skyMaterial.SetColor("_HorizonColor", Color.Lerp(currentHorizonColor, NIGHT_HORIZON, pctDone)); - skyMaterial.SetFloat("_SunRadiusB", Mathf.Lerp(currentSunRadiusB, NIGHT_SUN_RADIUS_B, pctDone)); - skyMaterial.SetFloat("_CloudStrength", Mathf.Lerp(currentCloudStrength, NIGHT_CLOUD_STRENGTH, pctDone)); - skyMaterial.SetTexture("_Sky", skyTexture); - - skyMaterial.SetColor("_SkyColor", Color.Lerp(currentSkyColor, NIGHT, pctDone)); - skyMaterial.SetFloat("_SkyStrength", Mathf.Lerp(currentSkyStrength, 1.00f, pctDone)); - groundMaterial.SetColor("_Color", Color.Lerp(currentGroundColor, NIGHT_GROUND, pctDone)); - RenderSettings.fogColor = Color.Lerp(currentFogColor, NIGHT_FOG, pctDone); - if(pctDone < 0.5f) { - RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, 1.0f, pctDone * 2.0f); - } else { - if (tempFogDensity < 0) currentFogDensity = RenderSettings.fogDensity; - RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, NIGHT_FOG_DENSITY, (pctDone - 0.5f) / 0.5f); + + void Update() + { + // Animate + float pctDone = (Time.time - transitionStartTime) / transitionLength; + if (pctDone <= 1.0f) + { + // Set color transtion + UpdateSkyColor(pctDone); } - RenderSettings.fog = true; - RenderSettings.ambientLight = Color.Lerp(currentAmbientColor, NIGHT_AMBIENT, pctDone); - break; - case EnvironmentTheme.PURPLE: - skyMaterial.SetColor("_HorizonColor", Color.Lerp(currentHorizonColor, PURPLE_HORIZON, pctDone)); - skyMaterial.SetFloat("_SunRadiusB", Mathf.Lerp(currentSunRadiusB, PURPLE_SUN_RADIUS_B, pctDone)); - skyMaterial.SetFloat("_CloudStrength", Mathf.Lerp(currentCloudStrength, PURPLE_CLOUD_STRENGTH, pctDone)); - skyMaterial.SetTexture("_Sky", skyTexture); - - skyMaterial.SetColor("_SkyColor", Color.Lerp(currentSkyColor, PURPLE, pctDone)); - skyMaterial.SetFloat("_SkyStrength", Mathf.Lerp(currentSkyStrength, 1.00f, pctDone)); - groundMaterial.SetColor("_Color", Color.Lerp(currentGroundColor, PURPLE_GROUND, pctDone)); - RenderSettings.fogColor = Color.Lerp(currentFogColor, PURPLE_FOG, pctDone); - if (pctDone < 0.5f) { - RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, 1.0f, pctDone * 2.0f); - } else { - if (tempFogDensity < 0) currentFogDensity = RenderSettings.fogDensity; - RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, PURPLE_FOG_DENSITY, (pctDone - 0.5f) / 0.5f); + } + + /// + /// Entry point for setting environment. Things that need only happen once and not on update loop. + /// Mainly we cache some previous values to facilitate the transitions. + /// + /// + public void SetEnvironment(EnvironmentTheme theme) + { + tempFogDensity = -1f; + if (EnvironmentThemeActionHandler != null) EnvironmentThemeActionHandler(null, theme); + if (skyMaterial != null) + { + currentSkyColor = skyMaterial.GetColor("_SkyColor"); + currentSkyStrength = skyMaterial.GetFloat("_SkyStrength"); + currentHorizonColor = skyMaterial.GetColor("_HorizonColor"); + currentSunRadiusB = skyMaterial.GetFloat("_SunRadiusB"); + currentCloudStrength = skyMaterial.GetFloat("_CloudStrength"); + currentSkyTexture = skyMaterial.GetTexture("_Sky"); } - RenderSettings.fog = true; - RenderSettings.ambientLight = Color.Lerp(currentAmbientColor, PURPLE_AMBIENT, pctDone); - break; - case EnvironmentTheme.BLACK: - skyMaterial.SetColor("_HorizonColor", Color.Lerp(currentHorizonColor, BLACK_HORIZON, pctDone)); - skyMaterial.SetFloat("_SunRadiusB", Mathf.Lerp(currentSunRadiusB, BLACK_SUN_RADIUS_B, pctDone)); - skyMaterial.SetFloat("_CloudStrength", Mathf.Lerp(currentCloudStrength, BLACK_CLOUD_STRENGTH, pctDone)); - skyMaterial.SetTexture("_Sky", skyTexture); - - skyMaterial.SetColor("_SkyColor", Color.Lerp(currentSkyColor, BLACK, pctDone)); - skyMaterial.SetFloat("_SkyStrength", Mathf.Lerp(currentSkyStrength, 0.00f, pctDone)); - groundMaterial.SetColor("_Color", Color.Lerp(currentGroundColor, BLACK_GROUND, pctDone)); - RenderSettings.fogColor = Color.Lerp(currentFogColor, BLACK_FOG, pctDone); - if (pctDone < 0.5f) { - RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, 1.0f, pctDone * 2.0f); - } else { - if (tempFogDensity < 0) currentFogDensity = RenderSettings.fogDensity; - RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, BLACK_FOG_DENSITY, (pctDone - 0.5f) / 0.5f); + currentFogColor = RenderSettings.fogColor; + currentFogDensity = RenderSettings.fogDensity; + currentAmbientColor = RenderSettings.ambientLight; + if (groundMaterial != null) currentGroundColor = groundMaterial.GetColor("_Color"); + currentTheme = theme; + transitionStartTime = Time.time; + switch (theme) + { + case EnvironmentTheme.DAY: + dayEnvironment.SetActive(true); + nightEnvironment.SetActive(false); + purpleGroundAndPlane.SetActive(true); + break; + case EnvironmentTheme.PURPLE: + dayEnvironment.SetActive(false); + nightEnvironment.SetActive(true); + purpleGroundAndPlane.SetActive(true); + break; + case EnvironmentTheme.BLACK: + dayEnvironment.SetActive(false); + nightEnvironment.SetActive(true); + purpleGroundAndPlane.SetActive(false); + break; + case EnvironmentTheme.WHITE: + dayEnvironment.SetActive(false); + nightEnvironment.SetActive(true); + purpleGroundAndPlane.SetActive(false); + break; } - RenderSettings.fog = true; - RenderSettings.ambientLight = Color.Lerp(currentAmbientColor, BLACK_AMBIENT, pctDone); - break; - case EnvironmentTheme.WHITE: - skyMaterial.SetColor("_HorizonColor", Color.Lerp(currentHorizonColor, WHITE_HORIZON, pctDone)); - skyMaterial.SetFloat("_SunRadiusB", Mathf.Lerp(currentSunRadiusB, WHITE_SUN_RADIUS_B, pctDone)); - skyMaterial.SetFloat("_CloudStrength", Mathf.Lerp(currentCloudStrength, WHITE_CLOUD_STRENGTH, pctDone)); - skyMaterial.SetTexture("_Sky", skyTexture); - - skyMaterial.SetColor("_SkyColor", Color.Lerp(currentSkyColor, WHITE, pctDone)); - skyMaterial.SetFloat("_SkyStrength", Mathf.Lerp(currentSkyStrength, 0.00f, pctDone)); - groundMaterial.SetColor("_Color", Color.Lerp(currentGroundColor, WHITE_GROUND, pctDone)); - RenderSettings.fogColor = Color.Lerp(currentFogColor, WHITE_FOG, pctDone); - if (pctDone < 0.5f) { - RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, 1.0f, pctDone * 2.0f); - } else { - if (tempFogDensity < 0) currentFogDensity = RenderSettings.fogDensity; - RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, WHITE_FOG_DENSITY, (pctDone - 0.5f) / 0.5f); + } + + /// + /// Lerps the current theme parameters, updates anything that needs to be per frame. + /// + /// Percent done of transition time. + private void UpdateSkyColor(float pctDone) + { + switch (currentTheme) + { + case EnvironmentTheme.DAY: + // Sky + skyMaterial.SetColor("_HorizonColor", Color.Lerp(currentHorizonColor, DAY_HORIZON, pctDone)); + skyMaterial.SetFloat("_SunRadiusB", Mathf.Lerp(currentSunRadiusB, DAY_SUN_RADIUS_B, pctDone)); + skyMaterial.SetFloat("_CloudStrength", Mathf.Lerp(currentCloudStrength, DAY_CLOUD_STRENGTH, pctDone)); + skyMaterial.SetTexture("_Sky", daySkyTexture); + + skyMaterial.SetColor("_SkyColor", Color.Lerp(currentSkyColor, DAY, pctDone)); + skyMaterial.SetFloat("_SkyStrength", Mathf.Lerp(currentSkyStrength, 1.00f, pctDone)); + groundMaterial.SetColor("_Color", Color.Lerp(currentGroundColor, DAY_GROUND, pctDone)); + RenderSettings.fogColor = Color.Lerp(currentFogColor, DAY_FOG, pctDone); + if (pctDone < 0.5f) + { + RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, 1.0f, pctDone * 2.0f); + } + else + { + if (tempFogDensity < 0) currentFogDensity = RenderSettings.fogDensity; + RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, DAY_FOG_DENSITY, (pctDone - 0.5f) / 0.5f); + } + RenderSettings.ambientLight = Color.Lerp(currentAmbientColor, DAY_AMBIENT, pctDone); + break; + case EnvironmentTheme.NIGHT: + skyMaterial.SetColor("_HorizonColor", Color.Lerp(currentHorizonColor, NIGHT_HORIZON, pctDone)); + skyMaterial.SetFloat("_SunRadiusB", Mathf.Lerp(currentSunRadiusB, NIGHT_SUN_RADIUS_B, pctDone)); + skyMaterial.SetFloat("_CloudStrength", Mathf.Lerp(currentCloudStrength, NIGHT_CLOUD_STRENGTH, pctDone)); + skyMaterial.SetTexture("_Sky", skyTexture); + + skyMaterial.SetColor("_SkyColor", Color.Lerp(currentSkyColor, NIGHT, pctDone)); + skyMaterial.SetFloat("_SkyStrength", Mathf.Lerp(currentSkyStrength, 1.00f, pctDone)); + groundMaterial.SetColor("_Color", Color.Lerp(currentGroundColor, NIGHT_GROUND, pctDone)); + RenderSettings.fogColor = Color.Lerp(currentFogColor, NIGHT_FOG, pctDone); + if (pctDone < 0.5f) + { + RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, 1.0f, pctDone * 2.0f); + } + else + { + if (tempFogDensity < 0) currentFogDensity = RenderSettings.fogDensity; + RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, NIGHT_FOG_DENSITY, (pctDone - 0.5f) / 0.5f); + } + RenderSettings.fog = true; + RenderSettings.ambientLight = Color.Lerp(currentAmbientColor, NIGHT_AMBIENT, pctDone); + break; + case EnvironmentTheme.PURPLE: + skyMaterial.SetColor("_HorizonColor", Color.Lerp(currentHorizonColor, PURPLE_HORIZON, pctDone)); + skyMaterial.SetFloat("_SunRadiusB", Mathf.Lerp(currentSunRadiusB, PURPLE_SUN_RADIUS_B, pctDone)); + skyMaterial.SetFloat("_CloudStrength", Mathf.Lerp(currentCloudStrength, PURPLE_CLOUD_STRENGTH, pctDone)); + skyMaterial.SetTexture("_Sky", skyTexture); + + skyMaterial.SetColor("_SkyColor", Color.Lerp(currentSkyColor, PURPLE, pctDone)); + skyMaterial.SetFloat("_SkyStrength", Mathf.Lerp(currentSkyStrength, 1.00f, pctDone)); + groundMaterial.SetColor("_Color", Color.Lerp(currentGroundColor, PURPLE_GROUND, pctDone)); + RenderSettings.fogColor = Color.Lerp(currentFogColor, PURPLE_FOG, pctDone); + if (pctDone < 0.5f) + { + RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, 1.0f, pctDone * 2.0f); + } + else + { + if (tempFogDensity < 0) currentFogDensity = RenderSettings.fogDensity; + RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, PURPLE_FOG_DENSITY, (pctDone - 0.5f) / 0.5f); + } + RenderSettings.fog = true; + RenderSettings.ambientLight = Color.Lerp(currentAmbientColor, PURPLE_AMBIENT, pctDone); + break; + case EnvironmentTheme.BLACK: + skyMaterial.SetColor("_HorizonColor", Color.Lerp(currentHorizonColor, BLACK_HORIZON, pctDone)); + skyMaterial.SetFloat("_SunRadiusB", Mathf.Lerp(currentSunRadiusB, BLACK_SUN_RADIUS_B, pctDone)); + skyMaterial.SetFloat("_CloudStrength", Mathf.Lerp(currentCloudStrength, BLACK_CLOUD_STRENGTH, pctDone)); + skyMaterial.SetTexture("_Sky", skyTexture); + + skyMaterial.SetColor("_SkyColor", Color.Lerp(currentSkyColor, BLACK, pctDone)); + skyMaterial.SetFloat("_SkyStrength", Mathf.Lerp(currentSkyStrength, 0.00f, pctDone)); + groundMaterial.SetColor("_Color", Color.Lerp(currentGroundColor, BLACK_GROUND, pctDone)); + RenderSettings.fogColor = Color.Lerp(currentFogColor, BLACK_FOG, pctDone); + if (pctDone < 0.5f) + { + RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, 1.0f, pctDone * 2.0f); + } + else + { + if (tempFogDensity < 0) currentFogDensity = RenderSettings.fogDensity; + RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, BLACK_FOG_DENSITY, (pctDone - 0.5f) / 0.5f); + } + RenderSettings.fog = true; + RenderSettings.ambientLight = Color.Lerp(currentAmbientColor, BLACK_AMBIENT, pctDone); + break; + case EnvironmentTheme.WHITE: + skyMaterial.SetColor("_HorizonColor", Color.Lerp(currentHorizonColor, WHITE_HORIZON, pctDone)); + skyMaterial.SetFloat("_SunRadiusB", Mathf.Lerp(currentSunRadiusB, WHITE_SUN_RADIUS_B, pctDone)); + skyMaterial.SetFloat("_CloudStrength", Mathf.Lerp(currentCloudStrength, WHITE_CLOUD_STRENGTH, pctDone)); + skyMaterial.SetTexture("_Sky", skyTexture); + + skyMaterial.SetColor("_SkyColor", Color.Lerp(currentSkyColor, WHITE, pctDone)); + skyMaterial.SetFloat("_SkyStrength", Mathf.Lerp(currentSkyStrength, 0.00f, pctDone)); + groundMaterial.SetColor("_Color", Color.Lerp(currentGroundColor, WHITE_GROUND, pctDone)); + RenderSettings.fogColor = Color.Lerp(currentFogColor, WHITE_FOG, pctDone); + if (pctDone < 0.5f) + { + RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, 1.0f, pctDone * 2.0f); + } + else + { + if (tempFogDensity < 0) currentFogDensity = RenderSettings.fogDensity; + RenderSettings.fogDensity = Mathf.Lerp(currentFogDensity, WHITE_FOG_DENSITY, (pctDone - 0.5f) / 0.5f); + } + RenderSettings.fog = true; + RenderSettings.ambientLight = Color.Lerp(currentAmbientColor, WHITE_AMBIENT, pctDone); + break; + default: + break; } - RenderSettings.fog = true; - RenderSettings.ambientLight = Color.Lerp(currentAmbientColor, WHITE_AMBIENT, pctDone); - break; - default: - break; } - } } diff --git a/Assets/MaterialLibrary.cs b/Assets/MaterialLibrary.cs index b1dafe77..e9e3f98e 100644 --- a/Assets/MaterialLibrary.cs +++ b/Assets/MaterialLibrary.cs @@ -20,34 +20,36 @@ /// This class primarily exists to provide an editor level interface for assigning materials for various effects /// other than adding them directly to PeltzerMain. /// -public class MaterialLibrary : MonoBehaviour { - public float paperCraftNoiseScale = 500f; - public float paperCraftNoiseIntensity = 0.2f; +public class MaterialLibrary : MonoBehaviour +{ + public float paperCraftNoiseScale = 500f; + public float paperCraftNoiseIntensity = 0.2f; - public Material baseMaterial; - public Material highlightMaterial; - public Material highlightMaterial2; - public Material highlightSilhouetteMaterial; - public Material gemMaterial; - public Material glassMaterial; - public Material glassSpecMaterial; - public Material subtractMaterial; - public Material copyMaterial; - public Material snapEffectMaterial; - public Material meshInsertEffectMaterial; - public Material meshSelectMaterial; - public Material gridMaterial; - public Material pointHighlightMaterial; - public Material pointInactiveMaterial; - public Material edgeHighlightMaterial; - public Material edgeInactiveMaterial; - public Material faceHighlightMaterial; - public Material facePaintMaterial; - public Material faceExtrudeMaterial; - public Material selectMaterial; + public Material baseMaterial; + public Material highlightMaterial; + public Material highlightMaterial2; + public Material highlightSilhouetteMaterial; + public Material gemMaterial; + public Material glassMaterial; + public Material glassSpecMaterial; + public Material subtractMaterial; + public Material copyMaterial; + public Material snapEffectMaterial; + public Material meshInsertEffectMaterial; + public Material meshSelectMaterial; + public Material gridMaterial; + public Material pointHighlightMaterial; + public Material pointInactiveMaterial; + public Material edgeHighlightMaterial; + public Material edgeInactiveMaterial; + public Material faceHighlightMaterial; + public Material facePaintMaterial; + public Material faceExtrudeMaterial; + public Material selectMaterial; - public void Start() { - Shader.SetGlobalFloat("_NoiseScale", paperCraftNoiseScale); - Shader.SetGlobalFloat("_NoiseIntensity", paperCraftNoiseIntensity); - } + public void Start() + { + Shader.SetGlobalFloat("_NoiseScale", paperCraftNoiseScale); + Shader.SetGlobalFloat("_NoiseIntensity", paperCraftNoiseIntensity); + } } diff --git a/Assets/Overlay.cs b/Assets/Overlay.cs index ef3d8925..c8ebb232 100644 --- a/Assets/Overlay.cs +++ b/Assets/Overlay.cs @@ -16,22 +16,23 @@ using System.Collections.Generic; using UnityEngine; -public class Overlay : MonoBehaviour { - public GameObject up; - public GameObject down; - public GameObject left; - public GameObject right; - public GameObject center; - public GameObject on; - public GameObject off; +public class Overlay : MonoBehaviour +{ + public GameObject up; + public GameObject down; + public GameObject left; + public GameObject right; + public GameObject center; + public GameObject on; + public GameObject off; - public SpriteRenderer upIcon; - public SpriteRenderer downIcon; - public SpriteRenderer leftIcon; - public SpriteRenderer rightIcon; - public SpriteRenderer centerIcon; - public SpriteRenderer onIcon; - public SpriteRenderer offIcon; + public SpriteRenderer upIcon; + public SpriteRenderer downIcon; + public SpriteRenderer leftIcon; + public SpriteRenderer rightIcon; + public SpriteRenderer centerIcon; + public SpriteRenderer onIcon; + public SpriteRenderer offIcon; - public SpriteRenderer[] icons; + public SpriteRenderer[] icons; } diff --git a/Assets/Rendering/EnvironmentSphere.cs b/Assets/Rendering/EnvironmentSphere.cs index c7e15c97..89761c26 100644 --- a/Assets/Rendering/EnvironmentSphere.cs +++ b/Assets/Rendering/EnvironmentSphere.cs @@ -16,23 +16,27 @@ using System.Collections; [ExecuteInEditMode] -public class EnvironmentSphere : MonoBehaviour { +public class EnvironmentSphere : MonoBehaviour +{ public Texture2D ambientTex; public float ambientStrength; private float cachedAmbientStrength; - - void Start () { - Shader.SetGlobalTexture("_EnvironmentSphere", ambientTex); - cachedAmbientStrength = ambientStrength; - Shader.SetGlobalFloat("_EnviroStrength", cachedAmbientStrength); - } - - void Update(){ - if(cachedAmbientStrength != ambientStrength){ + + void Start() + { + Shader.SetGlobalTexture("_EnvironmentSphere", ambientTex); + cachedAmbientStrength = ambientStrength; + Shader.SetGlobalFloat("_EnviroStrength", cachedAmbientStrength); + } + + void Update() + { + if (cachedAmbientStrength != ambientStrength) + { cachedAmbientStrength = ambientStrength; Shader.SetGlobalFloat("_EnviroStrength", cachedAmbientStrength); } - + } } diff --git a/Assets/Rendering/Lighting.cs b/Assets/Rendering/Lighting.cs index 51dfb0ab..546080e7 100644 --- a/Assets/Rendering/Lighting.cs +++ b/Assets/Rendering/Lighting.cs @@ -15,235 +15,251 @@ using UnityEngine; [ExecuteInEditMode] -public class Lighting : MonoBehaviour { - public RenderTextureFormat renderTextureFormat = RenderTextureFormat.RGFloat; - - public Shader depthReplacementShader; - public Camera shadowCamera; - - // This value needs to be kept in sync with the value in Assets\Rendering\shaderMath.cginc - private const int NUMBER_OF_POINT_LIGHTS = 8; - - // Used to configure lighting within the Unity editor - public Color environmentOverride = new Color(1f, 1f, 1f); - public float environmentOverrideAmount = 0f; - public float environmentSpecularAmount = 1f; - public RenderTexture shadowTexture; - private RenderTexture blurShadowTexture; - - public Cubemap environmentCube; - public float environmentDiffuseStrength = 1f; - public float environmentSpecularStrength = 1f; - - public float shadowDistance = 100; - public Color lightColor = new Color(1f, 1f, 1f); - public float lightStrength = .6f; - public float lightSize = 0.5f; - public Color fillLightColor = new Color(1f, 1f, 1f); - public float fillLightStrength = .6f; - public Color fogColor = new Color(.8f, .7f, .7f); - public float fogStrength = 0.1f; - public Material blurMat; - public bool downsample = false; - public int blurIterations = 0; - const int shadowResolution = 1024; - - public Light[] pointLights = new Light[NUMBER_OF_POINT_LIGHTS]; - - // Index of pointLights that holds the button light effect. - private const int BUTTON_LIGHT_INDEX = 0; - - private Vector4[] lightColors = new Vector4[NUMBER_OF_POINT_LIGHTS]; - private Vector4[] lightPositions = new Vector4[NUMBER_OF_POINT_LIGHTS]; - - // String identifiers for global shader uniforms used for lighting the majority of materials - const string LIGHT_DIRECTION = "_LightDirection"; - const string LIGHT_COLOR = "_LightColor"; - const string LIGHT_POSITION = "_LightPosition"; - const string LIGHT_SIZE = "_LightSize"; - const string CAMERA_SIZE = "_CameraSize"; - const string FILL_LIGHT_COLOR = "_FillLightColor"; - const string FILL_LIGHT_POSITION = "_FillLightPosition"; - const string SHADOW_DISTANCE = "_ShadowInfo"; - const string SHADOW_MATRIX = "_ShadowMatrix"; - const string SHADOW_TEX = "_ShadowTexture"; - const string SHADOW_BLUR_TEX = "_ShadowBlurTexture"; - const string FOG_COLOR = "_FogColor"; - const string FOG_STRENGTH = "_FogStrength"; - const string POINT_LIGHT_POSITIONS = "_PointLightWorldPositions"; - const string POINT_LIGHT_COLORS = "_PointLightColors"; - const string ENV_OVERRIDE = "_EnvOverrideColor"; - const string ENV_OVERRIDE_AMOUNT = "_EnvOverrideAmount"; - const string ENV_SPECULAR_AMOUNT = "_EnvSpecularAmount"; - const string ENV_CUBEMAP = "_EnvCubeMap"; - const string ENV_DIFFUSE_STRENGTH = "_EnvDiffuseStrength"; - const string ENV_SPECULAR_STRENGTH = "_EnvSpecularStrength"; - - // Integer ids for global shader uniforms - it's slightly faster to set values using these. - int LIGHT_DIRECTION_ID; - int LIGHT_COLOR_ID; - int LIGHT_POSITION_ID; - int LIGHT_SIZE_ID; - int CAMERA_SIZE_ID; - int FILL_LIGHT_COLOR_ID; - int FILL_LIGHT_POSITION_ID; - int SHADOW_DISTANCE_ID; - int SHADOW_MATRIX_ID; - int SHADOW_TEX_ID; - int SHADOW_BLUR_TEX_ID; - int FOG_COLOR_ID; - int FOG_STRENGTH_ID; - int POINT_LIGHT_WORLD_POSITIONS_ID; - int POINT_LIGHT_COLORS_ID; - int ENV_OVERRIDE_ID; - int ENV_OVERRIDE_AMOUNT_ID; - int ENV_SPECULAR_AMOUNT_ID; - int ENV_CUBEMAP_ID; - int ENV_DIFFUSE_STRENGTH_ID; - int ENV_SPECULAR_STRENGTH_ID; - - void Start () { - LIGHT_DIRECTION_ID = Shader.PropertyToID(LIGHT_DIRECTION); - LIGHT_COLOR_ID = Shader.PropertyToID(LIGHT_COLOR); - LIGHT_POSITION_ID = Shader.PropertyToID(LIGHT_POSITION); - LIGHT_SIZE_ID = Shader.PropertyToID(LIGHT_SIZE); - CAMERA_SIZE_ID = Shader.PropertyToID(CAMERA_SIZE); - FILL_LIGHT_COLOR_ID = Shader.PropertyToID(FILL_LIGHT_COLOR); - FILL_LIGHT_POSITION_ID = Shader.PropertyToID(FILL_LIGHT_POSITION); - SHADOW_DISTANCE_ID = Shader.PropertyToID(SHADOW_DISTANCE); - SHADOW_MATRIX_ID = Shader.PropertyToID(SHADOW_MATRIX); - SHADOW_TEX_ID = Shader.PropertyToID(SHADOW_TEX); - SHADOW_BLUR_TEX_ID = Shader.PropertyToID(SHADOW_BLUR_TEX); - FOG_COLOR_ID = Shader.PropertyToID(FOG_COLOR); - FOG_STRENGTH_ID = Shader.PropertyToID(FOG_STRENGTH); - POINT_LIGHT_WORLD_POSITIONS_ID = Shader.PropertyToID(POINT_LIGHT_POSITIONS); - POINT_LIGHT_COLORS_ID = Shader.PropertyToID(POINT_LIGHT_COLORS); - ENV_OVERRIDE_ID = Shader.PropertyToID(ENV_OVERRIDE); - ENV_OVERRIDE_AMOUNT_ID = Shader.PropertyToID(ENV_OVERRIDE_AMOUNT); - ENV_SPECULAR_AMOUNT_ID = Shader.PropertyToID(ENV_SPECULAR_AMOUNT); - ENV_CUBEMAP_ID = Shader.PropertyToID(ENV_CUBEMAP); - ENV_DIFFUSE_STRENGTH_ID = Shader.PropertyToID(ENV_DIFFUSE_STRENGTH); - ENV_SPECULAR_STRENGTH_ID = Shader.PropertyToID(ENV_SPECULAR_STRENGTH); - InitializeShadows(); - UpdateLightPositions(); - Shader.SetGlobalTexture( ENV_CUBEMAP_ID, environmentCube); - - } - - public void UpdateLightPositions() { - // Updates array of lighting values from configured Unity point lights. We need to pass the data into the shader in - // this form. - for (int i = 0; i < NUMBER_OF_POINT_LIGHTS; i++) { - if (pointLights.Length > i && pointLights[i] != null) { - Light curLight = pointLights[i]; - Vector3 pos = pointLights[i].transform.position; - lightPositions[i] = new Vector4(pos.x, pos.y, pos.z, 1f); - lightColors[i] = new Vector4(curLight.color.r, curLight.color.g, curLight.color.b, curLight.intensity); - } - else { - lightPositions[i] = Vector4.zero; - lightColors[i] = new Vector4(0f, 0f, 0f, 0f); - } - } - } - - void InitializeShadows(){ - if(shadowCamera != null){ - shadowCamera.SetReplacementShader(depthReplacementShader, "RenderType"); - if (shadowTexture == null) { - shadowTexture = new RenderTexture(shadowResolution, - shadowResolution, - 32, - renderTextureFormat, - RenderTextureReadWrite.Linear); - shadowTexture.useMipMap = false; - shadowTexture.autoGenerateMips = false; - shadowTexture.filterMode = FilterMode.Point; - shadowTexture.wrapMode = TextureWrapMode.Clamp; - } - shadowTexture.useMipMap = false; - shadowTexture.autoGenerateMips = false; - - if (blurShadowTexture == null) { - blurShadowTexture = new RenderTexture(shadowResolution, - shadowResolution, - 24, - renderTextureFormat, - RenderTextureReadWrite.Linear); - blurShadowTexture.filterMode = FilterMode.Bilinear; - } - blurShadowTexture.useMipMap = false; - blurShadowTexture.autoGenerateMips = false; - shadowCamera.targetTexture = shadowTexture; - shadowCamera.nearClipPlane = 0f; - shadowCamera.farClipPlane = shadowDistance; - Shader.SetGlobalTexture( SHADOW_TEX_ID, shadowTexture); - Shader.SetGlobalTexture( SHADOW_BLUR_TEX_ID, blurShadowTexture); +public class Lighting : MonoBehaviour +{ + public RenderTextureFormat renderTextureFormat = RenderTextureFormat.RGFloat; + + public Shader depthReplacementShader; + public Camera shadowCamera; + + // This value needs to be kept in sync with the value in Assets\Rendering\shaderMath.cginc + private const int NUMBER_OF_POINT_LIGHTS = 8; + + // Used to configure lighting within the Unity editor + public Color environmentOverride = new Color(1f, 1f, 1f); + public float environmentOverrideAmount = 0f; + public float environmentSpecularAmount = 1f; + public RenderTexture shadowTexture; + private RenderTexture blurShadowTexture; + + public Cubemap environmentCube; + public float environmentDiffuseStrength = 1f; + public float environmentSpecularStrength = 1f; + + public float shadowDistance = 100; + public Color lightColor = new Color(1f, 1f, 1f); + public float lightStrength = .6f; + public float lightSize = 0.5f; + public Color fillLightColor = new Color(1f, 1f, 1f); + public float fillLightStrength = .6f; + public Color fogColor = new Color(.8f, .7f, .7f); + public float fogStrength = 0.1f; + public Material blurMat; + public bool downsample = false; + public int blurIterations = 0; + const int shadowResolution = 1024; + + public Light[] pointLights = new Light[NUMBER_OF_POINT_LIGHTS]; + + // Index of pointLights that holds the button light effect. + private const int BUTTON_LIGHT_INDEX = 0; + + private Vector4[] lightColors = new Vector4[NUMBER_OF_POINT_LIGHTS]; + private Vector4[] lightPositions = new Vector4[NUMBER_OF_POINT_LIGHTS]; + + // String identifiers for global shader uniforms used for lighting the majority of materials + const string LIGHT_DIRECTION = "_LightDirection"; + const string LIGHT_COLOR = "_LightColor"; + const string LIGHT_POSITION = "_LightPosition"; + const string LIGHT_SIZE = "_LightSize"; + const string CAMERA_SIZE = "_CameraSize"; + const string FILL_LIGHT_COLOR = "_FillLightColor"; + const string FILL_LIGHT_POSITION = "_FillLightPosition"; + const string SHADOW_DISTANCE = "_ShadowInfo"; + const string SHADOW_MATRIX = "_ShadowMatrix"; + const string SHADOW_TEX = "_ShadowTexture"; + const string SHADOW_BLUR_TEX = "_ShadowBlurTexture"; + const string FOG_COLOR = "_FogColor"; + const string FOG_STRENGTH = "_FogStrength"; + const string POINT_LIGHT_POSITIONS = "_PointLightWorldPositions"; + const string POINT_LIGHT_COLORS = "_PointLightColors"; + const string ENV_OVERRIDE = "_EnvOverrideColor"; + const string ENV_OVERRIDE_AMOUNT = "_EnvOverrideAmount"; + const string ENV_SPECULAR_AMOUNT = "_EnvSpecularAmount"; + const string ENV_CUBEMAP = "_EnvCubeMap"; + const string ENV_DIFFUSE_STRENGTH = "_EnvDiffuseStrength"; + const string ENV_SPECULAR_STRENGTH = "_EnvSpecularStrength"; + + // Integer ids for global shader uniforms - it's slightly faster to set values using these. + int LIGHT_DIRECTION_ID; + int LIGHT_COLOR_ID; + int LIGHT_POSITION_ID; + int LIGHT_SIZE_ID; + int CAMERA_SIZE_ID; + int FILL_LIGHT_COLOR_ID; + int FILL_LIGHT_POSITION_ID; + int SHADOW_DISTANCE_ID; + int SHADOW_MATRIX_ID; + int SHADOW_TEX_ID; + int SHADOW_BLUR_TEX_ID; + int FOG_COLOR_ID; + int FOG_STRENGTH_ID; + int POINT_LIGHT_WORLD_POSITIONS_ID; + int POINT_LIGHT_COLORS_ID; + int ENV_OVERRIDE_ID; + int ENV_OVERRIDE_AMOUNT_ID; + int ENV_SPECULAR_AMOUNT_ID; + int ENV_CUBEMAP_ID; + int ENV_DIFFUSE_STRENGTH_ID; + int ENV_SPECULAR_STRENGTH_ID; + + void Start() + { + LIGHT_DIRECTION_ID = Shader.PropertyToID(LIGHT_DIRECTION); + LIGHT_COLOR_ID = Shader.PropertyToID(LIGHT_COLOR); + LIGHT_POSITION_ID = Shader.PropertyToID(LIGHT_POSITION); + LIGHT_SIZE_ID = Shader.PropertyToID(LIGHT_SIZE); + CAMERA_SIZE_ID = Shader.PropertyToID(CAMERA_SIZE); + FILL_LIGHT_COLOR_ID = Shader.PropertyToID(FILL_LIGHT_COLOR); + FILL_LIGHT_POSITION_ID = Shader.PropertyToID(FILL_LIGHT_POSITION); + SHADOW_DISTANCE_ID = Shader.PropertyToID(SHADOW_DISTANCE); + SHADOW_MATRIX_ID = Shader.PropertyToID(SHADOW_MATRIX); + SHADOW_TEX_ID = Shader.PropertyToID(SHADOW_TEX); + SHADOW_BLUR_TEX_ID = Shader.PropertyToID(SHADOW_BLUR_TEX); + FOG_COLOR_ID = Shader.PropertyToID(FOG_COLOR); + FOG_STRENGTH_ID = Shader.PropertyToID(FOG_STRENGTH); + POINT_LIGHT_WORLD_POSITIONS_ID = Shader.PropertyToID(POINT_LIGHT_POSITIONS); + POINT_LIGHT_COLORS_ID = Shader.PropertyToID(POINT_LIGHT_COLORS); + ENV_OVERRIDE_ID = Shader.PropertyToID(ENV_OVERRIDE); + ENV_OVERRIDE_AMOUNT_ID = Shader.PropertyToID(ENV_OVERRIDE_AMOUNT); + ENV_SPECULAR_AMOUNT_ID = Shader.PropertyToID(ENV_SPECULAR_AMOUNT); + ENV_CUBEMAP_ID = Shader.PropertyToID(ENV_CUBEMAP); + ENV_DIFFUSE_STRENGTH_ID = Shader.PropertyToID(ENV_DIFFUSE_STRENGTH); + ENV_SPECULAR_STRENGTH_ID = Shader.PropertyToID(ENV_SPECULAR_STRENGTH); + InitializeShadows(); + UpdateLightPositions(); + Shader.SetGlobalTexture(ENV_CUBEMAP_ID, environmentCube); + } - } - - void Update () { - // Set global lighting information used by all Directional* material shaders. - UpdateLightPositions(); - Shader.SetGlobalVector(LIGHT_DIRECTION_ID, transform.forward); - Shader.SetGlobalVector(LIGHT_POSITION_ID, transform.position); - Shader.SetGlobalFloat(LIGHT_SIZE_ID, lightSize); - Shader.SetGlobalFloat(CAMERA_SIZE_ID, shadowCamera.orthographicSize); - Shader.SetGlobalVector(LIGHT_COLOR_ID, new Vector3(lightStrength * lightColor.r, - lightStrength * lightColor.g, - lightStrength * lightColor.b)); - Shader.SetGlobalVector(FILL_LIGHT_POSITION_ID, transform.position * -1f); - Shader.SetGlobalVector(FILL_LIGHT_COLOR_ID, new Vector3(fillLightStrength * fillLightColor.r, - fillLightStrength * fillLightColor.g, - fillLightStrength * fillLightColor.b)); - Shader.SetGlobalVector(FOG_COLOR_ID, fogColor); - Shader.SetGlobalFloat(FOG_STRENGTH_ID, fogStrength); - - Shader.SetGlobalVector(SHADOW_DISTANCE_ID, new Vector4(1.0f/shadowDistance, shadowResolution, 0f, 0f)); - - Shader.SetGlobalFloat(ENV_OVERRIDE_AMOUNT_ID, environmentOverrideAmount); - Shader.SetGlobalVector(ENV_OVERRIDE_ID, environmentOverride); - Shader.SetGlobalFloat(ENV_SPECULAR_AMOUNT_ID, environmentSpecularAmount); - Shader.SetGlobalFloat(ENV_SPECULAR_STRENGTH_ID, environmentSpecularStrength); - Shader.SetGlobalFloat(ENV_DIFFUSE_STRENGTH_ID, environmentDiffuseStrength); - Matrix4x4 shadowVP = GetComponent().projectionMatrix *GetComponent().worldToCameraMatrix; - Shader.SetGlobalMatrix(SHADOW_MATRIX_ID, shadowVP); - Shader.SetGlobalVectorArray(POINT_LIGHT_WORLD_POSITIONS_ID, lightPositions); - Shader.SetGlobalVectorArray(POINT_LIGHT_COLORS_ID, lightColors); - - } - - void OnDestroy(){ - if(shadowTexture!= null){ - shadowTexture.Release(); + + public void UpdateLightPositions() + { + // Updates array of lighting values from configured Unity point lights. We need to pass the data into the shader in + // this form. + for (int i = 0; i < NUMBER_OF_POINT_LIGHTS; i++) + { + if (pointLights.Length > i && pointLights[i] != null) + { + Light curLight = pointLights[i]; + Vector3 pos = pointLights[i].transform.position; + lightPositions[i] = new Vector4(pos.x, pos.y, pos.z, 1f); + lightColors[i] = new Vector4(curLight.color.r, curLight.color.g, curLight.color.b, curLight.intensity); + } + else + { + lightPositions[i] = Vector4.zero; + lightColors[i] = new Vector4(0f, 0f, 0f, 0f); + } + } } - } - void OnPostRender(){ - - int res = shadowResolution; - if(downsample){ - res = shadowResolution / 2; + void InitializeShadows() + { + if (shadowCamera != null) + { + shadowCamera.SetReplacementShader(depthReplacementShader, "RenderType"); + if (shadowTexture == null) + { + shadowTexture = new RenderTexture(shadowResolution, + shadowResolution, + 32, + renderTextureFormat, + RenderTextureReadWrite.Linear); + shadowTexture.useMipMap = false; + shadowTexture.autoGenerateMips = false; + shadowTexture.filterMode = FilterMode.Point; + shadowTexture.wrapMode = TextureWrapMode.Clamp; + } + shadowTexture.useMipMap = false; + shadowTexture.autoGenerateMips = false; + + if (blurShadowTexture == null) + { + blurShadowTexture = new RenderTexture(shadowResolution, + shadowResolution, + 24, + renderTextureFormat, + RenderTextureReadWrite.Linear); + blurShadowTexture.filterMode = FilterMode.Bilinear; + } + blurShadowTexture.useMipMap = false; + blurShadowTexture.autoGenerateMips = false; + shadowCamera.targetTexture = shadowTexture; + shadowCamera.nearClipPlane = 0f; + shadowCamera.farClipPlane = shadowDistance; + Shader.SetGlobalTexture(SHADOW_TEX_ID, shadowTexture); + Shader.SetGlobalTexture(SHADOW_BLUR_TEX_ID, blurShadowTexture); + } } - RenderTexture rtQuarter = - RenderTexture.GetTemporary(res,res,0, renderTextureFormat, RenderTextureReadWrite.Linear); - RenderTexture rtQuarterB = - RenderTexture.GetTemporary(res,res,0, renderTextureFormat, RenderTextureReadWrite.Linear); + void Update() + { + // Set global lighting information used by all Directional* material shaders. + UpdateLightPositions(); + Shader.SetGlobalVector(LIGHT_DIRECTION_ID, transform.forward); + Shader.SetGlobalVector(LIGHT_POSITION_ID, transform.position); + Shader.SetGlobalFloat(LIGHT_SIZE_ID, lightSize); + Shader.SetGlobalFloat(CAMERA_SIZE_ID, shadowCamera.orthographicSize); + Shader.SetGlobalVector(LIGHT_COLOR_ID, new Vector3(lightStrength * lightColor.r, + lightStrength * lightColor.g, + lightStrength * lightColor.b)); + Shader.SetGlobalVector(FILL_LIGHT_POSITION_ID, transform.position * -1f); + Shader.SetGlobalVector(FILL_LIGHT_COLOR_ID, new Vector3(fillLightStrength * fillLightColor.r, + fillLightStrength * fillLightColor.g, + fillLightStrength * fillLightColor.b)); + Shader.SetGlobalVector(FOG_COLOR_ID, fogColor); + Shader.SetGlobalFloat(FOG_STRENGTH_ID, fogStrength); - Graphics.Blit(shadowTexture, rtQuarter, blurMat, 0); + Shader.SetGlobalVector(SHADOW_DISTANCE_ID, new Vector4(1.0f / shadowDistance, shadowResolution, 0f, 0f)); + + Shader.SetGlobalFloat(ENV_OVERRIDE_AMOUNT_ID, environmentOverrideAmount); + Shader.SetGlobalVector(ENV_OVERRIDE_ID, environmentOverride); + Shader.SetGlobalFloat(ENV_SPECULAR_AMOUNT_ID, environmentSpecularAmount); + Shader.SetGlobalFloat(ENV_SPECULAR_STRENGTH_ID, environmentSpecularStrength); + Shader.SetGlobalFloat(ENV_DIFFUSE_STRENGTH_ID, environmentDiffuseStrength); + Matrix4x4 shadowVP = GetComponent().projectionMatrix * GetComponent().worldToCameraMatrix; + Shader.SetGlobalMatrix(SHADOW_MATRIX_ID, shadowVP); + Shader.SetGlobalVectorArray(POINT_LIGHT_WORLD_POSITIONS_ID, lightPositions); + Shader.SetGlobalVectorArray(POINT_LIGHT_COLORS_ID, lightColors); + + } - for(int i = 0; i < blurIterations; i++){ - Graphics.Blit(rtQuarter, rtQuarterB, blurMat, 3); - Graphics.Blit(rtQuarterB, rtQuarter, blurMat, 4); + void OnDestroy() + { + if (shadowTexture != null) + { + shadowTexture.Release(); + } } - Graphics.Blit(rtQuarter, blurShadowTexture); + void OnPostRender() + { - rtQuarter.DiscardContents(); - RenderTexture.ReleaseTemporary(rtQuarter); + int res = shadowResolution; + if (downsample) + { + res = shadowResolution / 2; + } - rtQuarterB.DiscardContents(); - RenderTexture.ReleaseTemporary(rtQuarterB); - } + RenderTexture rtQuarter = + RenderTexture.GetTemporary(res, res, 0, renderTextureFormat, RenderTextureReadWrite.Linear); + RenderTexture rtQuarterB = + RenderTexture.GetTemporary(res, res, 0, renderTextureFormat, RenderTextureReadWrite.Linear); + + Graphics.Blit(shadowTexture, rtQuarter, blurMat, 0); + + for (int i = 0; i < blurIterations; i++) + { + Graphics.Blit(rtQuarter, rtQuarterB, blurMat, 3); + Graphics.Blit(rtQuarterB, rtQuarter, blurMat, 4); + } + + Graphics.Blit(rtQuarter, blurShadowTexture); + + rtQuarter.DiscardContents(); + RenderTexture.ReleaseTemporary(rtQuarter); + + rtQuarterB.DiscardContents(); + RenderTexture.ReleaseTemporary(rtQuarterB); + } } diff --git a/Assets/Scripts/HintRope.cs b/Assets/Scripts/HintRope.cs index c5555102..f7a5bfac 100644 --- a/Assets/Scripts/HintRope.cs +++ b/Assets/Scripts/HintRope.cs @@ -21,187 +21,210 @@ /// For each item that is selcted, draws a stylized line between it and the destination position /// while enforcing rules of connection such as distance dropoff thresholding. /// -public class HintRope : MonoBehaviour { - - /// - /// Where the gameobject the strings will point to for each mesh when the rope is drawn. - /// - public GameObject destination; - /// - /// The root value for the distance threshold when determining whether to render the rope, or not. - /// - public float distThresh = .25f; - /// - /// The width of the rope - this is fed to the LineRenderer. - /// - public float hintWidth = .001f; - /// - /// Start color to interpolate for LineRenderer. - /// - public Color startColor = new Color(233f/255f, 249f/255f, 255f/255f, 50f/255f); - /// - /// End color to interpolate for LineRenderer. - /// - public Color endColor = new Color(1f, 1f, 1f, 155f/255f); - - /// - /// The start time for the hint, used for animations. - /// - private float hintStartTime = 0.0f; - /// - /// Lookup for our currently rendered rope and the associated meshId. - /// - private Dictionary ropes = new Dictionary(); - /// - /// Reference to the groupButton which we use to calculate the default destination, when no destination is given. - /// - private Transform groupButton; - /// - /// Material for LineRenderer - White / Diffuse. - /// - private Material whiteDiffuseMat; - - void Start () { - if (!Features.showMultiselectTooltip) { - Destroy(this); - return; - } - whiteDiffuseMat = new Material(Shader.Find("Particles/Additive")); - } - - /// - /// - Draw the hint rope for each selected meash that is within the calculated distance threshold. - /// - Remove the hint ropes that do not meet the distance threshold. - /// - void Update () { - if (PeltzerMain.Instance.GetMover().userHasPerformedGroupAction) { - // destroy self - DestroySelf(); - return; +public class HintRope : MonoBehaviour +{ + + /// + /// Where the gameobject the strings will point to for each mesh when the rope is drawn. + /// + public GameObject destination; + /// + /// The root value for the distance threshold when determining whether to render the rope, or not. + /// + public float distThresh = .25f; + /// + /// The width of the rope - this is fed to the LineRenderer. + /// + public float hintWidth = .001f; + /// + /// Start color to interpolate for LineRenderer. + /// + public Color startColor = new Color(233f / 255f, 249f / 255f, 255f / 255f, 50f / 255f); + /// + /// End color to interpolate for LineRenderer. + /// + public Color endColor = new Color(1f, 1f, 1f, 155f / 255f); + + /// + /// The start time for the hint, used for animations. + /// + private float hintStartTime = 0.0f; + /// + /// Lookup for our currently rendered rope and the associated meshId. + /// + private Dictionary ropes = new Dictionary(); + /// + /// Reference to the groupButton which we use to calculate the default destination, when no destination is given. + /// + private Transform groupButton; + /// + /// Material for LineRenderer - White / Diffuse. + /// + private Material whiteDiffuseMat; + + void Start() + { + if (!Features.showMultiselectTooltip) + { + Destroy(this); + return; + } + whiteDiffuseMat = new Material(Shader.Find("Particles/Additive")); } + /// + /// - Draw the hint rope for each selected meash that is within the calculated distance threshold. + /// - Remove the hint ropes that do not meet the distance threshold. + /// + void Update() + { + if (PeltzerMain.Instance.GetMover().userHasPerformedGroupAction) + { + // destroy self + DestroySelf(); + return; + } + // Get the reference to the center of the icon over the appMenuButton // as it has the correct registration point and appMenuButton does not. destination = PeltzerMain.Instance.peltzerController.controllerGeometry.groupButtonIcon; - int selectedCount = PeltzerMain.Instance.GetSelector().selectedMeshes.Count; - - // Nothing to do, return. - if (selectedCount == 0) { - PruneRopes(); - return; - } - - // Prune ropes to remove invalid hint ropes. - PruneRopes(); - - if (selectedCount > 1) { - foreach (int meshId in PeltzerMain.Instance.GetSelector().selectedMeshes) { - GameObject go; - LineRenderer lineRenderer; - MMesh mesh = PeltzerMain.Instance.model.GetMesh(meshId); - Vector3 sourcePos = PeltzerMain.Instance.worldSpace - .ModelToWorld(mesh.bounds.center); - - float meshBoundsMaxEstimate = GetMaxElement(mesh.bounds.size); - - // If meshId not found, add entry. - if (!ropes.TryGetValue(meshId, out go)) { + int selectedCount = PeltzerMain.Instance.GetSelector().selectedMeshes.Count; - // Calculate distance threshold. - float dist = Vector3.Distance(sourcePos, destination.transform.position); - - if (dist > (distThresh + meshBoundsMaxEstimate) * PeltzerMain.Instance.worldSpace.scale) { - continue; - } - - // Create a GameObject instance to attach the line renderer. - go = new GameObject("hint_rope"); - go.transform.position = destination.transform.position; + // Nothing to do, return. + if (selectedCount == 0) + { + PruneRopes(); + return; + } - // Create a Line Renderer and attach to the gameobject. - lineRenderer = go.AddComponent(); - lineRenderer.startWidth = hintWidth; - lineRenderer.endWidth = hintWidth; + // Prune ropes to remove invalid hint ropes. + PruneRopes(); + + if (selectedCount > 1) + { + foreach (int meshId in PeltzerMain.Instance.GetSelector().selectedMeshes) + { + GameObject go; + LineRenderer lineRenderer; + MMesh mesh = PeltzerMain.Instance.model.GetMesh(meshId); + Vector3 sourcePos = PeltzerMain.Instance.worldSpace + .ModelToWorld(mesh.bounds.center); + + float meshBoundsMaxEstimate = GetMaxElement(mesh.bounds.size); + + // If meshId not found, add entry. + if (!ropes.TryGetValue(meshId, out go)) + { + + // Calculate distance threshold. + float dist = Vector3.Distance(sourcePos, destination.transform.position); + + if (dist > (distThresh + meshBoundsMaxEstimate) * PeltzerMain.Instance.worldSpace.scale) + { + continue; + } + + // Create a GameObject instance to attach the line renderer. + go = new GameObject("hint_rope"); + go.transform.position = destination.transform.position; + + // Create a Line Renderer and attach to the gameobject. + lineRenderer = go.AddComponent(); + lineRenderer.startWidth = hintWidth; + lineRenderer.endWidth = hintWidth; + + // Set the material on the line renderer. + lineRenderer.material = whiteDiffuseMat; + lineRenderer.startColor = startColor; + lineRenderer.endColor = endColor; + + // Add the gameobject reference to ropes. + ropes.Add(meshId, go); + } + else + { + // Get reference to line renderer + lineRenderer = go.GetComponent(); + } + + // Update source position - [0]. + lineRenderer.SetPosition(0, sourcePos); + + // Update destination position - [1]. + lineRenderer.SetPosition(1, destination.transform.position); + } + } + } - // Set the material on the line renderer. - lineRenderer.material = whiteDiffuseMat; - lineRenderer.startColor = startColor; - lineRenderer.endColor = endColor; + /// + /// Determine which ropes are not relevant and remove them. + /// + private void PruneRopes() + { + // Nothing to prune, return; + if (ropes.Count == 0) return; + + List pruneList = new List(); + foreach (KeyValuePair rope in ropes) + { + // Mark rope for pruning if no longer selected. + if (!PeltzerMain.Instance.GetSelector().selectedMeshes.Contains(rope.Key)) + { + pruneList.Add(rope.Key); + } + else + { + // Calculate distance threshold. + Vector3 sourcePos = rope.Value.GetComponent().GetPosition(0); + float dist = Vector3.Distance(sourcePos, destination.transform.position); + float meshBoundsMaxEstimate = GetMaxElement(PeltzerMain.Instance.model.GetMesh(rope.Key).bounds.size); + + // Mark rope for pruning if distance is too great. + if (dist > (distThresh + meshBoundsMaxEstimate) * PeltzerMain.Instance.worldSpace.scale) + { + pruneList.Add(rope.Key); + } + } + } - // Add the gameobject reference to ropes. - ropes.Add(meshId, go); - } else { - // Get reference to line renderer - lineRenderer = go.GetComponent(); + // Prune! + foreach (int key in pruneList) + { + DestroyHintRope(key); } - - // Update source position - [0]. - lineRenderer.SetPosition(0, sourcePos); + } - // Update destination position - [1]. - lineRenderer.SetPosition(1, destination.transform.position); - } + /// + /// Destroy the rope associated with meshId + /// + /// The mesh id of the assocated rope. + private void DestroyHintRope(int meshId) + { + // Destroy the GameObject holding the LineRenderer. + Destroy(ropes[meshId]); + ropes.Remove(meshId); } - } - - /// - /// Determine which ropes are not relevant and remove them. - /// - private void PruneRopes() { - // Nothing to prune, return; - if (ropes.Count == 0) return; - - List pruneList = new List(); - foreach (KeyValuePair rope in ropes) { - // Mark rope for pruning if no longer selected. - if ( !PeltzerMain.Instance.GetSelector().selectedMeshes.Contains(rope.Key)) { - pruneList.Add(rope.Key); - } else { - // Calculate distance threshold. - Vector3 sourcePos = rope.Value.GetComponent().GetPosition(0); - float dist = Vector3.Distance(sourcePos, destination.transform.position); - float meshBoundsMaxEstimate = GetMaxElement(PeltzerMain.Instance.model.GetMesh(rope.Key).bounds.size); - - // Mark rope for pruning if distance is too great. - if (dist > (distThresh + meshBoundsMaxEstimate) * PeltzerMain.Instance.worldSpace.scale) { - pruneList.Add(rope.Key); + + /// + /// Destroy the script. + /// + private void DestroySelf() + { + foreach (KeyValuePair rope in ropes) + { + Destroy(rope.Value); } - } + Destroy(this); } - // Prune! - foreach (int key in pruneList) { - DestroyHintRope(key); - } - } - - /// - /// Destroy the rope associated with meshId - /// - /// The mesh id of the assocated rope. - private void DestroyHintRope(int meshId) { - // Destroy the GameObject holding the LineRenderer. - Destroy(ropes[meshId]); - ropes.Remove(meshId); - } - - /// - /// Destroy the script. - /// - private void DestroySelf() { - foreach (KeyValuePair rope in ropes) { - Destroy(rope.Value); + /// + /// Convenience method for getting the max of three floats stored in a Vector3. + /// + /// Vector3 holding the 3 values from which we want to find the max. + /// + private float GetMaxElement(Vector3 v3) + { + return Mathf.Max(Mathf.Max(v3.x, v3.y), v3.z); } - Destroy(this); - } - - /// - /// Convenience method for getting the max of three floats stored in a Vector3. - /// - /// Vector3 holding the 3 values from which we want to find the max. - /// - private float GetMaxElement(Vector3 v3) { - return Mathf.Max(Mathf.Max(v3.x, v3.y), v3.z); - } } diff --git a/Assets/Scripts/ParticleSystemDestroy.cs b/Assets/Scripts/ParticleSystemDestroy.cs index 61c46747..45d1a3b2 100644 --- a/Assets/Scripts/ParticleSystemDestroy.cs +++ b/Assets/Scripts/ParticleSystemDestroy.cs @@ -20,10 +20,12 @@ /// Simple script to attach to Particle System prefabs that get instantiated /// during runtime so they destroy themselves after their duration elapses. /// -public class ParticleSystemDestroy : MonoBehaviour { +public class ParticleSystemDestroy : MonoBehaviour +{ + + void Start() + { + Destroy(this.gameObject, this.GetComponent().main.duration); + } - void Start () { - Destroy(this.gameObject, this.GetComponent().main.duration); - } - } diff --git a/Assets/Scripts/PolyWorldBounds.cs b/Assets/Scripts/PolyWorldBounds.cs index d6fdde9c..dbea02ec 100644 --- a/Assets/Scripts/PolyWorldBounds.cs +++ b/Assets/Scripts/PolyWorldBounds.cs @@ -19,18 +19,21 @@ using com.google.apps.peltzer.client.tools.utils; using UnityEngine; -public class PolyWorldBounds : MonoBehaviour { +public class PolyWorldBounds : MonoBehaviour +{ - private GameObject gridPlanes; + private GameObject gridPlanes; - // Use this for initialization - void Start () { - gridPlanes = transform.Find("GridPlanes").gameObject; - HandleFeatureToggle(); - } + // Use this for initialization + void Start() + { + gridPlanes = transform.Find("GridPlanes").gameObject; + HandleFeatureToggle(); + } - public void HandleFeatureToggle() { - Debug.Log(Features.enableWorldSpaceGridPlanes); - gridPlanes.SetActive(Features.enableWorldSpaceGridPlanes); - } + public void HandleFeatureToggle() + { + Debug.Log(Features.enableWorldSpaceGridPlanes); + gridPlanes.SetActive(Features.enableWorldSpaceGridPlanes); + } } diff --git a/Assets/Scripts/TextureScale.cs b/Assets/Scripts/TextureScale.cs index 4a6713c3..738fdffb 100644 --- a/Assets/Scripts/TextureScale.cs +++ b/Assets/Scripts/TextureScale.cs @@ -20,143 +20,168 @@ using System.Threading; using UnityEngine; -namespace TiltBrush { - -public class TextureScale { - public class SharedData { - // mutable - public int finishCount; - // read only - public Mutex mutex; - public Color32[] newColors; - public Color32[] texColors; - public int texWidth; - public int newWidth; - public float ratioX; - public float ratioY; - - } - public class ThreadData { - // read only - public SharedData shared; - public int start; - public int end; - } - - /// Like Bilinear(), but with an easier interface. - /// Blocking, runs on current thread. - /// newColors may be null. - /// Return value may reuse texColors or newColors (or neither!) - public static Color32[] BlockingBilinear( - Color32[] texColors, int texWidth, int texHeight, - Color32[] newColors, int newWidth, int newHeight) { - if (texWidth == newWidth && texHeight == newHeight) { - return texColors; - } - if (newColors == null) { - newColors = new Color32[newWidth * newHeight]; - } +namespace TiltBrush +{ - unsafe { - Bilinear(texColors, texWidth, texHeight, - newColors, newWidth, newHeight); - } - return newColors; - } - - // Helper for Bilinear() - // Returns a value up to 1 unit closer to desired (in log2 space) - static int moveTowardsLog(int start, int desired) { - if (start * desired <= 0) { - throw new System.ArgumentException("Signs must be the same; 0 not allowed."); - } - // Want the non-multiple of two to be at the upper end of the progression, - // for better filtering. But this is easier to write. - if (desired < start) { - int temp = start >> 1; - return (temp < desired ? desired : temp); - } else { - long temp = start << 1; - return (temp > desired ? desired : (int)temp); - } - } - - - // Helper to deal with manual buffer management. - // The repeated bilinear filtering seems to be causing excessive garbage - // to stick around. - unsafe struct ColorBuffer { - IntPtr dealloc; // Pointer to deallocate, or 0 if not explicitly allocated - public Color32* array; - public int length; // number of elements in array - public int width, height; // 2D size (product must be <= length) - - // no allocation; pointer comes from a [] - public ColorBuffer(Color32[] c, Color32* pc, int width_, int height_) { - width = width_; - height = height_; - length = c.Length; - dealloc = IntPtr.Zero; - array = pc; - } + public class TextureScale + { + public class SharedData + { + // mutable + public int finishCount; + // read only + public Mutex mutex; + public Color32[] newColors; + public Color32[] texColors; + public int texWidth; + public int newWidth; + public float ratioX; + public float ratioY; - // allocate non-garbage-collected memory - public ColorBuffer(int width_, int height_) { - width = width_; - height = height_; - length = width * height; - dealloc = System.Runtime.InteropServices.Marshal.AllocHGlobal(length * sizeof(Color32)); - array = (Color32*)dealloc; - } + } + public class ThreadData + { + // read only + public SharedData shared; + public int start; + public int end; + } - public void Deallocate() { - if (dealloc != IntPtr.Zero) { - System.Runtime.InteropServices.Marshal.FreeHGlobal(dealloc); - dealloc = IntPtr.Zero; - array = null; - length = width = height = 0; - } - } - }; - - /// Simplified to be single-threaded and blocking - private unsafe static void Bilinear( - Color32[] texColors, int texWidth, int texHeight, - Color32[] newColors, int newWidth, int newHeight) { - // A single pass of bilinear filtering blends 4 pixels together, - // so don't try to reduce by more than a factor of 2 per iteration - Debug.Assert(newWidth * newHeight == newColors.Length); - - fixed (Color32* pTex = texColors) { - ColorBuffer cur = new ColorBuffer(texColors, pTex, texWidth, texHeight); - while (true) { - int tmpWidth = moveTowardsLog(cur.width, newWidth); - int tmpHeight = moveTowardsLog(cur.height, newHeight); - if (newColors.Length == tmpWidth * tmpHeight) { - fixed (Color32* pNew = newColors) { - SinglePassBilinear(cur.array, cur.length, cur.width, cur.height, - pNew, newColors.Length, newWidth, newHeight); - } - return; + /// Like Bilinear(), but with an easier interface. + /// Blocking, runs on current thread. + /// newColors may be null. + /// Return value may reuse texColors or newColors (or neither!) + public static Color32[] BlockingBilinear( + Color32[] texColors, int texWidth, int texHeight, + Color32[] newColors, int newWidth, int newHeight) + { + if (texWidth == newWidth && texHeight == newHeight) + { + return texColors; + } + if (newColors == null) + { + newColors = new Color32[newWidth * newHeight]; + } + + unsafe + { + Bilinear(texColors, texWidth, texHeight, + newColors, newWidth, newHeight); + } + return newColors; } - ColorBuffer tmp = new ColorBuffer(tmpWidth, tmpHeight); - SinglePassBilinear(cur.array, cur.length, cur.width, cur.height, - tmp.array, tmp.length, tmp.width, tmp.height); - cur.Deallocate(); - cur = tmp; - } - } - } - - /// Unthreaded single pass bilinear filter - private static unsafe void SinglePassBilinear( - Color32* texColors, int texLen, int texWidth, int texHeight, - Color32* newColors, int newLen, int newWidth, int newHeight) { - if (newLen < newWidth * newHeight) { - Debug.Assert(newLen >= newWidth * newHeight); - return; - } + // Helper for Bilinear() + // Returns a value up to 1 unit closer to desired (in log2 space) + static int moveTowardsLog(int start, int desired) + { + if (start * desired <= 0) + { + throw new System.ArgumentException("Signs must be the same; 0 not allowed."); + } + // Want the non-multiple of two to be at the upper end of the progression, + // for better filtering. But this is easier to write. + if (desired < start) + { + int temp = start >> 1; + return (temp < desired ? desired : temp); + } + else + { + long temp = start << 1; + return (temp > desired ? desired : (int)temp); + } + } + + + // Helper to deal with manual buffer management. + // The repeated bilinear filtering seems to be causing excessive garbage + // to stick around. + unsafe struct ColorBuffer + { + IntPtr dealloc; // Pointer to deallocate, or 0 if not explicitly allocated + public Color32* array; + public int length; // number of elements in array + public int width, height; // 2D size (product must be <= length) + + // no allocation; pointer comes from a [] + public ColorBuffer(Color32[] c, Color32* pc, int width_, int height_) + { + width = width_; + height = height_; + length = c.Length; + dealloc = IntPtr.Zero; + array = pc; + } + + // allocate non-garbage-collected memory + public ColorBuffer(int width_, int height_) + { + width = width_; + height = height_; + length = width * height; + dealloc = System.Runtime.InteropServices.Marshal.AllocHGlobal(length * sizeof(Color32)); + array = (Color32*)dealloc; + } + + public void Deallocate() + { + if (dealloc != IntPtr.Zero) + { + System.Runtime.InteropServices.Marshal.FreeHGlobal(dealloc); + dealloc = IntPtr.Zero; + array = null; + length = width = height = 0; + } + } + }; + + /// Simplified to be single-threaded and blocking + private unsafe static void Bilinear( + Color32[] texColors, int texWidth, int texHeight, + Color32[] newColors, int newWidth, int newHeight) + { + // A single pass of bilinear filtering blends 4 pixels together, + // so don't try to reduce by more than a factor of 2 per iteration + Debug.Assert(newWidth * newHeight == newColors.Length); + + fixed (Color32* pTex = texColors) + { + ColorBuffer cur = new ColorBuffer(texColors, pTex, texWidth, texHeight); + while (true) + { + int tmpWidth = moveTowardsLog(cur.width, newWidth); + int tmpHeight = moveTowardsLog(cur.height, newHeight); + if (newColors.Length == tmpWidth * tmpHeight) + { + fixed (Color32* pNew = newColors) + { + SinglePassBilinear(cur.array, cur.length, cur.width, cur.height, + pNew, newColors.Length, newWidth, newHeight); + } + return; + } + + ColorBuffer tmp = new ColorBuffer(tmpWidth, tmpHeight); + SinglePassBilinear(cur.array, cur.length, cur.width, cur.height, + tmp.array, tmp.length, tmp.width, tmp.height); + cur.Deallocate(); + cur = tmp; + } + } + } + + /// Unthreaded single pass bilinear filter + private static unsafe void SinglePassBilinear( + Color32* texColors, int texLen, int texWidth, int texHeight, + Color32* newColors, int newLen, int newWidth, int newHeight) + { + if (newLen < newWidth * newHeight) + { + Debug.Assert(newLen >= newWidth * newHeight); + return; + } #if false // For reference, this is the point-sampled loop @@ -171,51 +196,58 @@ private static unsafe void SinglePassBilinear( } #endif - float ratioX = 1.0f / ((float)newWidth / (texWidth - 1)); - float ratioY = 1.0f / ((float)newHeight / (texHeight - 1)); - { - var w = texWidth; - var w2 = newWidth; - - for (int y = 0; y < newHeight; y++) { - int yFloor = (int)Mathf.Floor(y * ratioY); - var y1 = yFloor * w; - var y2 = (yFloor + 1) * w; - var yw = y * w2; - - for (int x = 0; x < w2; x++) { - int xFloor = (int)Mathf.Floor(x * ratioX); - var xLerp = x * ratioX - xFloor; - newColors[yw + x] = ColorLerpUnclamped( - ColorLerpUnclamped(texColors[y1 + xFloor], texColors[y1 + xFloor + 1], xLerp), - ColorLerpUnclamped(texColors[y2 + xFloor], texColors[y2 + xFloor + 1], xLerp), - y * ratioY - yFloor); + float ratioX = 1.0f / ((float)newWidth / (texWidth - 1)); + float ratioY = 1.0f / ((float)newHeight / (texHeight - 1)); + { + var w = texWidth; + var w2 = newWidth; + + for (int y = 0; y < newHeight; y++) + { + int yFloor = (int)Mathf.Floor(y * ratioY); + var y1 = yFloor * w; + var y2 = (yFloor + 1) * w; + var yw = y * w2; + + for (int x = 0; x < w2; x++) + { + int xFloor = (int)Mathf.Floor(x * ratioX); + var xLerp = x * ratioX - xFloor; + newColors[yw + x] = ColorLerpUnclamped( + ColorLerpUnclamped(texColors[y1 + xFloor], texColors[y1 + xFloor + 1], xLerp), + ColorLerpUnclamped(texColors[y2 + xFloor], texColors[y2 + xFloor + 1], xLerp), + y * ratioY - yFloor); + } + } + } + } + + // + // Safe Compress will only compress pow of 2 textures + // Non pow 2 textures get extremely strange mipmap artifacts when compressed + // If we run into memory issues here, we may need to convert all incoming textures textures to pow of 2 + // But if we do that we need to save the image aspect ratio before compression for reference Images + // + public static bool SafeCompress(Texture2D tex, bool highQuality = false) + { + if (Mathf.IsPowerOfTwo(tex.width) && Mathf.IsPowerOfTwo(tex.height)) + { + tex.Compress(highQuality); + return true; + } + else + { + return false; + } + } + + private static Color32 ColorLerpUnclamped(Color32 c1, Color32 c2, float value) + { + return new Color32( + (byte)(c1.r + (c2.r - c1.r) * value), + (byte)(c1.g + (c2.g - c1.g) * value), + (byte)(c1.b + (c2.b - c1.b) * value), + (byte)(c1.a + (c2.a - c1.a) * value)); } - } - } - } - - // - // Safe Compress will only compress pow of 2 textures - // Non pow 2 textures get extremely strange mipmap artifacts when compressed - // If we run into memory issues here, we may need to convert all incoming textures textures to pow of 2 - // But if we do that we need to save the image aspect ratio before compression for reference Images - // - public static bool SafeCompress(Texture2D tex, bool highQuality = false) { - if (Mathf.IsPowerOfTwo(tex.width) && Mathf.IsPowerOfTwo(tex.height)) { - tex.Compress(highQuality); - return true; - } else { - return false; } - } - - private static Color32 ColorLerpUnclamped(Color32 c1, Color32 c2, float value) { - return new Color32( - (byte)(c1.r + (c2.r - c1.r) * value), - (byte)(c1.g + (c2.g - c1.g) * value), - (byte)(c1.b + (c2.b - c1.b) * value), - (byte)(c1.a + (c2.a - c1.a) * value)); - } -} } // namespace TiltBrush diff --git a/Assets/Scripts/ThirdParty/GlTF/BoundsDouble.cs b/Assets/Scripts/ThirdParty/GlTF/BoundsDouble.cs index 6b67aac9..369259dc 100644 --- a/Assets/Scripts/ThirdParty/GlTF/BoundsDouble.cs +++ b/Assets/Scripts/ThirdParty/GlTF/BoundsDouble.cs @@ -2,113 +2,133 @@ using System; using System.Collections; -public class BoundsDouble { - private double[] min = new double[] { 0, 0, 0 }; - private double[] max = new double[] { 0, 0, 0 }; - private bool inited = false; - - public BoundsDouble() { - } - - public BoundsDouble(BoundsDouble b) { - for (int i = 0; i < 3; ++i) { - min[i] = b.min[i]; - max[i] = b.max[i]; +public class BoundsDouble +{ + private double[] min = new double[] { 0, 0, 0 }; + private double[] max = new double[] { 0, 0, 0 }; + private bool inited = false; + + public BoundsDouble() + { } - inited = true; - } - - public BoundsDouble(double[] min, double[] max) { - this.min[0] = min[0]; - this.min[1] = min[1]; - this.min[2] = min[2]; - - this.max[0] = max[0]; - this.max[1] = max[1]; - this.max[2] = max[2]; - inited = true; - } - - public BoundsDouble(Vector3 min, Vector3 max) { - this.min[0] = min.x; - this.min[1] = min.y; - this.min[2] = min.z; - - this.max[0] = max.x; - this.max[1] = max.y; - this.max[2] = max.z; - inited = true; - } - - public void Encapsulate(BoundsDouble b) { - if (inited) { - min[0] = Math.Min(min[0], b.min[0]); - min[1] = Math.Min(min[1], b.min[1]); - min[2] = Math.Min(min[2], b.min[2]); - - max[0] = Math.Max(max[0], b.max[0]); - max[1] = Math.Max(max[1], b.max[1]); - max[2] = Math.Max(max[2], b.max[2]); - } else { - min[0] = b.min[0]; - min[1] = b.min[1]; - min[2] = b.min[2]; - - max[0] = b.max[0]; - max[1] = b.max[1]; - max[2] = b.max[2]; - inited = true; + + public BoundsDouble(BoundsDouble b) + { + for (int i = 0; i < 3; ++i) + { + min[i] = b.min[i]; + max[i] = b.max[i]; + } + inited = true; } - } - public void Rotate(Matrix4x4 m) { - if (inited) { - double minx = m.m00 * min[0] + m.m01 * min[1] + m.m02 * min[2]; - double miny = m.m10 * min[0] + m.m11 * min[1] + m.m12 * min[2]; - double minz = m.m20 * min[0] + m.m21 * min[1] + m.m22 * min[2]; + public BoundsDouble(double[] min, double[] max) + { + this.min[0] = min[0]; + this.min[1] = min[1]; + this.min[2] = min[2]; - double maxx = m.m00 * max[0] + m.m01 * max[1] + m.m02 * max[2]; - double maxy = m.m10 * max[0] + m.m11 * max[1] + m.m12 * max[2]; - double maxz = m.m20 * max[0] + m.m21 * max[1] + m.m22 * max[2]; + this.max[0] = max[0]; + this.max[1] = max[1]; + this.max[2] = max[2]; + inited = true; + } - min[0] = Math.Min(minx, maxx); - min[1] = Math.Min(miny, maxy); - min[2] = Math.Min(minz, maxz); + public BoundsDouble(Vector3 min, Vector3 max) + { + this.min[0] = min.x; + this.min[1] = min.y; + this.min[2] = min.z; - max[0] = Math.Max(minx, maxx); - max[1] = Math.Max(miny, maxy); - max[2] = Math.Max(minz, maxz); + this.max[0] = max.x; + this.max[1] = max.y; + this.max[2] = max.z; + inited = true; } - } - public void Translate(double x, double y, double z) { - if (inited) { - min[0] += x; - min[1] += y; - min[2] += z; + public void Encapsulate(BoundsDouble b) + { + if (inited) + { + min[0] = Math.Min(min[0], b.min[0]); + min[1] = Math.Min(min[1], b.min[1]); + min[2] = Math.Min(min[2], b.min[2]); + + max[0] = Math.Max(max[0], b.max[0]); + max[1] = Math.Max(max[1], b.max[1]); + max[2] = Math.Max(max[2], b.max[2]); + } + else + { + min[0] = b.min[0]; + min[1] = b.min[1]; + min[2] = b.min[2]; + + max[0] = b.max[0]; + max[1] = b.max[1]; + max[2] = b.max[2]; + inited = true; + } + } + + public void Rotate(Matrix4x4 m) + { + if (inited) + { + double minx = m.m00 * min[0] + m.m01 * min[1] + m.m02 * min[2]; + double miny = m.m10 * min[0] + m.m11 * min[1] + m.m12 * min[2]; + double minz = m.m20 * min[0] + m.m21 * min[1] + m.m22 * min[2]; + + double maxx = m.m00 * max[0] + m.m01 * max[1] + m.m02 * max[2]; + double maxy = m.m10 * max[0] + m.m11 * max[1] + m.m12 * max[2]; + double maxz = m.m20 * max[0] + m.m21 * max[1] + m.m22 * max[2]; + + min[0] = Math.Min(minx, maxx); + min[1] = Math.Min(miny, maxy); + min[2] = Math.Min(minz, maxz); + + max[0] = Math.Max(minx, maxx); + max[1] = Math.Max(miny, maxy); + max[2] = Math.Max(minz, maxz); + } + } - max[0] += x; - max[1] += y; - max[2] += z; + public void Translate(double x, double y, double z) + { + if (inited) + { + min[0] += x; + min[1] += y; + min[2] += z; + + max[0] += x; + max[1] += y; + max[2] += z; + } } - } - public bool Empty { - get { - return !inited; + public bool Empty + { + get + { + return !inited; + } } - } - public double[] Min { - get { - return min; + public double[] Min + { + get + { + return min; + } } - } - public double[] Max { - get { - return max; + public double[] Max + { + get + { + return max; + } } - } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Accessor.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Accessor.cs index e5709748..b8b7442f 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Accessor.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Accessor.cs @@ -3,293 +3,339 @@ using System.Collections.Generic; using System.Linq; -public class GlTF_Accessor : GlTF_Writer { - public enum Type { - SCALAR, - VEC2, - VEC3, - VEC4 - } - - public enum ComponentType { - USHORT = 5123, - FLOAT = 5126 - } - - public GlTF_BufferView bufferView;// "bufferView": "bufferView_30", - public long byteOffset; //": 0, - public int byteStride;// ": 12, - // GL enum vals ": BYTE (5120), UNSIGNED_BYTE (5121), SHORT (5122), UNSIGNED_SHORT (5123), FLOAT - // (5126) - public ComponentType componentType; - public int count;//": 2399, - public Type type = Type.SCALAR; - - public Vector4 maxFloat; - public Vector4 minFloat; - public int minInt; - public int maxInt; - - public GlTF_Accessor(string n) { - name = n; - } - public GlTF_Accessor(string n, Type t, ComponentType c) { - name = n; - type = t; - switch (t) { - case Type.SCALAR: - byteStride = 0; - break; - case Type.VEC2: - byteStride = 8; - break; - case Type.VEC3: - byteStride = 12; - break; - case Type.VEC4: - byteStride = 16; - break; +public class GlTF_Accessor : GlTF_Writer +{ + public enum Type + { + SCALAR, + VEC2, + VEC3, + VEC4 } - componentType = c; - } - - public static string GetNameFromObject(Object o, string name) { - return "accessor_" + name + "_" + GlTF_Writer.GetNameFromObject(o, true); - } - - private void InitMinMaxInt() { - maxInt = int.MinValue; - minInt = int.MaxValue; - } - - private void InitMinMaxFloat() { - float min = float.MinValue; - float max = float.MaxValue; - maxFloat = new Vector4(min, min, min, min); - minFloat = new Vector4(max, max, max, max); - } - - public void PopulateUshort(int[] vs, bool flippedTriangle) { - if (type != Type.SCALAR) - throw (new System.Exception()); - byteOffset = bufferView.currentOffset; - bufferView.PopulateUshort(vs, flippedTriangle); - count = vs.Length; - if (count > 0) { - InitMinMaxInt(); - for (int i = 0; i < count; ++i) { - minInt = Mathf.Min(vs[i], minInt); - maxInt = Mathf.Max(vs[i], maxInt); - } + + public enum ComponentType + { + USHORT = 5123, + FLOAT = 5126 + } + + public GlTF_BufferView bufferView;// "bufferView": "bufferView_30", + public long byteOffset; //": 0, + public int byteStride;// ": 12, + // GL enum vals ": BYTE (5120), UNSIGNED_BYTE (5121), SHORT (5122), UNSIGNED_SHORT (5123), FLOAT + // (5126) + public ComponentType componentType; + public int count;//": 2399, + public Type type = Type.SCALAR; + + public Vector4 maxFloat; + public Vector4 minFloat; + public int minInt; + public int maxInt; + + public GlTF_Accessor(string n) + { + name = n; + } + public GlTF_Accessor(string n, Type t, ComponentType c) + { + name = n; + type = t; + switch (t) + { + case Type.SCALAR: + byteStride = 0; + break; + case Type.VEC2: + byteStride = 8; + break; + case Type.VEC3: + byteStride = 12; + break; + case Type.VEC4: + byteStride = 16; + break; + } + componentType = c; } - } - - public void PopulateHalfFloat(float[] vs) { - if (type != Type.SCALAR) - throw (new System.Exception()); - byteOffset = bufferView.currentOffset; - bufferView.PopulateHalfFloat(vs); - count = vs.Length; - if (count > 0) { - InitMinMaxFloat(); - for (int i = 0; i < count; ++i) { - minFloat.x = Mathf.Min(vs[i], minFloat.x); - maxFloat.x = Mathf.Max(vs[i], maxFloat.x); - } + + public static string GetNameFromObject(Object o, string name) + { + return "accessor_" + name + "_" + GlTF_Writer.GetNameFromObject(o, true); + } + + private void InitMinMaxInt() + { + maxInt = int.MinValue; + minInt = int.MaxValue; + } + + private void InitMinMaxFloat() + { + float min = float.MinValue; + float max = float.MaxValue; + maxFloat = new Vector4(min, min, min, min); + minFloat = new Vector4(max, max, max, max); + } + + public void PopulateUshort(int[] vs, bool flippedTriangle) + { + if (type != Type.SCALAR) + throw (new System.Exception()); + byteOffset = bufferView.currentOffset; + bufferView.PopulateUshort(vs, flippedTriangle); + count = vs.Length; + if (count > 0) + { + InitMinMaxInt(); + for (int i = 0; i < count; ++i) + { + minInt = Mathf.Min(vs[i], minInt); + maxInt = Mathf.Max(vs[i], maxInt); + } + } + } + + public void PopulateHalfFloat(float[] vs) + { + if (type != Type.SCALAR) + throw (new System.Exception()); + byteOffset = bufferView.currentOffset; + bufferView.PopulateHalfFloat(vs); + count = vs.Length; + if (count > 0) + { + InitMinMaxFloat(); + for (int i = 0; i < count; ++i) + { + minFloat.x = Mathf.Min(vs[i], minFloat.x); + maxFloat.x = Mathf.Max(vs[i], maxFloat.x); + } + } + } + + public void Populate(float[] vs) + { + if (type != Type.SCALAR) + throw (new System.Exception()); + byteOffset = bufferView.currentOffset; + bufferView.Populate(vs); + count = vs.Length; + if (count > 0) + { + InitMinMaxFloat(); + for (int i = 0; i < count; ++i) + { + minFloat.x = Mathf.Min(vs[i], minFloat.x); + maxFloat.x = Mathf.Max(vs[i], maxFloat.x); + } + } } - } - - public void Populate(float[] vs) { - if (type != Type.SCALAR) - throw (new System.Exception()); - byteOffset = bufferView.currentOffset; - bufferView.Populate(vs); - count = vs.Length; - if (count > 0) { - InitMinMaxFloat(); - for (int i = 0; i < count; ++i) { - minFloat.x = Mathf.Min(vs[i], minFloat.x); - maxFloat.x = Mathf.Max(vs[i], maxFloat.x); - } + + public void Populate(Vector2[] v2s, bool flip = false) + { + if (type != Type.VEC2) + throw (new System.Exception()); + byteOffset = bufferView.currentOffset; + count = v2s.Length; + if (count > 0) + { + InitMinMaxFloat(); + + if (flip) + { + for (int i = 0; i < v2s.Length; i++) + { + bufferView.Populate(v2s[i].x); + float y = 1.0f - v2s[i].y; + bufferView.Populate(y); + minFloat.x = Mathf.Min(v2s[i].x, minFloat.x); + minFloat.y = Mathf.Min(y, minFloat.y); + maxFloat.x = Mathf.Max(v2s[i].x, maxFloat.x); + maxFloat.y = Mathf.Max(y, maxFloat.y); + } + } + else + { + for (int i = 0; i < v2s.Length; i++) + { + bufferView.Populate(v2s[i].x); + bufferView.Populate(v2s[i].y); + minFloat.x = Mathf.Min(v2s[i].x, minFloat.x); + minFloat.y = Mathf.Min(v2s[i].y, minFloat.y); + maxFloat.x = Mathf.Max(v2s[i].x, maxFloat.x); + maxFloat.y = Mathf.Max(v2s[i].y, maxFloat.y); + } + } + } } - } - - public void Populate(Vector2[] v2s, bool flip = false) { - if (type != Type.VEC2) - throw (new System.Exception()); - byteOffset = bufferView.currentOffset; - count = v2s.Length; - if (count > 0) { - InitMinMaxFloat(); - - if (flip) { - for (int i = 0; i < v2s.Length; i++) { - bufferView.Populate(v2s[i].x); - float y = 1.0f - v2s[i].y; - bufferView.Populate(y); - minFloat.x = Mathf.Min(v2s[i].x, minFloat.x); - minFloat.y = Mathf.Min(y, minFloat.y); - maxFloat.x = Mathf.Max(v2s[i].x, maxFloat.x); - maxFloat.y = Mathf.Max(y, maxFloat.y); + + private void PopulateVec3(IEnumerator v3s, bool convertToGL, bool convertToMeters) + { + if (type != Type.VEC3) + throw (new System.Exception()); + byteOffset = bufferView.currentOffset; + count = 0; + + while (v3s.MoveNext()) + { + if (count == 0) { InitMinMaxFloat(); } + count++; + Vector3 v = v3s.Current; + // Convert from DirectX/LeftHanded/(fwd=z,rt=x,up=y) -> OpenGL/RightHanded/(fwd=-z,rt=x,up=y) + // Note that this must be kept in sync with ScriptableExporter.MakeNode(), which does a + // similar conversion for matrices. + if (convertToGL) { v.z *= -1; } + if (convertToMeters) { v *= GlTFScriptableExporter.localUnitsToMeters; } + bufferView.Populate(v.x); + bufferView.Populate(v.y); + bufferView.Populate(v.z); + + minFloat.x = Mathf.Min(v.x, minFloat.x); + minFloat.y = Mathf.Min(v.y, minFloat.y); + minFloat.z = Mathf.Min(v.z, minFloat.z); + maxFloat.x = Mathf.Max(v.x, maxFloat.x); + maxFloat.y = Mathf.Max(v.y, maxFloat.y); + maxFloat.z = Mathf.Max(v.z, maxFloat.z); + } - } else { - for (int i = 0; i < v2s.Length; i++) { - bufferView.Populate(v2s[i].x); - bufferView.Populate(v2s[i].y); - minFloat.x = Mathf.Min(v2s[i].x, minFloat.x); - minFloat.y = Mathf.Min(v2s[i].y, minFloat.y); - maxFloat.x = Mathf.Max(v2s[i].x, maxFloat.x); - maxFloat.y = Mathf.Max(v2s[i].y, maxFloat.y); + } + + public void Populate(Vector3[] v3s, bool convertToGL, bool convertToMeters) + { + PopulateVec3(v3s.AsEnumerable().GetEnumerator(), convertToGL, convertToMeters); + } + + public void Populate(List v3s, bool convertToGL) + { + PopulateVec3(v3s.GetEnumerator(), convertToGL, false); + } + + private void PopulateVec4(IEnumerator v4s, bool convertToGL) + { + if (type != Type.VEC4) + throw (new System.Exception()); + byteOffset = bufferView.currentOffset; + count = 0; + while (v4s.MoveNext()) + { + if (count == 0) { InitMinMaxFloat(); } + count++; + Vector4 v = v4s.Current; + // Convert from DirectX/LeftHanded/(fwd=z,rt=x,up=y) -> OpenGL/RightHanded/(fwd=-z,rt=x,up=y) + // Note that this must be kept in sync with ScriptableExporter.MakeNode(), which does a + // similar conversion for matrices. + if (convertToGL) { v.z *= -1; } + bufferView.Populate(v.x); + bufferView.Populate(v.y); + bufferView.Populate(v.z); + bufferView.Populate(v.w); + + minFloat.x = Mathf.Min(v.x, minFloat.x); + minFloat.y = Mathf.Min(v.y, minFloat.y); + minFloat.z = Mathf.Min(v.z, minFloat.z); + minFloat.w = Mathf.Min(v.w, minFloat.w); + maxFloat.x = Mathf.Max(v.x, maxFloat.x); + maxFloat.y = Mathf.Max(v.y, maxFloat.y); + maxFloat.z = Mathf.Max(v.z, maxFloat.z); + maxFloat.w = Mathf.Max(v.w, maxFloat.w); } - } } - } - - private void PopulateVec3(IEnumerator v3s, bool convertToGL, bool convertToMeters) { - if (type != Type.VEC3) - throw (new System.Exception()); - byteOffset = bufferView.currentOffset; - count = 0; - - while (v3s.MoveNext()) { - if (count == 0) { InitMinMaxFloat(); } - count++; - Vector3 v = v3s.Current; - // Convert from DirectX/LeftHanded/(fwd=z,rt=x,up=y) -> OpenGL/RightHanded/(fwd=-z,rt=x,up=y) - // Note that this must be kept in sync with ScriptableExporter.MakeNode(), which does a - // similar conversion for matrices. - if (convertToGL) { v.z *= -1; } - if (convertToMeters) { v *= GlTFScriptableExporter.localUnitsToMeters; } - bufferView.Populate(v.x); - bufferView.Populate(v.y); - bufferView.Populate(v.z); - - minFloat.x = Mathf.Min(v.x, minFloat.x); - minFloat.y = Mathf.Min(v.y, minFloat.y); - minFloat.z = Mathf.Min(v.z, minFloat.z); - maxFloat.x = Mathf.Max(v.x, maxFloat.x); - maxFloat.y = Mathf.Max(v.y, maxFloat.y); - maxFloat.z = Mathf.Max(v.z, maxFloat.z); + public void Populate(List v4s) + { + PopulateVec4(v4s.GetEnumerator(), convertToGL: false); } - } - - public void Populate(Vector3[] v3s, bool convertToGL, bool convertToMeters) { - PopulateVec3(v3s.AsEnumerable().GetEnumerator(), convertToGL, convertToMeters); - } - - public void Populate(List v3s, bool convertToGL) { - PopulateVec3(v3s.GetEnumerator(), convertToGL, false); - } - - private void PopulateVec4(IEnumerator v4s, bool convertToGL) { - if (type != Type.VEC4) - throw (new System.Exception()); - byteOffset = bufferView.currentOffset; - count = 0; - while (v4s.MoveNext()) { - if (count == 0) { InitMinMaxFloat(); } - count++; - Vector4 v = v4s.Current; - // Convert from DirectX/LeftHanded/(fwd=z,rt=x,up=y) -> OpenGL/RightHanded/(fwd=-z,rt=x,up=y) - // Note that this must be kept in sync with ScriptableExporter.MakeNode(), which does a - // similar conversion for matrices. - if (convertToGL) { v.z *= -1; } - bufferView.Populate(v.x); - bufferView.Populate(v.y); - bufferView.Populate(v.z); - bufferView.Populate(v.w); - - minFloat.x = Mathf.Min(v.x, minFloat.x); - minFloat.y = Mathf.Min(v.y, minFloat.y); - minFloat.z = Mathf.Min(v.z, minFloat.z); - minFloat.w = Mathf.Min(v.w, minFloat.w); - maxFloat.x = Mathf.Max(v.x, maxFloat.x); - maxFloat.y = Mathf.Max(v.y, maxFloat.y); - maxFloat.z = Mathf.Max(v.z, maxFloat.z); - maxFloat.w = Mathf.Max(v.w, maxFloat.w); + + public void Populate(Vector4[] v4s, bool convertToGL) + { + PopulateVec4(v4s.AsEnumerable().GetEnumerator(), convertToGL); } - } - - public void Populate(List v4s) { - PopulateVec4(v4s.GetEnumerator(), convertToGL: false); - } - - public void Populate(Vector4[] v4s, bool convertToGL) { - PopulateVec4(v4s.AsEnumerable().GetEnumerator(), convertToGL); - } - - private void WriteMin() { - if (componentType == ComponentType.FLOAT) { - switch (type) { - case Type.SCALAR: - jsonWriter.Write(minFloat.x); - break; - - case Type.VEC2: - jsonWriter.Write(minFloat.x + ", " + minFloat.y); - break; - - case Type.VEC3: - jsonWriter.Write(minFloat.x + ", " + minFloat.y + ", " + minFloat.z); - break; - - case Type.VEC4: - jsonWriter.Write(minFloat.x + ", " + minFloat.y + ", " + minFloat.z + ", " + minFloat.w); - break; - } - } else if (componentType == ComponentType.USHORT) { - if (type == Type.SCALAR) { - jsonWriter.Write(minInt); - } + + private void WriteMin() + { + if (componentType == ComponentType.FLOAT) + { + switch (type) + { + case Type.SCALAR: + jsonWriter.Write(minFloat.x); + break; + + case Type.VEC2: + jsonWriter.Write(minFloat.x + ", " + minFloat.y); + break; + + case Type.VEC3: + jsonWriter.Write(minFloat.x + ", " + minFloat.y + ", " + minFloat.z); + break; + + case Type.VEC4: + jsonWriter.Write(minFloat.x + ", " + minFloat.y + ", " + minFloat.z + ", " + minFloat.w); + break; + } + } + else if (componentType == ComponentType.USHORT) + { + if (type == Type.SCALAR) + { + jsonWriter.Write(minInt); + } + } } - } - - private void WriteMax() { - if (componentType == ComponentType.FLOAT) { - switch (type) { - case Type.SCALAR: - jsonWriter.Write(maxFloat.x); - break; - - case Type.VEC2: - jsonWriter.Write(maxFloat.x + ", " + maxFloat.y); - break; - - case Type.VEC3: - jsonWriter.Write(maxFloat.x + ", " + maxFloat.y + ", " + maxFloat.z); - break; - - case Type.VEC4: - jsonWriter.Write(maxFloat.x + ", " + maxFloat.y + ", " + maxFloat.z + ", " + maxFloat.w); - break; - } - } else if (componentType == ComponentType.USHORT) { - if (type == Type.SCALAR) { - jsonWriter.Write(maxInt); - } + + private void WriteMax() + { + if (componentType == ComponentType.FLOAT) + { + switch (type) + { + case Type.SCALAR: + jsonWriter.Write(maxFloat.x); + break; + + case Type.VEC2: + jsonWriter.Write(maxFloat.x + ", " + maxFloat.y); + break; + + case Type.VEC3: + jsonWriter.Write(maxFloat.x + ", " + maxFloat.y + ", " + maxFloat.z); + break; + + case Type.VEC4: + jsonWriter.Write(maxFloat.x + ", " + maxFloat.y + ", " + maxFloat.z + ", " + maxFloat.w); + break; + } + } + else if (componentType == ComponentType.USHORT) + { + if (type == Type.SCALAR) + { + jsonWriter.Write(maxInt); + } + } + } + + public override void Write() + { + Indent(); jsonWriter.Write("\"" + name + "\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"bufferView\": \"" + bufferView.name + "\",\n"); + Indent(); jsonWriter.Write("\"byteOffset\": " + byteOffset + ",\n"); + Indent(); jsonWriter.Write("\"byteStride\": " + byteStride + ",\n"); + Indent(); jsonWriter.Write("\"componentType\": " + (int)componentType + ",\n"); + Indent(); jsonWriter.Write("\"count\": " + count + ",\n"); + + + Indent(); jsonWriter.Write("\"max\": [ "); + WriteMax(); + jsonWriter.Write(" ],\n"); + Indent(); jsonWriter.Write("\"min\": [ "); + WriteMin(); + jsonWriter.Write(" ],\n"); + + Indent(); jsonWriter.Write("\"type\": \"" + type + "\"\n"); + IndentOut(); + Indent(); jsonWriter.Write(" }"); } - } - - public override void Write() { - Indent(); jsonWriter.Write("\"" + name + "\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"bufferView\": \"" + bufferView.name + "\",\n"); - Indent(); jsonWriter.Write("\"byteOffset\": " + byteOffset + ",\n"); - Indent(); jsonWriter.Write("\"byteStride\": " + byteStride + ",\n"); - Indent(); jsonWriter.Write("\"componentType\": " + (int) componentType + ",\n"); - Indent(); jsonWriter.Write("\"count\": " + count + ",\n"); - - - Indent(); jsonWriter.Write("\"max\": [ "); - WriteMax(); - jsonWriter.Write(" ],\n"); - Indent(); jsonWriter.Write("\"min\": [ "); - WriteMin(); - jsonWriter.Write(" ],\n"); - - Indent(); jsonWriter.Write("\"type\": \"" + type + "\"\n"); - IndentOut(); - Indent(); jsonWriter.Write(" }"); - } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_AmbientLight.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_AmbientLight.cs index 91af6784..d103d06f 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_AmbientLight.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_AmbientLight.cs @@ -1,8 +1,10 @@ using UnityEngine; using System.Collections; -public class GlTF_AmbientLight : GlTF_Light { - public override void Write() { - color.Write(); - } +public class GlTF_AmbientLight : GlTF_Light +{ + public override void Write() + { + color.Write(); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_AnimSampler.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_AnimSampler.cs index cd9233a3..e1116b0e 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_AnimSampler.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_AnimSampler.cs @@ -1,22 +1,25 @@ using UnityEngine; using System.Collections; -public class GlTF_AnimSampler : GlTF_Writer { - public string input = "TIME"; - public string interpolation = "LINEAR"; // only things in glTF as of today - public string output = "translation"; // or whatever +public class GlTF_AnimSampler : GlTF_Writer +{ + public string input = "TIME"; + public string interpolation = "LINEAR"; // only things in glTF as of today + public string output = "translation"; // or whatever - public GlTF_AnimSampler(string n, string o) { - name = n; output = o; - } + public GlTF_AnimSampler(string n, string o) + { + name = n; output = o; + } - public override void Write() { - Indent(); jsonWriter.Write("\"" + name + "\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"input\": \"" + input + "\",\n"); - Indent(); jsonWriter.Write("\"interpolation\": \"" + interpolation + "\",\n"); - Indent(); jsonWriter.Write("\"output\": \"" + output + "\"\n"); - IndentOut(); - Indent(); jsonWriter.Write("}"); - } + public override void Write() + { + Indent(); jsonWriter.Write("\"" + name + "\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"input\": \"" + input + "\",\n"); + Indent(); jsonWriter.Write("\"interpolation\": \"" + interpolation + "\",\n"); + Indent(); jsonWriter.Write("\"output\": \"" + output + "\"\n"); + IndentOut(); + Indent(); jsonWriter.Write("}"); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Attributes.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Attributes.cs index 951411f1..2ad51606 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Attributes.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Attributes.cs @@ -3,157 +3,183 @@ using System.Collections; using System.Collections.Generic; -public class GlTF_Attributes : GlTF_Writer { - public GlTF_Accessor normalAccessor; - public GlTF_Accessor colorAccessor; - public GlTF_Accessor tangentAccessor; - public GlTF_Accessor vertexIdAccessor; - public GlTF_Accessor positionAccessor; - public GlTF_Accessor texCoord0Accessor; - public GlTF_Accessor texCoord1Accessor; - public GlTF_Accessor texCoord2Accessor; - public GlTF_Accessor texCoord3Accessor; +public class GlTF_Attributes : GlTF_Writer +{ + public GlTF_Accessor normalAccessor; + public GlTF_Accessor colorAccessor; + public GlTF_Accessor tangentAccessor; + public GlTF_Accessor vertexIdAccessor; + public GlTF_Accessor positionAccessor; + public GlTF_Accessor texCoord0Accessor; + public GlTF_Accessor texCoord1Accessor; + public GlTF_Accessor texCoord2Accessor; + public GlTF_Accessor texCoord3Accessor; - // Populate the accessor with the given UV channel on the given mesh, with the correct number of - // elements used by the mesh (e.g. scalar/Vec2/Vec3/Vec4). - private void PopulateUv(int channel, Mesh mesh, GlTF_Accessor accessor, bool packVertId = false) { - if (accessor == null) { - return; - } - if (channel < 0 || channel > 3) { - throw new ArgumentException("Invalid channel"); - } - - if (packVertId && (accessor.type != GlTF_Accessor.Type.VEC4 || channel != 1)) { - throw new ArgumentException("VertexIDs can only be packed into channel 1 with vec4 uvs"); - } + // Populate the accessor with the given UV channel on the given mesh, with the correct number of + // elements used by the mesh (e.g. scalar/Vec2/Vec3/Vec4). + private void PopulateUv(int channel, Mesh mesh, GlTF_Accessor accessor, bool packVertId = false) + { + if (accessor == null) + { + return; + } + if (channel < 0 || channel > 3) + { + throw new ArgumentException("Invalid channel"); + } - switch (accessor.type) { - case GlTF_Accessor.Type.SCALAR: - List uvTemp = new List(); - mesh.GetUVs(channel, uvTemp); - float[] uvs = new float[uvTemp.Count]; - for (int i = 0; i < uvTemp.Count; i++) { - uvs[i] = uvTemp[i].x; - } - accessor.Populate(uvs); - return; - case GlTF_Accessor.Type.VEC2: - // Fast path, avoid conversions. - switch (channel) { - case 0: - accessor.Populate(mesh.uv, true); - return; - case 1: - accessor.Populate(mesh.uv2, true); - return; - case 2: - accessor.Populate(mesh.uv3, true); - return; - case 3: - default: - accessor.Populate(mesh.uv4, true); - return; - } - case GlTF_Accessor.Type.VEC3: - // This could use ListExtensions to avoid a copy, but that code lives in TiltBrush. - List uvTemp3 = new List(); - mesh.GetUVs(channel, uvTemp3); - accessor.Populate(uvTemp3, convertToGL: false); - return; - case GlTF_Accessor.Type.VEC4: - List uvTemp4 = new List(); - mesh.GetUVs(channel, uvTemp4); - if (packVertId) { - for (int i = 0; i < uvTemp4.Count; i++) { - Vector4 v = uvTemp4[i]; - v.w = i; - uvTemp4[i] = v; - } - } - accessor.Populate(uvTemp4); - return; - default: - throw new ArgumentException("Unexpected accessor type"); - } - } + if (packVertId && (accessor.type != GlTF_Accessor.Type.VEC4 || channel != 1)) + { + throw new ArgumentException("VertexIDs can only be packed into channel 1 with vec4 uvs"); + } - public void Populate(Mesh m) { - positionAccessor.Populate(m.vertices, convertToGL: true, convertToMeters: true); - if (normalAccessor != null) { - normalAccessor.Populate(m.normals, convertToGL: true, convertToMeters: false); - } - if (colorAccessor != null) { - // We assume that colors are LDR and use the more efficient colors32 format. - // - // TODO(ineula): Switch the glTF vertex color format to use a uint8 triple - // or similar. This will require some changes to ThirdParty/GlTF. - var colors = Array.ConvertAll(m.colors32, item => - new Vector4(item.r * 1.0f/255, item.g * 1.0f/255, item.b * 1.0f/255, item.a * 1.0f/255)); - colorAccessor.Populate(colors, convertToGL: false); - } - if (tangentAccessor != null) { - tangentAccessor.Populate(m.tangents, convertToGL: true); - } - if (vertexIdAccessor != null) { - float[] vertexIds = new float[m.vertexCount]; - for (float i = 0; i < m.vertexCount; i++) { - vertexIds[(int)i] = i; - } - vertexIdAccessor.Populate(vertexIds); + switch (accessor.type) + { + case GlTF_Accessor.Type.SCALAR: + List uvTemp = new List(); + mesh.GetUVs(channel, uvTemp); + float[] uvs = new float[uvTemp.Count]; + for (int i = 0; i < uvTemp.Count; i++) + { + uvs[i] = uvTemp[i].x; + } + accessor.Populate(uvs); + return; + case GlTF_Accessor.Type.VEC2: + // Fast path, avoid conversions. + switch (channel) + { + case 0: + accessor.Populate(mesh.uv, true); + return; + case 1: + accessor.Populate(mesh.uv2, true); + return; + case 2: + accessor.Populate(mesh.uv3, true); + return; + case 3: + default: + accessor.Populate(mesh.uv4, true); + return; + } + case GlTF_Accessor.Type.VEC3: + // This could use ListExtensions to avoid a copy, but that code lives in TiltBrush. + List uvTemp3 = new List(); + mesh.GetUVs(channel, uvTemp3); + accessor.Populate(uvTemp3, convertToGL: false); + return; + case GlTF_Accessor.Type.VEC4: + List uvTemp4 = new List(); + mesh.GetUVs(channel, uvTemp4); + if (packVertId) + { + for (int i = 0; i < uvTemp4.Count; i++) + { + Vector4 v = uvTemp4[i]; + v.w = i; + uvTemp4[i] = v; + } + } + accessor.Populate(uvTemp4); + return; + default: + throw new ArgumentException("Unexpected accessor type"); + } } - // UVs may be 1, 2, 3 or 4 element tuples, which the following helper method resolves. - // In the case of zero UVs, the texCoord accessor will be null and will not be populated. - PopulateUv(0, m, texCoord0Accessor); - PopulateUv(1, m, texCoord1Accessor, packVertId: vertexIdAccessor != null); - PopulateUv(2, m, texCoord2Accessor); - PopulateUv(3, m, texCoord3Accessor); - } + public void Populate(Mesh m) + { + positionAccessor.Populate(m.vertices, convertToGL: true, convertToMeters: true); + if (normalAccessor != null) + { + normalAccessor.Populate(m.normals, convertToGL: true, convertToMeters: false); + } + if (colorAccessor != null) + { + // We assume that colors are LDR and use the more efficient colors32 format. + // + // TODO(ineula): Switch the glTF vertex color format to use a uint8 triple + // or similar. This will require some changes to ThirdParty/GlTF. + var colors = Array.ConvertAll(m.colors32, item => + new Vector4(item.r * 1.0f / 255, item.g * 1.0f / 255, item.b * 1.0f / 255, item.a * 1.0f / 255)); + colorAccessor.Populate(colors, convertToGL: false); + } + if (tangentAccessor != null) + { + tangentAccessor.Populate(m.tangents, convertToGL: true); + } + if (vertexIdAccessor != null) + { + float[] vertexIds = new float[m.vertexCount]; + for (float i = 0; i < m.vertexCount; i++) + { + vertexIds[(int)i] = i; + } + vertexIdAccessor.Populate(vertexIds); + } - public override void Write() { - Indent(); jsonWriter.Write("\"attributes\": {\n"); - IndentIn(); - if (positionAccessor != null) { - CommaNL(); - Indent(); jsonWriter.Write("\"POSITION\": \"" + positionAccessor.name + "\""); - } - if (normalAccessor != null) { - CommaNL(); - Indent(); jsonWriter.Write("\"NORMAL\": \"" + normalAccessor.name + "\""); + // UVs may be 1, 2, 3 or 4 element tuples, which the following helper method resolves. + // In the case of zero UVs, the texCoord accessor will be null and will not be populated. + PopulateUv(0, m, texCoord0Accessor); + PopulateUv(1, m, texCoord1Accessor, packVertId: vertexIdAccessor != null); + PopulateUv(2, m, texCoord2Accessor); + PopulateUv(3, m, texCoord3Accessor); } - if (colorAccessor != null) { - CommaNL(); - Indent(); jsonWriter.Write("\"COLOR\": \"" + colorAccessor.name + "\""); - } - if (tangentAccessor != null) { - CommaNL(); - Indent(); jsonWriter.Write("\"TANGENT\": \"" + tangentAccessor.name + "\""); - } - if (vertexIdAccessor != null) { - CommaNL(); - Indent(); jsonWriter.Write("\"VERTEXID\": \"" + vertexIdAccessor.name + "\""); - } - if (texCoord0Accessor != null) { - CommaNL(); - Indent(); jsonWriter.Write("\"TEXCOORD_0\": \"" + texCoord0Accessor.name + "\""); - } - if (texCoord1Accessor != null) { - CommaNL(); - Indent(); jsonWriter.Write("\"TEXCOORD_1\": \"" + texCoord1Accessor.name + "\""); - } - if (texCoord2Accessor != null) { - CommaNL(); - Indent(); jsonWriter.Write("\"TEXCOORD_2\": \"" + texCoord2Accessor.name + "\""); - } - if (texCoord3Accessor != null) { - CommaNL(); - Indent(); jsonWriter.Write("\"TEXCOORD_3\": \"" + texCoord3Accessor.name + "\""); + + public override void Write() + { + Indent(); jsonWriter.Write("\"attributes\": {\n"); + IndentIn(); + if (positionAccessor != null) + { + CommaNL(); + Indent(); jsonWriter.Write("\"POSITION\": \"" + positionAccessor.name + "\""); + } + if (normalAccessor != null) + { + CommaNL(); + Indent(); jsonWriter.Write("\"NORMAL\": \"" + normalAccessor.name + "\""); + } + if (colorAccessor != null) + { + CommaNL(); + Indent(); jsonWriter.Write("\"COLOR\": \"" + colorAccessor.name + "\""); + } + if (tangentAccessor != null) + { + CommaNL(); + Indent(); jsonWriter.Write("\"TANGENT\": \"" + tangentAccessor.name + "\""); + } + if (vertexIdAccessor != null) + { + CommaNL(); + Indent(); jsonWriter.Write("\"VERTEXID\": \"" + vertexIdAccessor.name + "\""); + } + if (texCoord0Accessor != null) + { + CommaNL(); + Indent(); jsonWriter.Write("\"TEXCOORD_0\": \"" + texCoord0Accessor.name + "\""); + } + if (texCoord1Accessor != null) + { + CommaNL(); + Indent(); jsonWriter.Write("\"TEXCOORD_1\": \"" + texCoord1Accessor.name + "\""); + } + if (texCoord2Accessor != null) + { + CommaNL(); + Indent(); jsonWriter.Write("\"TEXCOORD_2\": \"" + texCoord2Accessor.name + "\""); + } + if (texCoord3Accessor != null) + { + CommaNL(); + Indent(); jsonWriter.Write("\"TEXCOORD_3\": \"" + texCoord3Accessor.name + "\""); + } + //CommaNL(); + jsonWriter.WriteLine(); + IndentOut(); + Indent(); jsonWriter.Write("}"); } - //CommaNL(); - jsonWriter.WriteLine(); - IndentOut(); - Indent(); jsonWriter.Write("}"); - } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_BufferView.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_BufferView.cs index 2ef403ba..34f85237 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_BufferView.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_BufferView.cs @@ -3,87 +3,102 @@ using System.IO; using System; -public class GlTF_BufferView : GlTF_Writer { - public string buffer;// ": "duck", - public long byteLength;//": 25272, - public long byteOffset;//": 0, - public int target = 34962; - // public string target = "ARRAY_BUFFER"; - public int currentOffset = 0; - public MemoryStream memoryStream = new MemoryStream(); - public bool bin = false; +public class GlTF_BufferView : GlTF_Writer +{ + public string buffer;// ": "duck", + public long byteLength;//": 25272, + public long byteOffset;//": 0, + public int target = 34962; + // public string target = "ARRAY_BUFFER"; + public int currentOffset = 0; + public MemoryStream memoryStream = new MemoryStream(); + public bool bin = false; - public GlTF_BufferView(string n) { - name = n; - } - public GlTF_BufferView(string n, int t) { - name = n; target = t; - } + public GlTF_BufferView(string n) + { + name = n; + } + public GlTF_BufferView(string n, int t) + { + name = n; target = t; + } - public void PopulateUshort(int[] vs, bool flippedTriangle) { - if (flippedTriangle) { - for (int i = 0; i < vs.Length; i += 3) { - ushort u = (ushort) vs[i]; - memoryStream.Write(BitConverter.GetBytes(u), 0, 2); - currentOffset += 2; + public void PopulateUshort(int[] vs, bool flippedTriangle) + { + if (flippedTriangle) + { + for (int i = 0; i < vs.Length; i += 3) + { + ushort u = (ushort)vs[i]; + memoryStream.Write(BitConverter.GetBytes(u), 0, 2); + currentOffset += 2; - u = (ushort) vs[i + 2]; - memoryStream.Write(BitConverter.GetBytes(u), 0, 2); - currentOffset += 2; + u = (ushort)vs[i + 2]; + memoryStream.Write(BitConverter.GetBytes(u), 0, 2); + currentOffset += 2; - u = (ushort) vs[i + 1]; - memoryStream.Write(BitConverter.GetBytes(u), 0, 2); - currentOffset += 2; - } - } else { - for (int i = 0; i < vs.Length; i++) { - ushort u = (ushort) vs[i]; - memoryStream.Write(BitConverter.GetBytes(u), 0, 2); - currentOffset += 2; - } + u = (ushort)vs[i + 1]; + memoryStream.Write(BitConverter.GetBytes(u), 0, 2); + currentOffset += 2; + } + } + else + { + for (int i = 0; i < vs.Length; i++) + { + ushort u = (ushort)vs[i]; + memoryStream.Write(BitConverter.GetBytes(u), 0, 2); + currentOffset += 2; + } + } + byteLength = currentOffset; } - byteLength = currentOffset; - } - public void PopulateHalfFloat(float[] vs) { - for (int i = 0; i < vs.Length; i++) { - float f = vs[i]; - memoryStream.Write(BitConverter.GetBytes(f), 0, 2); - currentOffset += 2; + public void PopulateHalfFloat(float[] vs) + { + for (int i = 0; i < vs.Length; i++) + { + float f = vs[i]; + memoryStream.Write(BitConverter.GetBytes(f), 0, 2); + currentOffset += 2; + } + byteLength = currentOffset; } - byteLength = currentOffset; - } - public void Populate(float[] vs) { - for (int i = 0; i < vs.Length; i++) { - Populate(vs[i]); + public void Populate(float[] vs) + { + for (int i = 0; i < vs.Length; i++) + { + Populate(vs[i]); + } + byteLength = currentOffset; } - byteLength = currentOffset; - } - public void Populate(float v) { - memoryStream.Write(BitConverter.GetBytes(v), 0, 4); - currentOffset += 4; - byteLength = currentOffset; - } + public void Populate(float v) + { + memoryStream.Write(BitConverter.GetBytes(v), 0, 4); + currentOffset += 4; + byteLength = currentOffset; + } - public override void Write() { - /* - "bufferView_4642": { - "buffer": "vc.bin", - "byteLength": 630080, - "byteOffset": 0, - "target": "ARRAY_BUFFER" - }, - */ - Indent(); jsonWriter.Write("\"" + name + "\": {\n"); - IndentIn(); - var binName = binary ? "binary_glTF" : Path.GetFileNameWithoutExtension(GlTF_Writer.binFileName); - Indent(); jsonWriter.Write("\"buffer\": \"" + binName + "\",\n"); - Indent(); jsonWriter.Write("\"byteLength\": " + byteLength + ",\n"); - Indent(); jsonWriter.Write("\"byteOffset\": " + byteOffset + ",\n"); - Indent(); jsonWriter.Write("\"target\": " + target + "\n"); - IndentOut(); - Indent(); jsonWriter.Write("}"); - } + public override void Write() + { + /* + "bufferView_4642": { + "buffer": "vc.bin", + "byteLength": 630080, + "byteOffset": 0, + "target": "ARRAY_BUFFER" + }, + */ + Indent(); jsonWriter.Write("\"" + name + "\": {\n"); + IndentIn(); + var binName = binary ? "binary_glTF" : Path.GetFileNameWithoutExtension(GlTF_Writer.binFileName); + Indent(); jsonWriter.Write("\"buffer\": \"" + binName + "\",\n"); + Indent(); jsonWriter.Write("\"byteLength\": " + byteLength + ",\n"); + Indent(); jsonWriter.Write("\"byteOffset\": " + byteOffset + ",\n"); + Indent(); jsonWriter.Write("\"target\": " + target + "\n"); + IndentOut(); + Indent(); jsonWriter.Write("}"); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Camera.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Camera.cs index 09d7d91c..ec704193 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Camera.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Camera.cs @@ -1,6 +1,7 @@ using UnityEngine; using System.Collections; -public class GlTF_Camera : GlTF_Writer { - public string type; // should be enum ": "perspective" +public class GlTF_Camera : GlTF_Writer +{ + public string type; // should be enum ": "perspective" } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Channel.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Channel.cs index 1fdce341..7aa1ff50 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Channel.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Channel.cs @@ -1,31 +1,35 @@ using UnityEngine; using System.Collections; -public class GlTF_Channel : GlTF_Writer { - public GlTF_AnimSampler sampler; - public GlTF_Target target; +public class GlTF_Channel : GlTF_Writer +{ + public GlTF_AnimSampler sampler; + public GlTF_Target target; - public GlTF_Channel(string ch, GlTF_AnimSampler s) { - sampler = s; - switch (ch) { - case "translation": - break; - case "rotation": - break; - case "scale": - break; + public GlTF_Channel(string ch, GlTF_AnimSampler s) + { + sampler = s; + switch (ch) + { + case "translation": + break; + case "rotation": + break; + case "scale": + break; + } } - } - public override void Write() { - IndentIn(); - Indent(); jsonWriter.Write("{\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"sampler\": \"" + sampler.name + "\",\n"); - target.Write(); - jsonWriter.WriteLine(); - IndentOut(); - Indent(); jsonWriter.Write("}"); - IndentOut(); - } + public override void Write() + { + IndentIn(); + Indent(); jsonWriter.Write("{\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"sampler\": \"" + sampler.name + "\",\n"); + target.Write(); + jsonWriter.WriteLine(); + IndentOut(); + Indent(); jsonWriter.Write("}"); + IndentOut(); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_ColorOrTexture.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_ColorOrTexture.cs index dbf1579c..75c7becb 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_ColorOrTexture.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_ColorOrTexture.cs @@ -1,10 +1,13 @@ using UnityEngine; using System.Collections; -public class GlTF_ColorOrTexture : GlTF_Writer { - public GlTF_ColorOrTexture() { - } - public GlTF_ColorOrTexture(string n) { - name = n; - } +public class GlTF_ColorOrTexture : GlTF_Writer +{ + public GlTF_ColorOrTexture() + { + } + public GlTF_ColorOrTexture(string n) + { + name = n; + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_ColorRGB.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_ColorRGB.cs index 2bc42a14..2330bf92 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_ColorRGB.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_ColorRGB.cs @@ -1,23 +1,28 @@ using UnityEngine; using System.Collections; -public class GlTF_ColorRGB : GlTF_Writer { - private Color color; - public GlTF_ColorRGB(string n) { - name = n; - } - public GlTF_ColorRGB(Color c) { - color = c; - } - public GlTF_ColorRGB(string n, Color c) { - name = n; color = c; - } - public override void Write() { - Indent(); - if (name.Length > 0) - jsonWriter.Write("\"" + name + "\": "); - else - jsonWriter.Write("\"color\": ["); - jsonWriter.Write(color.r.ToString() + ", " + color.g.ToString() + ", " + color.b.ToString() + "]"); - } +public class GlTF_ColorRGB : GlTF_Writer +{ + private Color color; + public GlTF_ColorRGB(string n) + { + name = n; + } + public GlTF_ColorRGB(Color c) + { + color = c; + } + public GlTF_ColorRGB(string n, Color c) + { + name = n; color = c; + } + public override void Write() + { + Indent(); + if (name.Length > 0) + jsonWriter.Write("\"" + name + "\": "); + else + jsonWriter.Write("\"color\": ["); + jsonWriter.Write(color.r.ToString() + ", " + color.g.ToString() + ", " + color.b.ToString() + "]"); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_ColorRGBA.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_ColorRGBA.cs index 0fa34d85..c5849f5c 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_ColorRGBA.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_ColorRGBA.cs @@ -1,23 +1,28 @@ using UnityEngine; using System.Collections; -public class GlTF_ColorRGBA : GlTF_Writer { - private Color color; - public GlTF_ColorRGBA(string n) { - name = n; - } - public GlTF_ColorRGBA(Color c) { - color = c; - } - public GlTF_ColorRGBA(string n, Color c) { - name = n; color = c; - } - public override void Write() { - Indent(); - if (name.Length > 0) - jsonWriter.Write("\"" + name + "\": ["); - else - jsonWriter.Write("\"color\": ["); - jsonWriter.Write(color.r.ToString() + ", " + color.g.ToString() + ", " + color.b.ToString() + ", " + color.a.ToString() + "]"); - } +public class GlTF_ColorRGBA : GlTF_Writer +{ + private Color color; + public GlTF_ColorRGBA(string n) + { + name = n; + } + public GlTF_ColorRGBA(Color c) + { + color = c; + } + public GlTF_ColorRGBA(string n, Color c) + { + name = n; color = c; + } + public override void Write() + { + Indent(); + if (name.Length > 0) + jsonWriter.Write("\"" + name + "\": ["); + else + jsonWriter.Write("\"color\": ["); + jsonWriter.Write(color.r.ToString() + ", " + color.g.ToString() + ", " + color.b.ToString() + ", " + color.a.ToString() + "]"); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_DirectionalLight.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_DirectionalLight.cs index 9afa9b19..18108905 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_DirectionalLight.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_DirectionalLight.cs @@ -1,8 +1,10 @@ using UnityEngine; using System.Collections; -public class GlTF_DirectionalLight : GlTF_Light { - public override void Write() { - color.Write(); - } +public class GlTF_DirectionalLight : GlTF_Light +{ + public override void Write() + { + color.Write(); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_FloatArray.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_FloatArray.cs index d48f99a9..7975cedc 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_FloatArray.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_FloatArray.cs @@ -1,32 +1,40 @@ using UnityEngine; using System.Collections; -public class GlTF_FloatArray : GlTF_Writer { - public float[] items; - public int minItems = 0; - public int maxItems = 0; +public class GlTF_FloatArray : GlTF_Writer +{ + public float[] items; + public int minItems = 0; + public int maxItems = 0; - public GlTF_FloatArray() { - } - public GlTF_FloatArray(string n) { - name = n; - } - - public override void Write() { - if (name.Length > 0) { - Indent(); jsonWriter.Write("\"" + name + "\": ["); + public GlTF_FloatArray() + { + } + public GlTF_FloatArray(string n) + { + name = n; } - WriteVals(); - if (name.Length > 0) { - jsonWriter.Write("]"); + + public override void Write() + { + if (name.Length > 0) + { + Indent(); jsonWriter.Write("\"" + name + "\": ["); + } + WriteVals(); + if (name.Length > 0) + { + jsonWriter.Write("]"); + } } - } - public virtual void WriteVals() { - for (int i = 0; i < maxItems; i++) { - if (i > 0) - jsonWriter.Write(", "); - jsonWriter.Write(items[i].ToString()); + public virtual void WriteVals() + { + for (int i = 0; i < maxItems; i++) + { + if (i > 0) + jsonWriter.Write(", "); + jsonWriter.Write(items[i].ToString()); + } } - } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_FloatArray4.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_FloatArray4.cs index 902e8bcd..2b9eb19d 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_FloatArray4.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_FloatArray4.cs @@ -1,16 +1,18 @@ using UnityEngine; using System.Collections; -public class GlTF_FloatArray4 : GlTF_FloatArray { - public GlTF_FloatArray4() { - minItems = 4; maxItems = 4; items = new float[] { 1.0f, 0.0f, 0.0f, 0.0f }; - } - /* - public override void Write() - { - Indent(); jsonWriter.Write ("\"rotation\": [ "); - WriteVals(); - jsonWriter.Write ("]"); - } -*/ +public class GlTF_FloatArray4 : GlTF_FloatArray +{ + public GlTF_FloatArray4() + { + minItems = 4; maxItems = 4; items = new float[] { 1.0f, 0.0f, 0.0f, 0.0f }; + } + /* + public override void Write() + { + Indent(); jsonWriter.Write ("\"rotation\": [ "); + WriteVals(); + jsonWriter.Write ("]"); + } + */ } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Image.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Image.cs index afbebe44..de660054 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Image.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Image.cs @@ -1,18 +1,21 @@ using UnityEngine; using System.Collections; -public class GlTF_Image : GlTF_Writer { - public string uri; +public class GlTF_Image : GlTF_Writer +{ + public string uri; - public static string GetNameFromObject(Object o) { - return "image_" + GlTF_Writer.GetNameFromObject(o, false); - } + public static string GetNameFromObject(Object o) + { + return "image_" + GlTF_Writer.GetNameFromObject(o, false); + } - public override void Write() { - Indent(); jsonWriter.Write("\"" + name + "\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"uri\": \"" + uri + "\"\n"); - IndentOut(); - Indent(); jsonWriter.Write("}"); - } + public override void Write() + { + Indent(); jsonWriter.Write("\"" + name + "\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"uri\": \"" + uri + "\"\n"); + IndentOut(); + Indent(); jsonWriter.Write("}"); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Light.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Light.cs index 6618f938..5c1f57d0 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Light.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Light.cs @@ -1,10 +1,11 @@ using UnityEngine; using System.Collections; -public class GlTF_Light : GlTF_Writer { - public GlTF_ColorRGB color; - public string type; - // public override void Write () - // { - // } +public class GlTF_Light : GlTF_Writer +{ + public GlTF_ColorRGB color; + public string type; + // public override void Write () + // { + // } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Material.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Material.cs index 9782992c..e91308f8 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Material.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Material.cs @@ -2,98 +2,111 @@ using System.Collections; using System.Collections.Generic; -public class GlTF_Material : GlTF_Writer { +public class GlTF_Material : GlTF_Writer +{ - public class Value : GlTF_Writer { - } + public class Value : GlTF_Writer + { + } - public class ColorValue : Value { - public Color color; + public class ColorValue : Value + { + public Color color; - public override void Write() { - jsonWriter.Write("\"" + name + "\": ["); - jsonWriter.Write(color.r.ToString() + ", " + color.g.ToString() + ", " + color.b.ToString() + ", " + color.a.ToString()); - jsonWriter.Write("]"); + public override void Write() + { + jsonWriter.Write("\"" + name + "\": ["); + jsonWriter.Write(color.r.ToString() + ", " + color.g.ToString() + ", " + color.b.ToString() + ", " + color.a.ToString()); + jsonWriter.Write("]"); + } } - } - public class VectorValue : Value { - public Vector4 vector; + public class VectorValue : Value + { + public Vector4 vector; - public override void Write() { - jsonWriter.Write("\"" + name + "\": ["); - jsonWriter.Write(vector.x.ToString() + ", " + vector.y.ToString() + ", " + vector.z.ToString() + ", " + vector.w.ToString()); - jsonWriter.Write("]"); + public override void Write() + { + jsonWriter.Write("\"" + name + "\": ["); + jsonWriter.Write(vector.x.ToString() + ", " + vector.y.ToString() + ", " + vector.z.ToString() + ", " + vector.w.ToString()); + jsonWriter.Write("]"); + } } - } - public class FloatValue : Value { - public float value; + public class FloatValue : Value + { + public float value; - public override void Write() { - jsonWriter.Write("\"" + name + "\": " + value + ""); + public override void Write() + { + jsonWriter.Write("\"" + name + "\": " + value + ""); + } } - } - public class StringValue : Value { - public string value; + public class StringValue : Value + { + public string value; - public override void Write() { - jsonWriter.Write("\"" + name + "\": \"" + value + "\""); + public override void Write() + { + jsonWriter.Write("\"" + name + "\": \"" + value + "\""); + } } - } - - public string instanceTechniqueName = "technique1"; - public GlTF_ColorOrTexture ambient;// = new GlTF_ColorRGBA ("ambient"); - public GlTF_ColorOrTexture diffuse; - public float shininess; - public GlTF_ColorOrTexture specular;// = new GlTF_ColorRGBA ("specular"); - public List values = new List(); - - public static string GetNameFromObject(Object o) { - return "material_" + GlTF_Writer.GetNameFromObject(o, false); - } - - public override void Write() { - Indent(); jsonWriter.Write("\"" + name + "\": {\n"); - IndentIn(); - CommaNL(); - Indent(); jsonWriter.Write("\"technique\": \"" + instanceTechniqueName + "\",\n"); - Indent(); jsonWriter.Write("\"values\": {\n"); - IndentIn(); - foreach (var v in values) { - CommaNL(); - Indent(); v.Write(); + + public string instanceTechniqueName = "technique1"; + public GlTF_ColorOrTexture ambient;// = new GlTF_ColorRGBA ("ambient"); + public GlTF_ColorOrTexture diffuse; + public float shininess; + public GlTF_ColorOrTexture specular;// = new GlTF_ColorRGBA ("specular"); + public List values = new List(); + + public static string GetNameFromObject(Object o) + { + return "material_" + GlTF_Writer.GetNameFromObject(o, false); } + public override void Write() + { + Indent(); jsonWriter.Write("\"" + name + "\": {\n"); + IndentIn(); + CommaNL(); + Indent(); jsonWriter.Write("\"technique\": \"" + instanceTechniqueName + "\",\n"); + Indent(); jsonWriter.Write("\"values\": {\n"); + IndentIn(); + foreach (var v in values) + { + CommaNL(); + Indent(); v.Write(); + } + + + // if (ambient != null) + // { + // CommaNL(); + // ambient.Write (); + // } + // if (diffuse != null) + // { + // CommaNL(); + // diffuse.Write (); + // } + // CommaNL(); + // Indent(); jsonWriter.Write ("\"shininess\": " + shininess); + // if (specular != null) + // { + // CommaNL(); + // specular.Write (); + // } + // jsonWriter.WriteLine(); + + Indent(); jsonWriter.Write("\n"); + IndentOut(); + Indent(); jsonWriter.Write("}"); + CommaNL(); + Indent(); jsonWriter.Write("\"name\": \"" + name + "\"\n"); + IndentOut(); + Indent(); jsonWriter.Write("}"); - // if (ambient != null) - // { - // CommaNL(); - // ambient.Write (); - // } - // if (diffuse != null) - // { - // CommaNL(); - // diffuse.Write (); - // } - // CommaNL(); - // Indent(); jsonWriter.Write ("\"shininess\": " + shininess); - // if (specular != null) - // { - // CommaNL(); - // specular.Write (); - // } - // jsonWriter.WriteLine(); - - Indent(); jsonWriter.Write("\n"); - IndentOut(); - Indent(); jsonWriter.Write("}"); - CommaNL(); - Indent(); jsonWriter.Write("\"name\": \"" + name + "\"\n"); - IndentOut(); - Indent(); jsonWriter.Write("}"); - - } + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_MaterialColor.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_MaterialColor.cs index 49cfc7f3..da641822 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_MaterialColor.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_MaterialColor.cs @@ -1,13 +1,16 @@ using UnityEngine; using System.Collections; -public class GlTF_MaterialColor : GlTF_ColorOrTexture { - public GlTF_MaterialColor(string n, Color c) { - name = n; color = new GlTF_ColorRGBA(name, c); - } - public GlTF_ColorRGBA color = new GlTF_ColorRGBA("diffuse"); - public override void Write() { - // Indent(); jsonWriter.Write ("\"" + name + "\": "); - color.Write(); - } +public class GlTF_MaterialColor : GlTF_ColorOrTexture +{ + public GlTF_MaterialColor(string n, Color c) + { + name = n; color = new GlTF_ColorRGBA(name, c); + } + public GlTF_ColorRGBA color = new GlTF_ColorRGBA("diffuse"); + public override void Write() + { + // Indent(); jsonWriter.Write ("\"" + name + "\": "); + color.Write(); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_MaterialTexture.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_MaterialTexture.cs index 01f7a89c..11ab9a29 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_MaterialTexture.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_MaterialTexture.cs @@ -1,12 +1,15 @@ using UnityEngine; using System.Collections; -public class GlTF_MaterialTexture : GlTF_ColorOrTexture { - public GlTF_MaterialTexture(string n, GlTF_Texture t) { - name = n; texture = t; - } - public GlTF_Texture texture; - public override void Write() { - Indent(); jsonWriter.Write("\"" + name + "\": \"" + texture.name + "\""); - } +public class GlTF_MaterialTexture : GlTF_ColorOrTexture +{ + public GlTF_MaterialTexture(string n, GlTF_Texture t) + { + name = n; texture = t; + } + public GlTF_Texture texture; + public override void Write() + { + Indent(); jsonWriter.Write("\"" + name + "\": \"" + texture.name + "\""); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Matrix.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Matrix.cs index 31b0ecab..3a30a293 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Matrix.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Matrix.cs @@ -1,21 +1,24 @@ using UnityEngine; using System.Collections; -public class GlTF_Matrix : GlTF_FloatArray { - public GlTF_Matrix() { - name = "matrix"; minItems = 16; maxItems = 16; items = new float[] { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }; - } - public GlTF_Matrix(Matrix4x4 m) { - name = "matrix"; - minItems = 16; - maxItems = 16; - // unity: m[row][col] - // gltf: column major - items = new float[] { +public class GlTF_Matrix : GlTF_FloatArray +{ + public GlTF_Matrix() + { + name = "matrix"; minItems = 16; maxItems = 16; items = new float[] { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }; + } + public GlTF_Matrix(Matrix4x4 m) + { + name = "matrix"; + minItems = 16; + maxItems = 16; + // unity: m[row][col] + // gltf: column major + items = new float[] { m.m00, m.m10, m.m20, m.m30, m.m01, m.m11, m.m21, m.m31, m.m02, m.m12, m.m22, m.m32, m.m03, m.m13, m.m23, m.m33 }; - } + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Mesh.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Mesh.cs index 5e262914..e0bd3ebe 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Mesh.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Mesh.cs @@ -2,44 +2,52 @@ using System.Collections; using System.Collections.Generic; -public class GlTF_Mesh : GlTF_Writer { - public List primitives; +public class GlTF_Mesh : GlTF_Writer +{ + public List primitives; - public GlTF_Mesh() { - primitives = new List(); - } - - public static string GetNameFromObject(Object o) { - return "mesh_" + GlTF_Writer.GetNameFromObject(o, true); - } + public GlTF_Mesh() + { + primitives = new List(); + } - public void Populate(Mesh m) { - if (primitives.Count > 0) { - // only populate first attributes because the data are shared between primitives - primitives[0].attributes.Populate(m); + public static string GetNameFromObject(Object o) + { + return "mesh_" + GlTF_Writer.GetNameFromObject(o, true); } - foreach (GlTF_Primitive p in primitives) { - p.Populate(m); + public void Populate(Mesh m) + { + if (primitives.Count > 0) + { + // only populate first attributes because the data are shared between primitives + primitives[0].attributes.Populate(m); + } + + foreach (GlTF_Primitive p in primitives) + { + p.Populate(m); + } } - } - public override void Write() { - Indent(); jsonWriter.Write("\"" + name + "\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"name\": \"" + name + "\",\n"); - Indent(); jsonWriter.Write("\"primitives\": [\n"); - IndentIn(); - foreach (GlTF_Primitive p in primitives) { - CommaNL(); - Indent(); jsonWriter.Write("{\n"); - p.Write(); - Indent(); jsonWriter.Write("}"); + public override void Write() + { + Indent(); jsonWriter.Write("\"" + name + "\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"name\": \"" + name + "\",\n"); + Indent(); jsonWriter.Write("\"primitives\": [\n"); + IndentIn(); + foreach (GlTF_Primitive p in primitives) + { + CommaNL(); + Indent(); jsonWriter.Write("{\n"); + p.Write(); + Indent(); jsonWriter.Write("}"); + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); jsonWriter.Write("]\n"); + IndentOut(); + Indent(); jsonWriter.Write("}"); } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); jsonWriter.Write("]\n"); - IndentOut(); - Indent(); jsonWriter.Write("}"); - } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Node.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Node.cs index a895b48a..0d8cc3ab 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Node.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Node.cs @@ -2,85 +2,100 @@ using System.Collections; using System.Collections.Generic; -public class GlTF_Node : GlTF_Writer { - public string cameraName; - public bool hasParent = false; - public List childrenNames = new List(); - public bool uniqueItems = true; - public string lightName; - public List bufferViewNames = new List(); - public List indexNames = new List(); - public List accessorNames = new List(); - public List meshNames = new List(); - public GlTF_Matrix matrix; - // public GlTF_Mesh mesh; - public GlTF_Rotation rotation; - public GlTF_Scale scale; - public GlTF_Translation translation; - public bool additionalProperties = false; +public class GlTF_Node : GlTF_Writer +{ + public string cameraName; + public bool hasParent = false; + public List childrenNames = new List(); + public bool uniqueItems = true; + public string lightName; + public List bufferViewNames = new List(); + public List indexNames = new List(); + public List accessorNames = new List(); + public List meshNames = new List(); + public GlTF_Matrix matrix; + // public GlTF_Mesh mesh; + public GlTF_Rotation rotation; + public GlTF_Scale scale; + public GlTF_Translation translation; + public bool additionalProperties = false; - public static string GetNameFromObject(Object o) { - return "node_" + GlTF_Writer.GetNameFromObject(o, false); - } - - public override void Write() { - Indent(); - jsonWriter.Write("\"" + name + "\": {\n"); - IndentIn(); - Indent(); - jsonWriter.Write("\"name\": \"" + name + "\",\n"); - if (cameraName != null) { - CommaNL(); - Indent(); - jsonWriter.Write("\"camera\": \"" + cameraName + "\""); - } else if (lightName != null) { - CommaNL(); - Indent(); - jsonWriter.Write("\"light\": \"" + lightName + "\""); - } else if (meshNames.Count > 0) { - CommaNL(); - Indent(); - jsonWriter.Write("\"meshes\": [\n"); - IndentIn(); - foreach (string m in meshNames) { - CommaNL(); - Indent(); jsonWriter.Write("\"" + m + "\""); - } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); jsonWriter.Write("]"); + public static string GetNameFromObject(Object o) + { + return "node_" + GlTF_Writer.GetNameFromObject(o, false); } - if (childrenNames != null && childrenNames.Count > 0) { - CommaNL(); - Indent(); jsonWriter.Write("\"children\": [\n"); - IndentIn(); - foreach (string ch in childrenNames) { - CommaNL(); - Indent(); jsonWriter.Write("\"" + ch + "\""); - } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); jsonWriter.Write("]"); - } - if (matrix != null) { - CommaNL(); - matrix.Write(); - } - if (translation != null && (translation.items[0] != 0f || translation.items[1] != 0f || translation.items[2] != 0f)) { - CommaNL(); - translation.Write(); - } - if (scale != null && (scale.items[0] != 1f || scale.items[1] != 1f || scale.items[2] != 1f)) { - CommaNL(); - scale.Write(); - } - if (rotation != null && (rotation.items[0] != 0f || rotation.items[1] != 0f || rotation.items[2] != 0f || rotation.items[3] != 0f)) { - CommaNL(); - rotation.Write(); + public override void Write() + { + Indent(); + jsonWriter.Write("\"" + name + "\": {\n"); + IndentIn(); + Indent(); + jsonWriter.Write("\"name\": \"" + name + "\",\n"); + if (cameraName != null) + { + CommaNL(); + Indent(); + jsonWriter.Write("\"camera\": \"" + cameraName + "\""); + } + else if (lightName != null) + { + CommaNL(); + Indent(); + jsonWriter.Write("\"light\": \"" + lightName + "\""); + } + else if (meshNames.Count > 0) + { + CommaNL(); + Indent(); + jsonWriter.Write("\"meshes\": [\n"); + IndentIn(); + foreach (string m in meshNames) + { + CommaNL(); + Indent(); jsonWriter.Write("\"" + m + "\""); + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); jsonWriter.Write("]"); + } + + if (childrenNames != null && childrenNames.Count > 0) + { + CommaNL(); + Indent(); jsonWriter.Write("\"children\": [\n"); + IndentIn(); + foreach (string ch in childrenNames) + { + CommaNL(); + Indent(); jsonWriter.Write("\"" + ch + "\""); + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); jsonWriter.Write("]"); + } + if (matrix != null) + { + CommaNL(); + matrix.Write(); + } + if (translation != null && (translation.items[0] != 0f || translation.items[1] != 0f || translation.items[2] != 0f)) + { + CommaNL(); + translation.Write(); + } + if (scale != null && (scale.items[0] != 1f || scale.items[1] != 1f || scale.items[2] != 1f)) + { + CommaNL(); + scale.Write(); + } + if (rotation != null && (rotation.items[0] != 0f || rotation.items[1] != 0f || rotation.items[2] != 0f || rotation.items[3] != 0f)) + { + CommaNL(); + rotation.Write(); + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); jsonWriter.Write("}"); } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); jsonWriter.Write("}"); - } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Orthographic.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Orthographic.cs index bb25ad72..157bdbaf 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Orthographic.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Orthographic.cs @@ -1,14 +1,17 @@ using UnityEngine; using System.Collections; -public class GlTF_Orthographic : GlTF_Camera { - public float xmag; - public float ymag; - public float zfar; - public float znear; - public GlTF_Orthographic() { - type = "orthographic"; - } - public override void Write() { - } +public class GlTF_Orthographic : GlTF_Camera +{ + public float xmag; + public float ymag; + public float zfar; + public float znear; + public GlTF_Orthographic() + { + type = "orthographic"; + } + public override void Write() + { + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Perspective.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Perspective.cs index 08211e17..dc47d1ef 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Perspective.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Perspective.cs @@ -1,37 +1,40 @@ using UnityEngine; using System.Collections; -public class GlTF_Perspective : GlTF_Camera { - public float aspect_ratio; - public float yfov;//": 37.8492, - public float zfar;//": 100, - public float znear;//": 0.01 - public GlTF_Perspective() { - type = "perspective"; - } - public override void Write() { - /* - "camera_0": { - "perspective": { - "yfov": 45, - "zfar": 3162.76, - "znear": 12.651 - }, - "type": "perspective" - } - */ - Indent(); jsonWriter.Write("\"" + name + "\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"perspective\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"aspect_ratio\": " + aspect_ratio.ToString() + ",\n"); - Indent(); jsonWriter.Write("\"yfov\": " + yfov.ToString() + ",\n"); - Indent(); jsonWriter.Write("\"zfar\": " + zfar.ToString() + ",\n"); - Indent(); jsonWriter.Write("\"znear\": " + znear.ToString() + "\n"); - IndentOut(); - Indent(); jsonWriter.Write("},\n"); - Indent(); jsonWriter.Write("\"type\": \"perspective\"\n"); - IndentOut(); - Indent(); jsonWriter.Write("}"); - } +public class GlTF_Perspective : GlTF_Camera +{ + public float aspect_ratio; + public float yfov;//": 37.8492, + public float zfar;//": 100, + public float znear;//": 0.01 + public GlTF_Perspective() + { + type = "perspective"; + } + public override void Write() + { + /* + "camera_0": { + "perspective": { + "yfov": 45, + "zfar": 3162.76, + "znear": 12.651 + }, + "type": "perspective" + } + */ + Indent(); jsonWriter.Write("\"" + name + "\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"perspective\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"aspect_ratio\": " + aspect_ratio.ToString() + ",\n"); + Indent(); jsonWriter.Write("\"yfov\": " + yfov.ToString() + ",\n"); + Indent(); jsonWriter.Write("\"zfar\": " + zfar.ToString() + ",\n"); + Indent(); jsonWriter.Write("\"znear\": " + znear.ToString() + "\n"); + IndentOut(); + Indent(); jsonWriter.Write("},\n"); + Indent(); jsonWriter.Write("\"type\": \"perspective\"\n"); + IndentOut(); + Indent(); jsonWriter.Write("}"); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_PointLight.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_PointLight.cs index 3f471690..3dc300d5 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_PointLight.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_PointLight.cs @@ -1,20 +1,23 @@ using UnityEngine; using System.Collections; -public class GlTF_PointLight : GlTF_Light { - public float constantAttenuation = 1f; - public float linearAttenuation = 0f; - public float quadraticAttenuation = 0f; +public class GlTF_PointLight : GlTF_Light +{ + public float constantAttenuation = 1f; + public float linearAttenuation = 0f; + public float quadraticAttenuation = 0f; - public GlTF_PointLight() { - type = "point"; - } + public GlTF_PointLight() + { + type = "point"; + } - public override void Write() { - color.Write(); - Indent(); jsonWriter.Write("\"constantAttentuation\": " + constantAttenuation); - Indent(); jsonWriter.Write("\"linearAttenuation\": " + linearAttenuation); - Indent(); jsonWriter.Write("\"quadraticAttenuation\": " + quadraticAttenuation); - jsonWriter.Write("}"); - } + public override void Write() + { + color.Write(); + Indent(); jsonWriter.Write("\"constantAttentuation\": " + constantAttenuation); + Indent(); jsonWriter.Write("\"linearAttenuation\": " + linearAttenuation); + Indent(); jsonWriter.Write("\"quadraticAttenuation\": " + quadraticAttenuation); + jsonWriter.Write("}"); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Primitive.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Primitive.cs index 76057757..910e7ca1 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Primitive.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Primitive.cs @@ -1,34 +1,39 @@ using UnityEngine; using System.Collections; -public class GlTF_Primitive : GlTF_Writer { - public GlTF_Attributes attributes = new GlTF_Attributes(); - public GlTF_Accessor indices; - public string materialName; - public int primitive = 4; - public int semantics = 4; - public int index = 0; +public class GlTF_Primitive : GlTF_Writer +{ + public GlTF_Attributes attributes = new GlTF_Attributes(); + public GlTF_Accessor indices; + public string materialName; + public int primitive = 4; + public int semantics = 4; + public int index = 0; - public static string GetNameFromObject(Object o, int index) { - return "primitive_" + index + "_" + GlTF_Writer.GetNameFromObject(o, false); - } + public static string GetNameFromObject(Object o, int index) + { + return "primitive_" + index + "_" + GlTF_Writer.GetNameFromObject(o, false); + } - public void Populate(Mesh m) { - if (m.GetTopology(index) == MeshTopology.Triangles) { - indices.PopulateUshort(m.GetTriangles(index), true); + public void Populate(Mesh m) + { + if (m.GetTopology(index) == MeshTopology.Triangles) + { + indices.PopulateUshort(m.GetTriangles(index), true); + } } - } - public override void Write() { - IndentIn(); - CommaNL(); - if (attributes != null) - attributes.Write(); - CommaNL(); - Indent(); jsonWriter.Write("\"indices\": \"" + indices.name + "\",\n"); - Indent(); jsonWriter.Write("\"material\": \"" + materialName + "\",\n"); - Indent(); jsonWriter.Write("\"mode\": " + primitive + "\n"); - // semantics - IndentOut(); - } + public override void Write() + { + IndentIn(); + CommaNL(); + if (attributes != null) + attributes.Write(); + CommaNL(); + Indent(); jsonWriter.Write("\"indices\": \"" + indices.name + "\",\n"); + Indent(); jsonWriter.Write("\"material\": \"" + materialName + "\",\n"); + Indent(); jsonWriter.Write("\"mode\": " + primitive + "\n"); + // semantics + IndentOut(); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Program.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Program.cs index 7bd491ec..ee68fb69 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Program.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Program.cs @@ -2,30 +2,34 @@ using System.Collections; using System.Collections.Generic; -public class GlTF_Program : GlTF_Writer { - public List attributes = new List(); - public string vertexShader = ""; - public string fragmentShader = ""; +public class GlTF_Program : GlTF_Writer +{ + public List attributes = new List(); + public string vertexShader = ""; + public string fragmentShader = ""; - public static string GetNameFromObject(Object o) { - return "program_" + GlTF_Writer.GetNameFromObject(o); - } + public static string GetNameFromObject(Object o) + { + return "program_" + GlTF_Writer.GetNameFromObject(o); + } - public override void Write() { - Indent(); jsonWriter.Write("\"" + name + "\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"attributes\": [\n"); - IndentIn(); - foreach (var a in attributes) { - CommaNL(); - Indent(); jsonWriter.Write("\"" + a + "\""); + public override void Write() + { + Indent(); jsonWriter.Write("\"" + name + "\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"attributes\": [\n"); + IndentIn(); + foreach (var a in attributes) + { + CommaNL(); + Indent(); jsonWriter.Write("\"" + a + "\""); + } + Indent(); jsonWriter.Write("\n"); + IndentOut(); + Indent(); jsonWriter.Write("],\n"); + Indent(); jsonWriter.Write("\"vertexShader\": \"" + vertexShader + "\",\n"); + Indent(); jsonWriter.Write("\"fragmentShader\": \"" + fragmentShader + "\"\n"); + IndentOut(); + Indent(); jsonWriter.Write("}"); } - Indent(); jsonWriter.Write("\n"); - IndentOut(); - Indent(); jsonWriter.Write("],\n"); - Indent(); jsonWriter.Write("\"vertexShader\": \"" + vertexShader + "\",\n"); - Indent(); jsonWriter.Write("\"fragmentShader\": \"" + fragmentShader + "\"\n"); - IndentOut(); - Indent(); jsonWriter.Write("}"); - } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Rotation.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Rotation.cs index a2bbaf90..973fed8b 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Rotation.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Rotation.cs @@ -1,16 +1,18 @@ using UnityEngine; using System.Collections; -public class GlTF_Rotation : GlTF_FloatArray4 { - public GlTF_Rotation(Quaternion q) { - name = "rotation"; minItems = 4; maxItems = 4; items = new float[] { q.x, q.y, q.z, q.w }; - } - /* - public override void Write() - { - Indent(); jsonWriter.Write ("\"rotation\": [ "); - WriteVals(); - jsonWriter.Write ("]"); - } -*/ +public class GlTF_Rotation : GlTF_FloatArray4 +{ + public GlTF_Rotation(Quaternion q) + { + name = "rotation"; minItems = 4; maxItems = 4; items = new float[] { q.x, q.y, q.z, q.w }; + } + /* + public override void Write() + { + Indent(); jsonWriter.Write ("\"rotation\": [ "); + WriteVals(); + jsonWriter.Write ("]"); + } + */ } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Sampler.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Sampler.cs index 8ad7cbb2..fcdea327 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Sampler.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Sampler.cs @@ -1,114 +1,142 @@ using UnityEngine; using System.Collections; -public class GlTF_Sampler : GlTF_Writer { - public enum MagFilter { - NEAREST = 9728, - LINEAR = 9729 - } - - public enum MinFilter { - NEAREST = 9728, - LINEAR = 9729, - NEAREST_MIPMAP_NEAREST = 9984, - LINEAR_MIPMAP_NEAREST = 9985, - NEAREST_MIPMAP_LINEAR = 9986, - LINEAR_MIPMAP_LINEAR = 9987 - } - - public enum Wrap { - CLAMP_TO_EDGE = 33071, - MIRRORED_REPEAT = 33648, - REPEAT = 10497 - } - - public MagFilter magFilter = MagFilter.LINEAR; - public MinFilter minFilter = MinFilter.LINEAR; - public Wrap wrap = Wrap.REPEAT; +public class GlTF_Sampler : GlTF_Writer +{ + public enum MagFilter + { + NEAREST = 9728, + LINEAR = 9729 + } - // Samplers are only distinguished by their filter settings, so no need for unique naming beyond - // that. - public string ComputeName() { - return "sampler_" + magFilter + "_" + minFilter + "_" + wrap; - } + public enum MinFilter + { + NEAREST = 9728, + LINEAR = 9729, + NEAREST_MIPMAP_NEAREST = 9984, + LINEAR_MIPMAP_NEAREST = 9985, + NEAREST_MIPMAP_LINEAR = 9986, + LINEAR_MIPMAP_LINEAR = 9987 + } - public static string GetNameFromObject(Texture tex) { - int fm = (int) tex.filterMode; - int w = (int) tex.wrapMode; - var n = "sampler_" + fm + "_" + w; - Texture2D t = tex as Texture2D; - if (t != null) { - if (t.mipmapCount > 0) { - n += "_m"; - } + public enum Wrap + { + CLAMP_TO_EDGE = 33071, + MIRRORED_REPEAT = 33648, + REPEAT = 10497 } - return n; - } - public GlTF_Sampler() {} + public MagFilter magFilter = MagFilter.LINEAR; + public MinFilter minFilter = MinFilter.LINEAR; + public Wrap wrap = Wrap.REPEAT; - public GlTF_Sampler(Texture tex) { - bool hasMipMap = false; - Texture2D t = tex as Texture2D; - if (t != null) { - if (t.mipmapCount > 0) { - hasMipMap = true; - } + // Samplers are only distinguished by their filter settings, so no need for unique naming beyond + // that. + public string ComputeName() + { + return "sampler_" + magFilter + "_" + minFilter + "_" + wrap; } - switch (tex.filterMode) { - case FilterMode.Point: { - magFilter = MagFilter.NEAREST; - if (hasMipMap) { - minFilter = MinFilter.NEAREST_MIPMAP_NEAREST; - } else { - minFilter = MinFilter.NEAREST; - } + public static string GetNameFromObject(Texture tex) + { + int fm = (int)tex.filterMode; + int w = (int)tex.wrapMode; + var n = "sampler_" + fm + "_" + w; + Texture2D t = tex as Texture2D; + if (t != null) + { + if (t.mipmapCount > 0) + { + n += "_m"; + } } - break; + return n; + } - case FilterMode.Bilinear: { - magFilter = MagFilter.LINEAR; - if (hasMipMap) { - minFilter = MinFilter.LINEAR_MIPMAP_NEAREST; - } else { - minFilter = MinFilter.LINEAR; - } - } - break; + public GlTF_Sampler() { } - case FilterMode.Trilinear: { - magFilter = MagFilter.LINEAR; - if (hasMipMap) { - minFilter = MinFilter.LINEAR_MIPMAP_LINEAR; - } else { - minFilter = MinFilter.LINEAR; - } + public GlTF_Sampler(Texture tex) + { + bool hasMipMap = false; + Texture2D t = tex as Texture2D; + if (t != null) + { + if (t.mipmapCount > 0) + { + hasMipMap = true; + } } - break; - } - switch (tex.wrapMode) { - case TextureWrapMode.Clamp: { - wrap = Wrap.CLAMP_TO_EDGE; + switch (tex.filterMode) + { + case FilterMode.Point: + { + magFilter = MagFilter.NEAREST; + if (hasMipMap) + { + minFilter = MinFilter.NEAREST_MIPMAP_NEAREST; + } + else + { + minFilter = MinFilter.NEAREST; + } + } + break; + + case FilterMode.Bilinear: + { + magFilter = MagFilter.LINEAR; + if (hasMipMap) + { + minFilter = MinFilter.LINEAR_MIPMAP_NEAREST; + } + else + { + minFilter = MinFilter.LINEAR; + } + } + break; + + case FilterMode.Trilinear: + { + magFilter = MagFilter.LINEAR; + if (hasMipMap) + { + minFilter = MinFilter.LINEAR_MIPMAP_LINEAR; + } + else + { + minFilter = MinFilter.LINEAR; + } + } + break; } - break; - case TextureWrapMode.Repeat: { - wrap = Wrap.REPEAT; + switch (tex.wrapMode) + { + case TextureWrapMode.Clamp: + { + wrap = Wrap.CLAMP_TO_EDGE; + } + break; + + case TextureWrapMode.Repeat: + { + wrap = Wrap.REPEAT; + } + break; } - break; } - } - public override void Write() { - Indent(); jsonWriter.Write("\"" + name + "\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"magFilter\": " + (int) magFilter + ",\n"); - Indent(); jsonWriter.Write("\"minFilter\": " + (int) minFilter + ",\n"); - Indent(); jsonWriter.Write("\"wrapS\": " + (int) wrap + ",\n"); - Indent(); jsonWriter.Write("\"wrapT\": " + (int) wrap + "\n"); - IndentOut(); - Indent(); jsonWriter.Write("}"); - } + public override void Write() + { + Indent(); jsonWriter.Write("\"" + name + "\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"magFilter\": " + (int)magFilter + ",\n"); + Indent(); jsonWriter.Write("\"minFilter\": " + (int)minFilter + ",\n"); + Indent(); jsonWriter.Write("\"wrapS\": " + (int)wrap + ",\n"); + Indent(); jsonWriter.Write("\"wrapT\": " + (int)wrap + "\n"); + IndentOut(); + Indent(); jsonWriter.Write("}"); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Scale.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Scale.cs index 83302252..96560ab8 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Scale.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Scale.cs @@ -1,16 +1,20 @@ using UnityEngine; using System.Collections; -public class GlTF_Scale : GlTF_Vector3 { - public GlTF_Scale() { - items = new float[] { 1f, 1f, 1f }; - } - public GlTF_Scale(Vector3 v) { - items = new float[] { v.x, v.y, v.z }; - } - public override void Write() { - Indent(); jsonWriter.Write("\"scale\": [ "); - WriteVals(); - jsonWriter.Write("]"); - } +public class GlTF_Scale : GlTF_Vector3 +{ + public GlTF_Scale() + { + items = new float[] { 1f, 1f, 1f }; + } + public GlTF_Scale(Vector3 v) + { + items = new float[] { v.x, v.y, v.z }; + } + public override void Write() + { + Indent(); jsonWriter.Write("\"scale\": [ "); + WriteVals(); + jsonWriter.Write("]"); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Shader.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Shader.cs index 2da3d0be..1bcbc0a9 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Shader.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Shader.cs @@ -1,37 +1,45 @@ using UnityEngine; using System.Collections; -public class GlTF_Shader : GlTF_Writer { - public enum Type { - Vertex, - Fragment - } - - public Type type = Type.Vertex; - public string uri = ""; +public class GlTF_Shader : GlTF_Writer +{ + public enum Type + { + Vertex, + Fragment + } - public static string GetNameFromObject(Object o, Type type) { - var name = GlTF_Writer.GetNameFromObject(o); - var typeName = type == Type.Vertex ? "vertex" : "fragment"; - return typeName + "_" + name; - } + public Type type = Type.Vertex; + public string uri = ""; - public override void Write() { - Indent(); jsonWriter.Write("\"" + name + "\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"type\": " + TypeStr() + ",\n"); - Indent(); jsonWriter.Write("\"uri\": \"" + uri + "\"\n"); - IndentOut(); - Indent(); jsonWriter.Write("}"); - } + public static string GetNameFromObject(Object o, Type type) + { + var name = GlTF_Writer.GetNameFromObject(o); + var typeName = type == Type.Vertex ? "vertex" : "fragment"; + return typeName + "_" + name; + } - private int TypeStr() { - if (type == Type.Vertex) { - return 35633; - } else if (type == Type.Fragment) { - return 35632; + public override void Write() + { + Indent(); jsonWriter.Write("\"" + name + "\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"type\": " + TypeStr() + ",\n"); + Indent(); jsonWriter.Write("\"uri\": \"" + uri + "\"\n"); + IndentOut(); + Indent(); jsonWriter.Write("}"); } - return 0; - } + private int TypeStr() + { + if (type == Type.Vertex) + { + return 35633; + } + else if (type == Type.Fragment) + { + return 35632; + } + + return 0; + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_SpotLight.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_SpotLight.cs index 7badaa30..e701b5fa 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_SpotLight.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_SpotLight.cs @@ -1,24 +1,27 @@ using UnityEngine; using System.Collections; -public class GlTF_SpotLight : GlTF_Light { - public float constantAttenuation = 1f; - public float fallOffAngle = 3.1415927f; - public float fallOffExponent = 0f; - public float linearAttenuation = 0f; - public float quadraticAttenuation = 0f; +public class GlTF_SpotLight : GlTF_Light +{ + public float constantAttenuation = 1f; + public float fallOffAngle = 3.1415927f; + public float fallOffExponent = 0f; + public float linearAttenuation = 0f; + public float quadraticAttenuation = 0f; - public GlTF_SpotLight() { - type = "spot"; - } + public GlTF_SpotLight() + { + type = "spot"; + } - public override void Write() { - color.Write(); - Indent(); jsonWriter.Write("\"constantAttentuation\": " + constantAttenuation); - Indent(); jsonWriter.Write("\"fallOffAngle\": " + fallOffAngle); - Indent(); jsonWriter.Write("\"fallOffExponent\": " + fallOffExponent); - Indent(); jsonWriter.Write("\"linearAttenuation\": " + linearAttenuation); - Indent(); jsonWriter.Write("\"quadraticAttenuation\": " + quadraticAttenuation); - jsonWriter.Write("}"); - } + public override void Write() + { + color.Write(); + Indent(); jsonWriter.Write("\"constantAttentuation\": " + constantAttenuation); + Indent(); jsonWriter.Write("\"fallOffAngle\": " + fallOffAngle); + Indent(); jsonWriter.Write("\"fallOffExponent\": " + fallOffExponent); + Indent(); jsonWriter.Write("\"linearAttenuation\": " + linearAttenuation); + Indent(); jsonWriter.Write("\"quadraticAttenuation\": " + quadraticAttenuation); + jsonWriter.Write("}"); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Target.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Target.cs index 307472e1..2df3ed6e 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Target.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Target.cs @@ -1,16 +1,18 @@ using UnityEngine; using System.Collections; -public class GlTF_Target : GlTF_Writer { - public string id; - public string path; - public override void Write() { - Indent(); jsonWriter.Write("\"" + "target" + "\": {\n"); - // Indent(); jsonWriter.Write ("\"" + name + "\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"id\": \"" + id + "\",\n"); - Indent(); jsonWriter.Write("\"path\": \"" + path + "\"\n"); - IndentOut(); - Indent(); jsonWriter.Write("}"); - } +public class GlTF_Target : GlTF_Writer +{ + public string id; + public string path; + public override void Write() + { + Indent(); jsonWriter.Write("\"" + "target" + "\": {\n"); + // Indent(); jsonWriter.Write ("\"" + name + "\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"id\": \"" + id + "\",\n"); + Indent(); jsonWriter.Write("\"path\": \"" + path + "\"\n"); + IndentOut(); + Indent(); jsonWriter.Write("}"); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Technique.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Technique.cs index 845463cc..43a499f4 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Technique.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Technique.cs @@ -4,307 +4,346 @@ using System.Collections.Generic; using Object = UnityEngine.Object; -public class GlTF_Technique : GlTF_Writer { - public enum Type { - FLOAT = 5126, - FLOAT_VEC2 = 35664, - FLOAT_VEC3 = 35665, - FLOAT_VEC4 = 35666, - FLOAT_MAT3 = 35675, - FLOAT_MAT4 = 35676, - SAMPLER_2D = 35678 - } - - public enum Enable { - BLEND = 3042, - CULL_FACE = 2884, - DEPTH_TEST = 2929, - POLYGON_OFFSET_FILL = 32823, - SAMPLE_ALPHA_TO_COVERAGE = 32926, - SCISSOR_TEST = 3089 - } - - [System.Serializable] - public enum Semantic { - UNKNOWN, - POSITION, - NORMAL, - TANGENT, - COLOR, - TEXCOORD_0, - TEXCOORD_1, - TEXCOORD_2, - TEXCOORD_3, - MODELVIEW, - PROJECTION, - MODELVIEWINVERSETRANSPOSE, - MODELINVERSETRANSPOSE, - CESIUM_RTC_MODELVIEW - } - - public class Parameter { - public string name; - public Type type; - public Semantic semantic = Semantic.UNKNOWN; - public string node; - } - - public class Attribute { - public string name; - public string param; - } - - public class Uniform { - public string name; - public string param; - } - - public class States { - public List enable; - public Dictionary functions = new Dictionary(); - } - - public class Value : GlTF_Writer { - public enum Type { - Unknown, - Bool, - Int, - Float, - Color, - Vector2, - Vector4, - IntArr, - BoolArr +public class GlTF_Technique : GlTF_Writer +{ + public enum Type + { + FLOAT = 5126, + FLOAT_VEC2 = 35664, + FLOAT_VEC3 = 35665, + FLOAT_VEC4 = 35666, + FLOAT_MAT3 = 35675, + FLOAT_MAT4 = 35676, + SAMPLER_2D = 35678 } - private bool boolValue; - private int intValue; - private float floatValue; - private Color colorValue; - private Vector2 vector2Value; - private Vector4 vector4Value; - private int[] intArrValue; - private bool[] boolArrvalue; - private Type type = Type.Unknown; - - public Value(bool value) { - boolValue = value; - type = Type.Bool; + public enum Enable + { + BLEND = 3042, + CULL_FACE = 2884, + DEPTH_TEST = 2929, + POLYGON_OFFSET_FILL = 32823, + SAMPLE_ALPHA_TO_COVERAGE = 32926, + SCISSOR_TEST = 3089 } - public Value(int value) { - intValue = value; - type = Type.Int; + [System.Serializable] + public enum Semantic + { + UNKNOWN, + POSITION, + NORMAL, + TANGENT, + COLOR, + TEXCOORD_0, + TEXCOORD_1, + TEXCOORD_2, + TEXCOORD_3, + MODELVIEW, + PROJECTION, + MODELVIEWINVERSETRANSPOSE, + MODELINVERSETRANSPOSE, + CESIUM_RTC_MODELVIEW } - public Value(float value) { - floatValue = value; - type = Type.Float; + public class Parameter + { + public string name; + public Type type; + public Semantic semantic = Semantic.UNKNOWN; + public string node; } - public Value(Color value) { - colorValue = value; - type = Type.Color; + public class Attribute + { + public string name; + public string param; } - public Value(Vector2 value) { - vector2Value = value; - type = Type.Vector2; + public class Uniform + { + public string name; + public string param; } - public Value(Vector4 value) { - vector4Value = value; - type = Type.Vector4; + public class States + { + public List enable; + public Dictionary functions = new Dictionary(); } - public Value(int[] value) { - intArrValue = value; - type = Type.IntArr; - } + public class Value : GlTF_Writer + { + public enum Type + { + Unknown, + Bool, + Int, + Float, + Color, + Vector2, + Vector4, + IntArr, + BoolArr + } - public Value(bool[] value) { - boolArrvalue = value; - type = Type.BoolArr; - } + private bool boolValue; + private int intValue; + private float floatValue; + private Color colorValue; + private Vector2 vector2Value; + private Vector4 vector4Value; + private int[] intArrValue; + private bool[] boolArrvalue; + private Type type = Type.Unknown; + + public Value(bool value) + { + boolValue = value; + type = Type.Bool; + } - private void WriteArr(T arr) where T : ArrayList { - jsonWriter.Write("["); - for (var i = 0; i < arr.Count; ++i) { - jsonWriter.Write(arr[i].ToString().ToLower()); - if (i != arr.Count - 1) { - jsonWriter.Write(", "); + public Value(int value) + { + intValue = value; + type = Type.Int; } - } - jsonWriter.Write("]"); - } - public override void Write() { - switch (type) { - case Type.Bool: - jsonWriter.Write("[" + boolValue.ToString().ToLower() + "]"); - break; + public Value(float value) + { + floatValue = value; + type = Type.Float; + } - case Type.Int: - jsonWriter.Write("[" + intValue + "]"); - break; + public Value(Color value) + { + colorValue = value; + type = Type.Color; + } - case Type.Float: - jsonWriter.Write("[" + floatValue + "]"); - break; + public Value(Vector2 value) + { + vector2Value = value; + type = Type.Vector2; + } - case Type.Color: - jsonWriter.Write("[" + colorValue.r + ", " + colorValue.g + ", " + colorValue.b + ", " + colorValue.a + "]"); - break; + public Value(Vector4 value) + { + vector4Value = value; + type = Type.Vector4; + } - case Type.Vector2: - jsonWriter.Write("[" + vector2Value.x + ", " + vector2Value.y + "]"); - break; + public Value(int[] value) + { + intArrValue = value; + type = Type.IntArr; + } - case Type.Vector4: - jsonWriter.Write("[" + vector4Value.x + ", " + vector4Value.y + ", " + vector4Value.z + ", " + vector4Value.w + "]"); - break; + public Value(bool[] value) + { + boolArrvalue = value; + type = Type.BoolArr; + } - case Type.IntArr: - WriteArr(new ArrayList(intArrValue)); - break; + private void WriteArr(T arr) where T : ArrayList + { + jsonWriter.Write("["); + for (var i = 0; i < arr.Count; ++i) + { + jsonWriter.Write(arr[i].ToString().ToLower()); + if (i != arr.Count - 1) + { + jsonWriter.Write(", "); + } + } + jsonWriter.Write("]"); + } - case Type.BoolArr: - WriteArr(new ArrayList(boolArrvalue)); - break; + public override void Write() + { + switch (type) + { + case Type.Bool: + jsonWriter.Write("[" + boolValue.ToString().ToLower() + "]"); + break; - } - } - } - - public string program; - public List attributes = new List(); - public List parameters = new List(); - public List uniforms = new List(); - public string materialExtra = ""; - public States states = new States(); - - public static string GetNameFromObject(Object o) { - return "technique_" + GlTF_Writer.GetNameFromObject(o); - } - - public void AddDefaultUniforms(bool rtc) { - var tParam = new Parameter(); - tParam.name = "modelViewMatrix"; - tParam.type = Type.FLOAT_MAT4; - tParam.semantic = rtc ? Semantic.CESIUM_RTC_MODELVIEW : Semantic.MODELVIEW; - parameters.Add(tParam); - var uni = new Uniform(); - uni.name = "u_modelViewMatrix"; - uni.param = tParam.name; - uniforms.Add(uni); - - tParam = new Parameter(); - tParam.name = "projectionMatrix"; - tParam.type = Type.FLOAT_MAT4; - tParam.semantic = Semantic.PROJECTION; - parameters.Add(tParam); - uni = new Uniform(); - uni.name = "u_projectionMatrix"; - uni.param = tParam.name; - uniforms.Add(uni); - - tParam = new Parameter(); - tParam.name = "normalMatrix"; - tParam.type = Type.FLOAT_MAT3; - tParam.semantic = Semantic.MODELVIEWINVERSETRANSPOSE; - parameters.Add(tParam); - uni = new Uniform(); - uni.name = "u_normalMatrix"; - uni.param = tParam.name; - uniforms.Add(uni); - } - - public override void Write() { - Indent(); jsonWriter.Write("\"" + name + "\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"program\": \"" + program + "\",\n"); - Indent(); jsonWriter.Write("\"extras\": " + materialExtra + ",\n"); - Indent(); jsonWriter.Write("\"parameters\": {\n"); - IndentIn(); - foreach (var p in parameters) { - CommaNL(); - Indent(); jsonWriter.Write("\"" + p.name + "\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"type\": " + (int) p.type); - if (p.semantic != Semantic.UNKNOWN) { - jsonWriter.Write(",\n"); - Indent(); jsonWriter.Write("\"semantic\": \"" + p.semantic + "\""); - } - if (p.node != null) { - jsonWriter.Write(",\n"); - Indent(); jsonWriter.Write("\"node\": \"" + p.node + "\"\n"); - } else { - jsonWriter.Write("\n"); - } - IndentOut(); - Indent(); jsonWriter.Write("}"); - } - Indent(); jsonWriter.Write("\n"); - IndentOut(); - Indent(); jsonWriter.Write("},\n"); - - Indent(); jsonWriter.Write("\"attributes\": {\n"); - IndentIn(); - foreach (var a in attributes) { - CommaNL(); - Indent(); jsonWriter.Write("\"" + a.name + "\": \"" + a.param + "\""); - } - Indent(); jsonWriter.Write("\n"); - IndentOut(); - Indent(); jsonWriter.Write("},\n"); - - Indent(); jsonWriter.Write("\"uniforms\": {\n"); - IndentIn(); - foreach (var u in uniforms) { - CommaNL(); - Indent(); jsonWriter.Write("\"" + u.name + "\": \"" + u.param + "\""); + case Type.Int: + jsonWriter.Write("[" + intValue + "]"); + break; + + case Type.Float: + jsonWriter.Write("[" + floatValue + "]"); + break; + + case Type.Color: + jsonWriter.Write("[" + colorValue.r + ", " + colorValue.g + ", " + colorValue.b + ", " + colorValue.a + "]"); + break; + + case Type.Vector2: + jsonWriter.Write("[" + vector2Value.x + ", " + vector2Value.y + "]"); + break; + + case Type.Vector4: + jsonWriter.Write("[" + vector4Value.x + ", " + vector4Value.y + ", " + vector4Value.z + ", " + vector4Value.w + "]"); + break; + + case Type.IntArr: + WriteArr(new ArrayList(intArrValue)); + break; + + case Type.BoolArr: + WriteArr(new ArrayList(boolArrvalue)); + break; + + } + } } - Indent(); jsonWriter.Write("\n"); - IndentOut(); - Indent(); jsonWriter.Write("},\n"); - - // states - Indent(); jsonWriter.Write("\"states\": {\n"); - IndentIn(); - - if (states != null && states.enable != null) { - Indent(); jsonWriter.Write("\"enable\": [\n"); - IndentIn(); - foreach (var en in states.enable) { - CommaNL(); - Indent(); jsonWriter.Write((int) en); - } - jsonWriter.Write("\n"); - IndentOut(); - Indent(); jsonWriter.Write("]"); + + public string program; + public List attributes = new List(); + public List parameters = new List(); + public List uniforms = new List(); + public string materialExtra = ""; + public States states = new States(); + + public static string GetNameFromObject(Object o) + { + return "technique_" + GlTF_Writer.GetNameFromObject(o); } - if (states != null && states.functions.Count > 0) { - jsonWriter.Write(",\n"); - Indent(); jsonWriter.Write("\"functions\": {\n"); - IndentIn(); - foreach (var fun in states.functions) { - CommaNL(); - Indent(); jsonWriter.Write("\"" + fun.Key + "\": "); - fun.Value.Write(); - } - jsonWriter.Write("\n"); - IndentOut(); - Indent(); jsonWriter.Write("}"); - jsonWriter.Write("\n"); - } else { - jsonWriter.Write("\n"); + public void AddDefaultUniforms(bool rtc) + { + var tParam = new Parameter(); + tParam.name = "modelViewMatrix"; + tParam.type = Type.FLOAT_MAT4; + tParam.semantic = rtc ? Semantic.CESIUM_RTC_MODELVIEW : Semantic.MODELVIEW; + parameters.Add(tParam); + var uni = new Uniform(); + uni.name = "u_modelViewMatrix"; + uni.param = tParam.name; + uniforms.Add(uni); + + tParam = new Parameter(); + tParam.name = "projectionMatrix"; + tParam.type = Type.FLOAT_MAT4; + tParam.semantic = Semantic.PROJECTION; + parameters.Add(tParam); + uni = new Uniform(); + uni.name = "u_projectionMatrix"; + uni.param = tParam.name; + uniforms.Add(uni); + + tParam = new Parameter(); + tParam.name = "normalMatrix"; + tParam.type = Type.FLOAT_MAT3; + tParam.semantic = Semantic.MODELVIEWINVERSETRANSPOSE; + parameters.Add(tParam); + uni = new Uniform(); + uni.name = "u_normalMatrix"; + uni.param = tParam.name; + uniforms.Add(uni); } - IndentOut(); - Indent(); jsonWriter.Write("}\n"); + public override void Write() + { + Indent(); jsonWriter.Write("\"" + name + "\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"program\": \"" + program + "\",\n"); + Indent(); jsonWriter.Write("\"extras\": " + materialExtra + ",\n"); + Indent(); jsonWriter.Write("\"parameters\": {\n"); + IndentIn(); + foreach (var p in parameters) + { + CommaNL(); + Indent(); jsonWriter.Write("\"" + p.name + "\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"type\": " + (int)p.type); + if (p.semantic != Semantic.UNKNOWN) + { + jsonWriter.Write(",\n"); + Indent(); jsonWriter.Write("\"semantic\": \"" + p.semantic + "\""); + } + if (p.node != null) + { + jsonWriter.Write(",\n"); + Indent(); jsonWriter.Write("\"node\": \"" + p.node + "\"\n"); + } + else + { + jsonWriter.Write("\n"); + } + IndentOut(); + Indent(); jsonWriter.Write("}"); + } + Indent(); jsonWriter.Write("\n"); + IndentOut(); + Indent(); jsonWriter.Write("},\n"); + + Indent(); jsonWriter.Write("\"attributes\": {\n"); + IndentIn(); + foreach (var a in attributes) + { + CommaNL(); + Indent(); jsonWriter.Write("\"" + a.name + "\": \"" + a.param + "\""); + } + Indent(); jsonWriter.Write("\n"); + IndentOut(); + Indent(); jsonWriter.Write("},\n"); + + Indent(); jsonWriter.Write("\"uniforms\": {\n"); + IndentIn(); + foreach (var u in uniforms) + { + CommaNL(); + Indent(); jsonWriter.Write("\"" + u.name + "\": \"" + u.param + "\""); + } + Indent(); jsonWriter.Write("\n"); + IndentOut(); + Indent(); jsonWriter.Write("},\n"); + + // states + Indent(); jsonWriter.Write("\"states\": {\n"); + IndentIn(); + + if (states != null && states.enable != null) + { + Indent(); jsonWriter.Write("\"enable\": [\n"); + IndentIn(); + foreach (var en in states.enable) + { + CommaNL(); + Indent(); jsonWriter.Write((int)en); + } + jsonWriter.Write("\n"); + IndentOut(); + Indent(); jsonWriter.Write("]"); + } + + if (states != null && states.functions.Count > 0) + { + jsonWriter.Write(",\n"); + Indent(); jsonWriter.Write("\"functions\": {\n"); + IndentIn(); + foreach (var fun in states.functions) + { + CommaNL(); + Indent(); jsonWriter.Write("\"" + fun.Key + "\": "); + fun.Value.Write(); + } + jsonWriter.Write("\n"); + IndentOut(); + Indent(); jsonWriter.Write("}"); + jsonWriter.Write("\n"); + } + else + { + jsonWriter.Write("\n"); + } - IndentOut(); - Indent(); jsonWriter.Write("}"); - } + IndentOut(); + Indent(); jsonWriter.Write("}\n"); + + IndentOut(); + Indent(); jsonWriter.Write("}"); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Texture.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Texture.cs index 44d0f2b8..1f8655a6 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Texture.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Texture.cs @@ -1,38 +1,41 @@ using UnityEngine; using System.Collections; -public class GlTF_Texture : GlTF_Writer { - /* - "texture_O21_jpg": { - "format": 6408, - "internalFormat": 6408, - "sampler": "sampler_0", - "source": "O21_jpg", - "target": 3553, - "type": 5121 - }, -*/ - public int format = 6408; - public int internalFormat = 6408; - public string samplerName; - public string source; - public int target = 3553; - public int tType = 5121; +public class GlTF_Texture : GlTF_Writer +{ + /* + "texture_O21_jpg": { + "format": 6408, + "internalFormat": 6408, + "sampler": "sampler_0", + "source": "O21_jpg", + "target": 3553, + "type": 5121 + }, + */ + public int format = 6408; + public int internalFormat = 6408; + public string samplerName; + public string source; + public int target = 3553; + public int tType = 5121; - public static string GetNameFromObject(Object o) { - return "texture_" + GlTF_Writer.GetNameFromObject(o, false); - } + public static string GetNameFromObject(Object o) + { + return "texture_" + GlTF_Writer.GetNameFromObject(o, false); + } - public override void Write() { - Indent(); jsonWriter.Write("\"" + name + "\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"format\": " + format + ",\n"); - Indent(); jsonWriter.Write("\"internalFormat\": " + internalFormat + ",\n"); - Indent(); jsonWriter.Write("\"sampler\": \"" + samplerName + "\",\n"); - Indent(); jsonWriter.Write("\"source\": \"" + source + "\",\n"); - Indent(); jsonWriter.Write("\"target\": " + target + ",\n"); - Indent(); jsonWriter.Write("\"type\": " + tType + "\n"); - IndentOut(); - Indent(); jsonWriter.Write("}"); - } + public override void Write() + { + Indent(); jsonWriter.Write("\"" + name + "\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"format\": " + format + ",\n"); + Indent(); jsonWriter.Write("\"internalFormat\": " + internalFormat + ",\n"); + Indent(); jsonWriter.Write("\"sampler\": \"" + samplerName + "\",\n"); + Indent(); jsonWriter.Write("\"source\": \"" + source + "\",\n"); + Indent(); jsonWriter.Write("\"target\": " + target + ",\n"); + Indent(); jsonWriter.Write("\"type\": " + tType + "\n"); + IndentOut(); + Indent(); jsonWriter.Write("}"); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Translation.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Translation.cs index 6a65d42a..8f5024e1 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Translation.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Translation.cs @@ -1,13 +1,16 @@ using UnityEngine; using System.Collections; -public class GlTF_Translation : GlTF_Vector3 { - public GlTF_Translation(Vector3 v) { - items = new float[] { v.x, v.y, v.z }; - } - public override void Write() { - Indent(); jsonWriter.Write("\"translation\": [ "); - WriteVals(); - jsonWriter.Write("]"); - } +public class GlTF_Translation : GlTF_Vector3 +{ + public GlTF_Translation(Vector3 v) + { + items = new float[] { v.x, v.y, v.z }; + } + public override void Write() + { + Indent(); jsonWriter.Write("\"translation\": [ "); + WriteVals(); + jsonWriter.Write("]"); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Vector3.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Vector3.cs index bdc0fe5a..1c129e52 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Vector3.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Vector3.cs @@ -1,11 +1,14 @@ using UnityEngine; using System.Collections; -public class GlTF_Vector3 : GlTF_FloatArray { - public GlTF_Vector3() { - minItems = 3; maxItems = 3; items = new float[] { 0f, 0f, 0f }; - } - public GlTF_Vector3(Vector3 v) { - minItems = 3; maxItems = 3; items = new float[] { v.x, v.y, v.z }; - } +public class GlTF_Vector3 : GlTF_FloatArray +{ + public GlTF_Vector3() + { + minItems = 3; maxItems = 3; items = new float[] { 0f, 0f, 0f }; + } + public GlTF_Vector3(Vector3 v) + { + minItems = 3; maxItems = 3; items = new float[] { v.x, v.y, v.z }; + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/GlTF_Writer.cs b/Assets/Scripts/ThirdParty/GlTF/GlTF_Writer.cs index 4e5fec58..4adfa041 100644 --- a/Assets/Scripts/ThirdParty/GlTF/GlTF_Writer.cs +++ b/Assets/Scripts/ThirdParty/GlTF/GlTF_Writer.cs @@ -5,545 +5,609 @@ using System.IO; using System.Collections.Generic; -public class GlTF_Writer { - public static FileStream fs; - public static StreamWriter jsonWriter; - public static BinaryWriter binWriter; - public static Stream binFile; - public static int indent = 0; - public static string binFileName; - public static bool binary; - public static bool b3dm; - public static GlTF_BufferView ushortBufferView = new GlTF_BufferView("ushortBufferView", 34963); - public static GlTF_BufferView floatBufferView = new GlTF_BufferView("floatBufferView"); - public static GlTF_BufferView vec2BufferView = new GlTF_BufferView("vec2BufferView"); - public static GlTF_BufferView vec3BufferView = new GlTF_BufferView("vec3BufferView"); - public static GlTF_BufferView vec4BufferView = new GlTF_BufferView("vec4BufferView"); - public static List bufferViews = new List(); - public static List cameras = new List(); - public static List lights = new List(); - public static List meshes = new List(); - public static List accessors = new List(); - public static Dictionary nodes = new Dictionary(); - public static Dictionary materials = new Dictionary(); - public static Dictionary samplers = new Dictionary(); - public static Dictionary textures = new Dictionary(); - public static List images = new List(); - public static Dictionary techniques = new Dictionary(); - public static List programs = new List(); - public static List shaders = new List(); - // Allows global-scope (key, value) string pairs to be passed along to the glTF consumer. - public static Dictionary extras = new Dictionary(); - - // glTF metadata to write out. - public string Copyright { - get { return OrUnknown(copyright); } - set { copyright = value; } - } - public string Generator { - get { return OrUnknown(generator); } - set { generator = value; } - } - public string Version { - get { return OrUnknown(version); } - set { version = value; } - } - - public double[] RTCCenter; - - public List exportedFiles; - - static public string GetNameFromObject(Object o, bool useId = false) { - var ret = o.name; - ret = ret.Replace(" ", "_"); - ret = ret.Replace("/", "_"); - ret = ret.Replace("\\", "_"); - - if (useId) { - ret += "_" + o.GetInstanceID(); +public class GlTF_Writer +{ + public static FileStream fs; + public static StreamWriter jsonWriter; + public static BinaryWriter binWriter; + public static Stream binFile; + public static int indent = 0; + public static string binFileName; + public static bool binary; + public static bool b3dm; + public static GlTF_BufferView ushortBufferView = new GlTF_BufferView("ushortBufferView", 34963); + public static GlTF_BufferView floatBufferView = new GlTF_BufferView("floatBufferView"); + public static GlTF_BufferView vec2BufferView = new GlTF_BufferView("vec2BufferView"); + public static GlTF_BufferView vec3BufferView = new GlTF_BufferView("vec3BufferView"); + public static GlTF_BufferView vec4BufferView = new GlTF_BufferView("vec4BufferView"); + public static List bufferViews = new List(); + public static List cameras = new List(); + public static List lights = new List(); + public static List meshes = new List(); + public static List accessors = new List(); + public static Dictionary nodes = new Dictionary(); + public static Dictionary materials = new Dictionary(); + public static Dictionary samplers = new Dictionary(); + public static Dictionary textures = new Dictionary(); + public static List images = new List(); + public static Dictionary techniques = new Dictionary(); + public static List programs = new List(); + public static List shaders = new List(); + // Allows global-scope (key, value) string pairs to be passed along to the glTF consumer. + public static Dictionary extras = new Dictionary(); + + // glTF metadata to write out. + public string Copyright + { + get { return OrUnknown(copyright); } + set { copyright = value; } } - return ret; - } - - public void Init() { - firsts = new bool[100]; - ushortBufferView = new GlTF_BufferView("ushortBufferView", 34963); - floatBufferView = new GlTF_BufferView("floatBufferView"); - vec2BufferView = new GlTF_BufferView("vec2BufferView"); - vec3BufferView = new GlTF_BufferView("vec3BufferView"); - vec4BufferView = new GlTF_BufferView("vec4BufferView"); - bufferViews = new List(); - cameras = new List(); - lights = new List(); - meshes = new List(); - accessors = new List(); - nodes = new Dictionary(); - materials = new Dictionary(); - samplers = new Dictionary(); - textures = new Dictionary(); - images = new List(); - techniques = new Dictionary(); - programs = new List(); - shaders = new List(); - exportedFiles = new List(); - } - - public void Indent() { - for (int i = 0; i < indent; i++) - jsonWriter.Write("\t"); - } - - public void IndentIn() { - indent++; - firsts[indent] = true; - } - - public void IndentOut() { - indent--; - } - - public void CommaStart() { - firsts[indent] = false; - } - - public void CommaNL() { - if (!firsts[indent]) - jsonWriter.Write(",\n"); - // else - // jsonWriter.Write ("\n"); - firsts[indent] = false; - } - - public string name; // name of this object - - public void OpenFiles(string filepath) { - fs = File.Open(filepath, FileMode.Create); - - exportedFiles.Add(filepath); - if (binary) { - binWriter = new BinaryWriter(fs); - binFile = fs; - long offset = 20 + (b3dm ? B3DM_HEADER_SIZE : 0); - fs.Seek(offset, SeekOrigin.Begin); // header skip - } else { - // separate bin file - binFileName = Path.GetFileNameWithoutExtension(filepath) + ".bin"; - var binPath = Path.Combine(Path.GetDirectoryName(filepath), binFileName); - binFile = File.Open(binPath, FileMode.Create); - exportedFiles.Add(binPath); + public string Generator + { + get { return OrUnknown(generator); } + set { generator = value; } } - - jsonWriter = new StreamWriter(fs); - jsonWriter.NewLine = "\n"; - } - - public void CloseFiles() { - if (binary) { - binWriter.Close(); - } else { - binFile.Close(); + public string Version + { + get { return OrUnknown(version); } + set { version = value; } } - jsonWriter.Close(); - fs.Close(); - } - - public virtual void Write() { - bufferViews.Add(ushortBufferView); - bufferViews.Add(floatBufferView); - bufferViews.Add(vec2BufferView); - bufferViews.Add(vec3BufferView); - bufferViews.Add(vec4BufferView); - - ushortBufferView.bin = binary; - floatBufferView.bin = binary; - vec2BufferView.bin = binary; - vec3BufferView.bin = binary; - vec4BufferView.bin = binary; - - // write memory streams to binary file - ushortBufferView.byteOffset = 0; - floatBufferView.byteOffset = ushortBufferView.byteLength; - vec2BufferView.byteOffset = floatBufferView.byteOffset + floatBufferView.byteLength; - vec3BufferView.byteOffset = vec2BufferView.byteOffset + vec2BufferView.byteLength; - vec4BufferView.byteOffset = vec3BufferView.byteOffset + vec3BufferView.byteLength; - - jsonWriter.Write("{\n"); - IndentIn(); - - // asset - CommaNL(); - Indent(); jsonWriter.Write("\"asset\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"generator\": \"" + Generator + "\",\n"); - Indent(); jsonWriter.Write("\"version\": \"" + Version + "\",\n"); - Indent(); jsonWriter.Write("\"copyright\": \"" + Copyright + "\"\n"); - IndentOut(); - Indent(); jsonWriter.Write("}"); - - if (!binary) { - // FIX: Should support multiple buffers - CommaNL(); - Indent(); jsonWriter.Write("\"buffers\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"" + Path.GetFileNameWithoutExtension(GlTF_Writer.binFileName) + "\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"byteLength\": " + (vec4BufferView.byteOffset + vec4BufferView.byteLength) + ",\n"); - Indent(); jsonWriter.Write("\"type\": \"arraybuffer\",\n"); - Indent(); jsonWriter.Write("\"uri\": \"" + GlTF_Writer.binFileName + "\"\n"); - - IndentOut(); - Indent(); jsonWriter.Write("}\n"); - - IndentOut(); - Indent(); jsonWriter.Write("}"); - } else { - CommaNL(); - Indent(); jsonWriter.Write("\"buffers\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"binary_glTF\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"byteLength\": " + (vec4BufferView.byteOffset + vec4BufferView.byteLength) + ",\n"); - Indent(); jsonWriter.Write("\"type\": \"arraybuffer\"\n"); - - IndentOut(); - Indent(); jsonWriter.Write("}\n"); - - IndentOut(); - Indent(); jsonWriter.Write("}"); - } + public double[] RTCCenter; - if (cameras != null && cameras.Count > 0) { - CommaNL(); - Indent(); jsonWriter.Write("\"cameras\": {\n"); - IndentIn(); - foreach (GlTF_Camera c in cameras) { - CommaNL(); - c.Write(); - } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); jsonWriter.Write("}"); - } + public List exportedFiles; - if (accessors != null && accessors.Count > 0) { - CommaNL(); - Indent(); jsonWriter.Write("\"accessors\": {\n"); - IndentIn(); - foreach (GlTF_Accessor a in accessors) { - CommaNL(); - a.Write(); - } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); jsonWriter.Write("}"); - } + static public string GetNameFromObject(Object o, bool useId = false) + { + var ret = o.name; + ret = ret.Replace(" ", "_"); + ret = ret.Replace("/", "_"); + ret = ret.Replace("\\", "_"); - if (bufferViews != null && bufferViews.Count > 0) { - CommaNL(); - Indent(); jsonWriter.Write("\"bufferViews\": {\n"); - IndentIn(); - foreach (GlTF_BufferView bv in bufferViews) { - if (bv.byteLength > 0) { - CommaNL(); - bv.Write(); + if (useId) + { + ret += "_" + o.GetInstanceID(); } - } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); jsonWriter.Write("}"); + return ret; } - if (meshes != null && meshes.Count > 0) { - CommaNL(); - Indent(); - jsonWriter.Write("\"meshes\": {\n"); - IndentIn(); - foreach (GlTF_Mesh m in meshes) { - CommaNL(); - m.Write(); - } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); - jsonWriter.Write("}"); + public void Init() + { + firsts = new bool[100]; + ushortBufferView = new GlTF_BufferView("ushortBufferView", 34963); + floatBufferView = new GlTF_BufferView("floatBufferView"); + vec2BufferView = new GlTF_BufferView("vec2BufferView"); + vec3BufferView = new GlTF_BufferView("vec3BufferView"); + vec4BufferView = new GlTF_BufferView("vec4BufferView"); + bufferViews = new List(); + cameras = new List(); + lights = new List(); + meshes = new List(); + accessors = new List(); + nodes = new Dictionary(); + materials = new Dictionary(); + samplers = new Dictionary(); + textures = new Dictionary(); + images = new List(); + techniques = new Dictionary(); + programs = new List(); + shaders = new List(); + exportedFiles = new List(); } - if (shaders != null && shaders.Count > 0) { - CommaNL(); - Indent(); - jsonWriter.Write("\"shaders\": {\n"); - IndentIn(); - foreach (var s in shaders) { - CommaNL(); - s.Write(); - } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); - jsonWriter.Write("}"); + public void Indent() + { + for (int i = 0; i < indent; i++) + jsonWriter.Write("\t"); } - if (programs != null && programs.Count > 0) { - CommaNL(); - Indent(); - jsonWriter.Write("\"programs\": {\n"); - IndentIn(); - foreach (var p in programs) { - CommaNL(); - p.Write(); - } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); - jsonWriter.Write("}"); + public void IndentIn() + { + indent++; + firsts[indent] = true; } - if (techniques != null && techniques.Count > 0) { - CommaNL(); - Indent(); - jsonWriter.Write("\"techniques\": {\n"); - IndentIn(); - foreach (KeyValuePair k in techniques) { - CommaNL(); - k.Value.Write(); - } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); - jsonWriter.Write("}"); + public void IndentOut() + { + indent--; } - if (samplers.Count > 0) { - CommaNL(); - Indent(); jsonWriter.Write("\"samplers\": {\n"); - IndentIn(); - foreach (KeyValuePair s in samplers) { - CommaNL(); - s.Value.Write(); - } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); jsonWriter.Write("}"); + public void CommaStart() + { + firsts[indent] = false; } - if (textures.Count > 0) { - CommaNL(); - Indent(); jsonWriter.Write("\"textures\": {\n"); - IndentIn(); - foreach (KeyValuePair t in textures) { - CommaNL(); - t.Value.Write(); - } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); jsonWriter.Write("}"); + public void CommaNL() + { + if (!firsts[indent]) + jsonWriter.Write(",\n"); + // else + // jsonWriter.Write ("\n"); + firsts[indent] = false; } - if (images.Count > 0) { - CommaNL(); - Indent(); jsonWriter.Write("\"images\": {\n"); - IndentIn(); - foreach (var i in images) { - CommaNL(); - i.Write(); - } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); jsonWriter.Write("}"); + public string name; // name of this object + + public void OpenFiles(string filepath) + { + fs = File.Open(filepath, FileMode.Create); + + exportedFiles.Add(filepath); + if (binary) + { + binWriter = new BinaryWriter(fs); + binFile = fs; + long offset = 20 + (b3dm ? B3DM_HEADER_SIZE : 0); + fs.Seek(offset, SeekOrigin.Begin); // header skip + } + else + { + // separate bin file + binFileName = Path.GetFileNameWithoutExtension(filepath) + ".bin"; + var binPath = Path.Combine(Path.GetDirectoryName(filepath), binFileName); + binFile = File.Open(binPath, FileMode.Create); + exportedFiles.Add(binPath); + } + + jsonWriter = new StreamWriter(fs); + jsonWriter.NewLine = "\n"; } - if (materials.Count > 0) { - CommaNL(); - Indent(); jsonWriter.Write("\"materials\": {\n"); - IndentIn(); - foreach (KeyValuePair m in materials) { - CommaNL(); - m.Value.Write(); - } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); jsonWriter.Write("}"); + public void CloseFiles() + { + if (binary) + { + binWriter.Close(); + } + else + { + binFile.Close(); + } + + jsonWriter.Close(); + fs.Close(); } - if (nodes != null && nodes.Count > 0) { - CommaNL(); - Indent(); jsonWriter.Write("\"nodes\": {\n"); - IndentIn(); - foreach (KeyValuePair n in nodes) { + public virtual void Write() + { + bufferViews.Add(ushortBufferView); + bufferViews.Add(floatBufferView); + bufferViews.Add(vec2BufferView); + bufferViews.Add(vec3BufferView); + bufferViews.Add(vec4BufferView); + + ushortBufferView.bin = binary; + floatBufferView.bin = binary; + vec2BufferView.bin = binary; + vec3BufferView.bin = binary; + vec4BufferView.bin = binary; + + // write memory streams to binary file + ushortBufferView.byteOffset = 0; + floatBufferView.byteOffset = ushortBufferView.byteLength; + vec2BufferView.byteOffset = floatBufferView.byteOffset + floatBufferView.byteLength; + vec3BufferView.byteOffset = vec2BufferView.byteOffset + vec2BufferView.byteLength; + vec4BufferView.byteOffset = vec3BufferView.byteOffset + vec3BufferView.byteLength; + + jsonWriter.Write("{\n"); + IndentIn(); + + // asset CommaNL(); - n.Value.Write(); - } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); jsonWriter.Write("}"); + Indent(); jsonWriter.Write("\"asset\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"generator\": \"" + Generator + "\",\n"); + Indent(); jsonWriter.Write("\"version\": \"" + Version + "\",\n"); + Indent(); jsonWriter.Write("\"copyright\": \"" + Copyright + "\"\n"); + IndentOut(); + Indent(); jsonWriter.Write("}"); + + if (!binary) + { + // FIX: Should support multiple buffers + CommaNL(); + Indent(); jsonWriter.Write("\"buffers\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"" + Path.GetFileNameWithoutExtension(GlTF_Writer.binFileName) + "\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"byteLength\": " + (vec4BufferView.byteOffset + vec4BufferView.byteLength) + ",\n"); + Indent(); jsonWriter.Write("\"type\": \"arraybuffer\",\n"); + Indent(); jsonWriter.Write("\"uri\": \"" + GlTF_Writer.binFileName + "\"\n"); + + IndentOut(); + Indent(); jsonWriter.Write("}\n"); + + IndentOut(); + Indent(); jsonWriter.Write("}"); + } + else + { + CommaNL(); + Indent(); jsonWriter.Write("\"buffers\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"binary_glTF\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"byteLength\": " + (vec4BufferView.byteOffset + vec4BufferView.byteLength) + ",\n"); + Indent(); jsonWriter.Write("\"type\": \"arraybuffer\"\n"); + + IndentOut(); + Indent(); jsonWriter.Write("}\n"); + + IndentOut(); + Indent(); jsonWriter.Write("}"); + } - } - CommaNL(); - - Indent(); jsonWriter.Write("\"scene\": \"defaultScene\",\n"); - Indent(); jsonWriter.Write("\"scenes\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"defaultScene\": {\n"); - IndentIn(); - CommaNL(); - Indent(); jsonWriter.Write("\"nodes\": [\n"); - IndentIn(); - foreach (KeyValuePair n in nodes) { - CommaNL(); - Indent(); jsonWriter.Write("\"" + n.Value.name + "\""); - } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); jsonWriter.Write("],\n"); - - Indent(); jsonWriter.Write("\"extras\": {\n"); - IndentIn(); - foreach (KeyValuePair e in extras) { - CommaNL(); - Indent(); jsonWriter.Write("\"" + e.Key + "\": \"" + e.Value + "\""); - } - jsonWriter.WriteLine(); - IndentOut(); - Indent(); jsonWriter.Write("}\n"); // end extras - IndentOut(); - Indent(); jsonWriter.Write("}\n"); // end defaultScene - IndentOut(); - Indent(); jsonWriter.Write("}"); // end scenes - - List extUsed = new List(); - if (binary) { - extUsed.Add("KHR_binary_glTF"); - } - var rtc = RTCCenter != null && RTCCenter.Length == 3; - if (rtc) { - extUsed.Add("CESIUM_RTC"); - } + if (cameras != null && cameras.Count > 0) + { + CommaNL(); + Indent(); jsonWriter.Write("\"cameras\": {\n"); + IndentIn(); + foreach (GlTF_Camera c in cameras) + { + CommaNL(); + c.Write(); + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); jsonWriter.Write("}"); + } - if (extUsed.Count > 0) { - CommaNL(); - Indent(); jsonWriter.Write("\"extensionsUsed\": [\n"); - IndentIn(); + if (accessors != null && accessors.Count > 0) + { + CommaNL(); + Indent(); jsonWriter.Write("\"accessors\": {\n"); + IndentIn(); + foreach (GlTF_Accessor a in accessors) + { + CommaNL(); + a.Write(); + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); jsonWriter.Write("}"); + } - for (var i = 0; i < extUsed.Count; ++i) { - CommaNL(); - Indent(); jsonWriter.Write("\"" + extUsed[i] + "\""); - } + if (bufferViews != null && bufferViews.Count > 0) + { + CommaNL(); + Indent(); jsonWriter.Write("\"bufferViews\": {\n"); + IndentIn(); + foreach (GlTF_BufferView bv in bufferViews) + { + if (bv.byteLength > 0) + { + CommaNL(); + bv.Write(); + } + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); jsonWriter.Write("}"); + } - jsonWriter.Write("\n"); - IndentOut(); - Indent(); jsonWriter.Write("]"); + if (meshes != null && meshes.Count > 0) + { + CommaNL(); + Indent(); + jsonWriter.Write("\"meshes\": {\n"); + IndentIn(); + foreach (GlTF_Mesh m in meshes) + { + CommaNL(); + m.Write(); + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); + jsonWriter.Write("}"); + } - } + if (shaders != null && shaders.Count > 0) + { + CommaNL(); + Indent(); + jsonWriter.Write("\"shaders\": {\n"); + IndentIn(); + foreach (var s in shaders) + { + CommaNL(); + s.Write(); + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); + jsonWriter.Write("}"); + } - if (rtc) { - CommaNL(); - Indent(); jsonWriter.Write("\"extensions\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"CESIUM_RTC\": {\n"); - IndentIn(); - Indent(); jsonWriter.Write("\"center\": [\n"); - IndentIn(); - for (var i = 0; i < 3; ++i) { + if (programs != null && programs.Count > 0) + { + CommaNL(); + Indent(); + jsonWriter.Write("\"programs\": {\n"); + IndentIn(); + foreach (var p in programs) + { + CommaNL(); + p.Write(); + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); + jsonWriter.Write("}"); + } + + if (techniques != null && techniques.Count > 0) + { + CommaNL(); + Indent(); + jsonWriter.Write("\"techniques\": {\n"); + IndentIn(); + foreach (KeyValuePair k in techniques) + { + CommaNL(); + k.Value.Write(); + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); + jsonWriter.Write("}"); + } + + if (samplers.Count > 0) + { + CommaNL(); + Indent(); jsonWriter.Write("\"samplers\": {\n"); + IndentIn(); + foreach (KeyValuePair s in samplers) + { + CommaNL(); + s.Value.Write(); + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); jsonWriter.Write("}"); + } + + if (textures.Count > 0) + { + CommaNL(); + Indent(); jsonWriter.Write("\"textures\": {\n"); + IndentIn(); + foreach (KeyValuePair t in textures) + { + CommaNL(); + t.Value.Write(); + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); jsonWriter.Write("}"); + } + + if (images.Count > 0) + { + CommaNL(); + Indent(); jsonWriter.Write("\"images\": {\n"); + IndentIn(); + foreach (var i in images) + { + CommaNL(); + i.Write(); + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); jsonWriter.Write("}"); + } + + if (materials.Count > 0) + { + CommaNL(); + Indent(); jsonWriter.Write("\"materials\": {\n"); + IndentIn(); + foreach (KeyValuePair m in materials) + { + CommaNL(); + m.Value.Write(); + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); jsonWriter.Write("}"); + } + + if (nodes != null && nodes.Count > 0) + { + CommaNL(); + Indent(); jsonWriter.Write("\"nodes\": {\n"); + IndentIn(); + foreach (KeyValuePair n in nodes) + { + CommaNL(); + n.Value.Write(); + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); jsonWriter.Write("}"); + + } CommaNL(); - Indent(); jsonWriter.Write(RTCCenter[i]); - } - jsonWriter.Write("\n"); - IndentOut(); - Indent(); jsonWriter.Write("]\n"); - IndentOut(); - Indent(); jsonWriter.Write("}\n"); - IndentOut(); - Indent(); jsonWriter.Write("}"); - } - jsonWriter.Write("\n"); - IndentOut(); - Indent(); jsonWriter.Write("}"); - - jsonWriter.Flush(); - - uint contentLength = 0; - if (binary) { - long curLen = fs.Position; - var rem = curLen % 4; - if (rem != 0) { - // add padding if not aligned to 4 bytes - var next = (curLen / 4 + 1) * 4; - rem = next - curLen; - for (int i = 0; i < rem; ++i) { - jsonWriter.Write(" "); + Indent(); jsonWriter.Write("\"scene\": \"defaultScene\",\n"); + Indent(); jsonWriter.Write("\"scenes\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"defaultScene\": {\n"); + IndentIn(); + CommaNL(); + Indent(); jsonWriter.Write("\"nodes\": [\n"); + IndentIn(); + foreach (KeyValuePair n in nodes) + { + CommaNL(); + Indent(); jsonWriter.Write("\"" + n.Value.name + "\""); + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); jsonWriter.Write("],\n"); + + Indent(); jsonWriter.Write("\"extras\": {\n"); + IndentIn(); + foreach (KeyValuePair e in extras) + { + CommaNL(); + Indent(); jsonWriter.Write("\"" + e.Key + "\": \"" + e.Value + "\""); + } + jsonWriter.WriteLine(); + IndentOut(); + Indent(); jsonWriter.Write("}\n"); // end extras + IndentOut(); + Indent(); jsonWriter.Write("}\n"); // end defaultScene + IndentOut(); + Indent(); jsonWriter.Write("}"); // end scenes + + List extUsed = new List(); + if (binary) + { + extUsed.Add("KHR_binary_glTF"); + } + var rtc = RTCCenter != null && RTCCenter.Length == 3; + if (rtc) + { + extUsed.Add("CESIUM_RTC"); } - } - jsonWriter.Flush(); - // current pos - header size - int offset = 20 + (b3dm ? B3DM_HEADER_SIZE : 0); - contentLength = (uint) (fs.Position - offset); - } + if (extUsed.Count > 0) + { + CommaNL(); + Indent(); jsonWriter.Write("\"extensionsUsed\": [\n"); + IndentIn(); + for (var i = 0; i < extUsed.Count; ++i) + { + CommaNL(); + Indent(); jsonWriter.Write("\"" + extUsed[i] + "\""); + } - ushortBufferView.memoryStream.WriteTo(binFile); - floatBufferView.memoryStream.WriteTo(binFile); - vec2BufferView.memoryStream.WriteTo(binFile); - vec3BufferView.memoryStream.WriteTo(binFile); - vec4BufferView.memoryStream.WriteTo(binFile); + jsonWriter.Write("\n"); + IndentOut(); + Indent(); jsonWriter.Write("]"); - binFile.Flush(); - if (binary) { - uint fileLength = (uint) fs.Length; + } + + if (rtc) + { + CommaNL(); + Indent(); jsonWriter.Write("\"extensions\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"CESIUM_RTC\": {\n"); + IndentIn(); + Indent(); jsonWriter.Write("\"center\": [\n"); + IndentIn(); + for (var i = 0; i < 3; ++i) + { + CommaNL(); + Indent(); jsonWriter.Write(RTCCenter[i]); + } + jsonWriter.Write("\n"); + IndentOut(); + Indent(); jsonWriter.Write("]\n"); + IndentOut(); + Indent(); jsonWriter.Write("}\n"); + IndentOut(); + Indent(); jsonWriter.Write("}"); + } - // write header - fs.Seek(0, SeekOrigin.Begin); + jsonWriter.Write("\n"); + IndentOut(); + Indent(); jsonWriter.Write("}"); - if (b3dm) { - jsonWriter.Write("b3dm"); // magic jsonWriter.Flush(); - binWriter.Write(1); // version - binWriter.Write(fileLength); - binWriter.Write(0); // batchTableJSONByteLength - binWriter.Write(0); // batchTableBinaryByteLength - binWriter.Write(0); // batchLength - binWriter.Flush(); - } - - jsonWriter.Write("glTF"); // magic - jsonWriter.Flush(); - binWriter.Write(1); // version - uint l = (uint) (fileLength - (b3dm ? B3DM_HEADER_SIZE : 0)); // min b3dm header - binWriter.Write(l); - binWriter.Write(contentLength); - binWriter.Write(0); // format - binWriter.Flush(); + + uint contentLength = 0; + if (binary) + { + long curLen = fs.Position; + var rem = curLen % 4; + if (rem != 0) + { + // add padding if not aligned to 4 bytes + var next = (curLen / 4 + 1) * 4; + rem = next - curLen; + for (int i = 0; i < rem; ++i) + { + jsonWriter.Write(" "); + } + } + jsonWriter.Flush(); + + // current pos - header size + int offset = 20 + (b3dm ? B3DM_HEADER_SIZE : 0); + contentLength = (uint)(fs.Position - offset); + } + + + ushortBufferView.memoryStream.WriteTo(binFile); + floatBufferView.memoryStream.WriteTo(binFile); + vec2BufferView.memoryStream.WriteTo(binFile); + vec3BufferView.memoryStream.WriteTo(binFile); + vec4BufferView.memoryStream.WriteTo(binFile); + + binFile.Flush(); + if (binary) + { + uint fileLength = (uint)fs.Length; + + // write header + fs.Seek(0, SeekOrigin.Begin); + + if (b3dm) + { + jsonWriter.Write("b3dm"); // magic + jsonWriter.Flush(); + binWriter.Write(1); // version + binWriter.Write(fileLength); + binWriter.Write(0); // batchTableJSONByteLength + binWriter.Write(0); // batchTableBinaryByteLength + binWriter.Write(0); // batchLength + binWriter.Flush(); + } + + jsonWriter.Write("glTF"); // magic + jsonWriter.Flush(); + binWriter.Write(1); // version + uint l = (uint)(fileLength - (b3dm ? B3DM_HEADER_SIZE : 0)); // min b3dm header + binWriter.Write(l); + binWriter.Write(contentLength); + binWriter.Write(0); // format + binWriter.Flush(); + } + } + + // Returns existing technique based on name of object. + public static GlTF_Technique GetTechnique(GameObject namedObject) + { + var name = GlTF_Technique.GetNameFromObject(namedObject); + Debug.Assert(techniques.ContainsKey(name)); + return techniques[name]; + } + + // Creates new technique based on name of object. + public static GlTF_Technique CreateTechnique(GameObject namedObject) + { + var name = GlTF_Technique.GetNameFromObject(namedObject); + Debug.Assert(!techniques.ContainsKey(name)); + var ret = new GlTF_Technique(); + ret.name = name; + techniques.Add(name, ret); + return ret; } - } - - // Returns existing technique based on name of object. - public static GlTF_Technique GetTechnique(GameObject namedObject) { - var name = GlTF_Technique.GetNameFromObject(namedObject); - Debug.Assert(techniques.ContainsKey(name)); - return techniques[name]; - } - - // Creates new technique based on name of object. - public static GlTF_Technique CreateTechnique(GameObject namedObject) { - var name = GlTF_Technique.GetNameFromObject(namedObject); - Debug.Assert(!techniques.ContainsKey(name)); - var ret = new GlTF_Technique(); - ret.name = name; - techniques.Add(name, ret); - return ret; - } - - - private static string OrUnknown(string str) { return str != null ? str : "Unknown."; } - - private const int B3DM_HEADER_SIZE = 24; - private static bool[] firsts = new bool[100]; - - private string copyright; - private string generator; - private string version; + + + private static string OrUnknown(string str) { return str != null ? str : "Unknown."; } + + private const int B3DM_HEADER_SIZE = 24; + private static bool[] firsts = new bool[100]; + + private string copyright; + private string generator; + private string version; } diff --git a/Assets/Scripts/ThirdParty/GlTF/Preset.cs b/Assets/Scripts/ThirdParty/GlTF/Preset.cs index 78078a6d..78fa6254 100644 --- a/Assets/Scripts/ThirdParty/GlTF/Preset.cs +++ b/Assets/Scripts/ThirdParty/GlTF/Preset.cs @@ -5,213 +5,258 @@ using System.Linq; using SimpleJSON; -public class Preset { - public class Shader { - public string vertexShader; - public string fragmentShader; - } - - public class UnpackUV { - public int index; // uv index - public List textureIndex = new List(); - } - - public Dictionary shaderMap = new Dictionary(); - public Dictionary techniqueStates = new Dictionary(); - public Dictionary techniqueExtras = new Dictionary(); - public Dictionary> unpackUV = new Dictionary>(); - - private const string DEFAULT_VERTEX_SHADER = "DefaultVS.glsl"; - private const string DEFAULT_FRAGMENT_SHADER = "DefaultFS.glsl"; - - // Sets fragment and vertex shader filenames for the current brushName. If a passed-in shader name - // is null, uses the default name for that shader type. - public void SetShaders(string brushName, string vertShader, string fragShader) { - var shaders = new Shader{ - vertexShader = vertShader != null ? vertShader : DEFAULT_VERTEX_SHADER, - fragmentShader = fragShader != null ? fragShader : DEFAULT_FRAGMENT_SHADER}; - shaderMap[brushName] = shaders; - } - - public string GetVertexShader(string shaderName) { - if (shaderMap.ContainsKey(shaderName)) { - var s = shaderMap[shaderName]; - return s.vertexShader; +public class Preset +{ + public class Shader + { + public string vertexShader; + public string fragmentShader; } - return DEFAULT_VERTEX_SHADER; - } - public string GetFragmentShader(string shaderName) { - if (shaderMap.ContainsKey(shaderName)) { - var s = shaderMap[shaderName]; - return s.fragmentShader; + public class UnpackUV + { + public int index; // uv index + public List textureIndex = new List(); } - return DEFAULT_FRAGMENT_SHADER; - } - - public List GetDefaultUnpackUVList() { - // default is uv0 to tex0 - var ret = new List(); - var uv = new UnpackUV(); - uv.index = 0; - uv.textureIndex.Add(0); - ret.Add(uv); - return ret; - } - - public void Load(string path) { - var text = File.ReadAllText(path); - var obj = JSON.Parse(text); - - var sm = obj["ShaderMap"]; - - shaderMap.Clear(); - foreach (var smc in sm.AsObject.Dict) { - Shader shader = new Shader(); - shader.vertexShader = smc.Value["shaders"]["vertexShader"]; - shader.fragmentShader = smc.Value["shaders"]["fragmentShader"]; - - if (shader.vertexShader == null) { - shader.vertexShader = DEFAULT_VERTEX_SHADER; - } - if (shader.fragmentShader == null) { - shader.fragmentShader = DEFAULT_FRAGMENT_SHADER; - } - - shaderMap[smc.Key] = shader; + + public Dictionary shaderMap = new Dictionary(); + public Dictionary techniqueStates = new Dictionary(); + public Dictionary techniqueExtras = new Dictionary(); + public Dictionary> unpackUV = new Dictionary>(); + + private const string DEFAULT_VERTEX_SHADER = "DefaultVS.glsl"; + private const string DEFAULT_FRAGMENT_SHADER = "DefaultFS.glsl"; + + // Sets fragment and vertex shader filenames for the current brushName. If a passed-in shader name + // is null, uses the default name for that shader type. + public void SetShaders(string brushName, string vertShader, string fragShader) + { + var shaders = new Shader + { + vertexShader = vertShader != null ? vertShader : DEFAULT_VERTEX_SHADER, + fragmentShader = fragShader != null ? fragShader : DEFAULT_FRAGMENT_SHADER + }; + shaderMap[brushName] = shaders; } - var ts = obj["TechniqueStates"]; - techniqueStates.Clear(); - foreach (var t in ts.AsObject.Dict) { - GlTF_Technique.States state = new GlTF_Technique.States(); - techniqueStates[t.Key] = state; - - var s = t.Value["states"]; - var enArr = s["enable"].AsArray; - if (enArr.Count > 0) { - state.enable = (from c in enArr.Children select (GlTF_Technique.Enable)c.AsInt).ToList(); - } - - var f = t.Value["functions"]; - var node = f["blendColor"]; - if (node != null) { - var nArr = node.AsArray; - if (nArr != null && nArr.Count == 4) { - state.functions["blendColor"] = new GlTF_Technique.Value(new Color(nArr[0].AsFloat, nArr[1].AsFloat, nArr[2].AsFloat, nArr[3].AsFloat)); + public string GetVertexShader(string shaderName) + { + if (shaderMap.ContainsKey(shaderName)) + { + var s = shaderMap[shaderName]; + return s.vertexShader; } - } + return DEFAULT_VERTEX_SHADER; + } - node = f["blendEquationSeparate"]; - if (node != null) { - var nArr = node.AsArray; - if (nArr != null && nArr.Count == 2) { - state.functions["blendEquationSeparate"] = new GlTF_Technique.Value(new int[2] { nArr[0].AsInt, nArr[1].AsInt }); + public string GetFragmentShader(string shaderName) + { + if (shaderMap.ContainsKey(shaderName)) + { + var s = shaderMap[shaderName]; + return s.fragmentShader; } - } + return DEFAULT_FRAGMENT_SHADER; + } - node = f["blendFuncSeparate"]; - if (node != null) { - var nArr = node.AsArray; - if (nArr != null && nArr.Count == 4) { - state.functions["blendFuncSeparate"] = new GlTF_Technique.Value(new int[4] { nArr[0].AsInt, nArr[1].AsInt, nArr[2].AsInt, nArr[3].AsInt }); - } - } + public List GetDefaultUnpackUVList() + { + // default is uv0 to tex0 + var ret = new List(); + var uv = new UnpackUV(); + uv.index = 0; + uv.textureIndex.Add(0); + ret.Add(uv); + return ret; + } - node = f["colorMask"]; - if (node != null) { - var nArr = node.AsArray; - if (nArr != null && nArr.Count == 4) { - state.functions["colorMask"] = new GlTF_Technique.Value(new bool[4] { nArr[0].AsBool, nArr[1].AsBool, nArr[2].AsBool, nArr[3].AsBool }); - } - } + public void Load(string path) + { + var text = File.ReadAllText(path); + var obj = JSON.Parse(text); - node = f["cullFace"]; - if (node != null) { - var nArr = node.AsArray; - if (nArr != null && nArr.Count == 1) { - state.functions["cullFace"] = new GlTF_Technique.Value(nArr[0].AsInt); - } - } + var sm = obj["ShaderMap"]; - node = f["depthFunc"]; - if (node != null) { - var nArr = node.AsArray; - if (nArr != null && nArr.Count == 1) { - state.functions["depthFunc"] = new GlTF_Technique.Value(nArr[0].AsInt); - } - } + shaderMap.Clear(); + foreach (var smc in sm.AsObject.Dict) + { + Shader shader = new Shader(); + shader.vertexShader = smc.Value["shaders"]["vertexShader"]; + shader.fragmentShader = smc.Value["shaders"]["fragmentShader"]; - node = f["depthMask"]; - if (node != null) { - var nArr = node.AsArray; - if (nArr != null && nArr.Count == 1) { - state.functions["depthMask"] = new GlTF_Technique.Value(nArr[0].AsBool); - } - } + if (shader.vertexShader == null) + { + shader.vertexShader = DEFAULT_VERTEX_SHADER; + } + if (shader.fragmentShader == null) + { + shader.fragmentShader = DEFAULT_FRAGMENT_SHADER; + } - node = f["depthRange"]; - if (node != null) { - var nArr = node.AsArray; - if (nArr != null && nArr.Count == 2) { - state.functions["depthRange"] = new GlTF_Technique.Value(new Vector2(nArr[0].AsFloat, nArr[1].AsFloat)); + shaderMap[smc.Key] = shader; } - } - node = f["frontFace"]; - if (node != null) { - var nArr = node.AsArray; - if (nArr != null && nArr.Count == 1) { - state.functions["frontFace"] = new GlTF_Technique.Value(nArr[0].AsInt); - } - } + var ts = obj["TechniqueStates"]; + techniqueStates.Clear(); + foreach (var t in ts.AsObject.Dict) + { + GlTF_Technique.States state = new GlTF_Technique.States(); + techniqueStates[t.Key] = state; - node = f["lineWidth"]; - if (node != null) { - var nArr = node.AsArray; - if (nArr != null && nArr.Count == 1) { - state.functions["lineWidth"] = new GlTF_Technique.Value(nArr[0].AsFloat); - } - } + var s = t.Value["states"]; + var enArr = s["enable"].AsArray; + if (enArr.Count > 0) + { + state.enable = (from c in enArr.Children select (GlTF_Technique.Enable)c.AsInt).ToList(); + } - node = f["polygonOffset"]; - if (node != null) { - var nArr = node.AsArray; - if (nArr != null && nArr.Count == 2) { - state.functions["polygonOffset"] = new GlTF_Technique.Value(new Vector2(nArr[0].AsFloat, nArr[1].AsFloat)); - } - } + var f = t.Value["functions"]; + var node = f["blendColor"]; + if (node != null) + { + var nArr = node.AsArray; + if (nArr != null && nArr.Count == 4) + { + state.functions["blendColor"] = new GlTF_Technique.Value(new Color(nArr[0].AsFloat, nArr[1].AsFloat, nArr[2].AsFloat, nArr[3].AsFloat)); + } + } + + node = f["blendEquationSeparate"]; + if (node != null) + { + var nArr = node.AsArray; + if (nArr != null && nArr.Count == 2) + { + state.functions["blendEquationSeparate"] = new GlTF_Technique.Value(new int[2] { nArr[0].AsInt, nArr[1].AsInt }); + } + } + + node = f["blendFuncSeparate"]; + if (node != null) + { + var nArr = node.AsArray; + if (nArr != null && nArr.Count == 4) + { + state.functions["blendFuncSeparate"] = new GlTF_Technique.Value(new int[4] { nArr[0].AsInt, nArr[1].AsInt, nArr[2].AsInt, nArr[3].AsInt }); + } + } + + node = f["colorMask"]; + if (node != null) + { + var nArr = node.AsArray; + if (nArr != null && nArr.Count == 4) + { + state.functions["colorMask"] = new GlTF_Technique.Value(new bool[4] { nArr[0].AsBool, nArr[1].AsBool, nArr[2].AsBool, nArr[3].AsBool }); + } + } + + node = f["cullFace"]; + if (node != null) + { + var nArr = node.AsArray; + if (nArr != null && nArr.Count == 1) + { + state.functions["cullFace"] = new GlTF_Technique.Value(nArr[0].AsInt); + } + } + + node = f["depthFunc"]; + if (node != null) + { + var nArr = node.AsArray; + if (nArr != null && nArr.Count == 1) + { + state.functions["depthFunc"] = new GlTF_Technique.Value(nArr[0].AsInt); + } + } + + node = f["depthMask"]; + if (node != null) + { + var nArr = node.AsArray; + if (nArr != null && nArr.Count == 1) + { + state.functions["depthMask"] = new GlTF_Technique.Value(nArr[0].AsBool); + } + } + + node = f["depthRange"]; + if (node != null) + { + var nArr = node.AsArray; + if (nArr != null && nArr.Count == 2) + { + state.functions["depthRange"] = new GlTF_Technique.Value(new Vector2(nArr[0].AsFloat, nArr[1].AsFloat)); + } + } + + node = f["frontFace"]; + if (node != null) + { + var nArr = node.AsArray; + if (nArr != null && nArr.Count == 1) + { + state.functions["frontFace"] = new GlTF_Technique.Value(nArr[0].AsInt); + } + } + + node = f["lineWidth"]; + if (node != null) + { + var nArr = node.AsArray; + if (nArr != null && nArr.Count == 1) + { + state.functions["lineWidth"] = new GlTF_Technique.Value(nArr[0].AsFloat); + } + } + + node = f["polygonOffset"]; + if (node != null) + { + var nArr = node.AsArray; + if (nArr != null && nArr.Count == 2) + { + state.functions["polygonOffset"] = new GlTF_Technique.Value(new Vector2(nArr[0].AsFloat, nArr[1].AsFloat)); + } + } + + node = f["scissor"]; + if (node != null) + { + var nArr = node.AsArray; + if (nArr != null && nArr.Count == 4) + { + state.functions["scissor"] = new GlTF_Technique.Value(new Vector4(nArr[0].AsFloat, nArr[1].AsFloat, nArr[2].AsFloat, nArr[3].AsFloat)); + } + } - node = f["scissor"]; - if (node != null) { - var nArr = node.AsArray; - if (nArr != null && nArr.Count == 4) { - state.functions["scissor"] = new GlTF_Technique.Value(new Vector4(nArr[0].AsFloat, nArr[1].AsFloat, nArr[2].AsFloat, nArr[3].AsFloat)); } - } + LoadTextureUnpacker(obj); } - LoadTextureUnpacker(obj); - } - - private void LoadTextureUnpacker(JSONNode obj) { - unpackUV.Clear(); - var tus = obj["TextureUnpacker"]; - foreach (var tu in tus.AsObject.Dict) { - var uvList = tu.Value.AsArray; - var l = new List(); - for (var i = 0; i < uvList.Count; ++i) { - UnpackUV unpack = new UnpackUV(); - var uv = uvList[i]; - unpack.index = uv["uvIndex"].AsInt; - var arr = uv["textureIndex"].AsArray; - for (var j = 0; j < arr.Count; ++j) { - unpack.textureIndex.Add(arr[j].AsInt); + private void LoadTextureUnpacker(JSONNode obj) + { + unpackUV.Clear(); + var tus = obj["TextureUnpacker"]; + foreach (var tu in tus.AsObject.Dict) + { + var uvList = tu.Value.AsArray; + var l = new List(); + for (var i = 0; i < uvList.Count; ++i) + { + UnpackUV unpack = new UnpackUV(); + var uv = uvList[i]; + unpack.index = uv["uvIndex"].AsInt; + var arr = uv["textureIndex"].AsArray; + for (var j = 0; j < arr.Count; ++j) + { + unpack.textureIndex.Add(arr[j].AsInt); + } + l.Add(unpack); + } + unpackUV[tu.Key] = l; } - l.Add(unpack); - } - unpackUV[tu.Key] = l; } - } } diff --git a/Assets/Scripts/ThirdParty/GlTF/SimpleJSON.cs b/Assets/Scripts/ThirdParty/GlTF/SimpleJSON.cs index cfa30597..23a56dde 100644 --- a/Assets/Scripts/ThirdParty/GlTF/SimpleJSON.cs +++ b/Assets/Scripts/ThirdParty/GlTF/SimpleJSON.cs @@ -49,422 +49,487 @@ using System.Linq; -namespace SimpleJSON { - public enum JSONBinaryTag { - Array = 1, - Class = 2, - Value = 3, - IntValue = 4, - DoubleValue = 5, - BoolValue = 6, - FloatValue = 7, - } - - public abstract class JSONNode { - #region common interface - - public virtual void Add(string aKey, JSONNode aItem) { - } +namespace SimpleJSON +{ + public enum JSONBinaryTag + { + Array = 1, + Class = 2, + Value = 3, + IntValue = 4, + DoubleValue = 5, + BoolValue = 6, + FloatValue = 7, + } + + public abstract class JSONNode + { + #region common interface + + public virtual void Add(string aKey, JSONNode aItem) + { + } - public virtual JSONNode this[int aIndex] { get { return null; } set { } } + public virtual JSONNode this[int aIndex] { get { return null; } set { } } - public virtual JSONNode this[string aKey] { get { return null; } set { } } + public virtual JSONNode this[string aKey] { get { return null; } set { } } - public virtual string Value { get { return ""; } set { } } + public virtual string Value { get { return ""; } set { } } - public virtual int Count { get { return 0; } } + public virtual int Count { get { return 0; } } - public virtual void Add(JSONNode aItem) { - Add("", aItem); - } + public virtual void Add(JSONNode aItem) + { + Add("", aItem); + } - public virtual JSONNode Remove(string aKey) { - return null; - } + public virtual JSONNode Remove(string aKey) + { + return null; + } - public virtual JSONNode Remove(int aIndex) { - return null; - } + public virtual JSONNode Remove(int aIndex) + { + return null; + } - public virtual JSONNode Remove(JSONNode aNode) { - return aNode; - } + public virtual JSONNode Remove(JSONNode aNode) + { + return aNode; + } - public virtual IEnumerable Children { - get { - yield break; - } - } + public virtual IEnumerable Children + { + get + { + yield break; + } + } - public IEnumerable DeepChildren { - get { - foreach (var C in Children) - foreach (var D in C.DeepChildren) - yield return D; - } - } + public IEnumerable DeepChildren + { + get + { + foreach (var C in Children) + foreach (var D in C.DeepChildren) + yield return D; + } + } - public override string ToString() { - return "JSONNode"; - } + public override string ToString() + { + return "JSONNode"; + } - public virtual string ToString(string aPrefix) { - return "JSONNode"; - } + public virtual string ToString(string aPrefix) + { + return "JSONNode"; + } - public abstract string ToJSON(int prefix); + public abstract string ToJSON(int prefix); - #endregion common interface + #endregion common interface - #region typecasting properties + #region typecasting properties - public virtual JSONBinaryTag Tag { get; set; } + public virtual JSONBinaryTag Tag { get; set; } - public virtual int AsInt { - get { - int v = 0; - if (int.TryParse(Value, out v)) - return v; - return 0; - } - set { - Value = value.ToString(); - Tag = JSONBinaryTag.IntValue; - } - } + public virtual int AsInt + { + get + { + int v = 0; + if (int.TryParse(Value, out v)) + return v; + return 0; + } + set + { + Value = value.ToString(); + Tag = JSONBinaryTag.IntValue; + } + } - public virtual float AsFloat { - get { - float v = 0.0f; - if (float.TryParse(Value, out v)) - return v; - return 0.0f; - } - set { - Value = value.ToString(); - Tag = JSONBinaryTag.FloatValue; - } - } + public virtual float AsFloat + { + get + { + float v = 0.0f; + if (float.TryParse(Value, out v)) + return v; + return 0.0f; + } + set + { + Value = value.ToString(); + Tag = JSONBinaryTag.FloatValue; + } + } - public virtual double AsDouble { - get { - double v = 0.0; - if (double.TryParse(Value, out v)) - return v; - return 0.0; - } - set { - Value = value.ToString(); - Tag = JSONBinaryTag.DoubleValue; - - } - } + public virtual double AsDouble + { + get + { + double v = 0.0; + if (double.TryParse(Value, out v)) + return v; + return 0.0; + } + set + { + Value = value.ToString(); + Tag = JSONBinaryTag.DoubleValue; - public virtual bool AsBool { - get { - bool v = false; - if (bool.TryParse(Value, out v)) - return v; - return !string.IsNullOrEmpty(Value); - } - set { - Value = (value) ? "true" : "false"; - Tag = JSONBinaryTag.BoolValue; - - } - } + } + } - public virtual JSONArray AsArray { - get { - return this as JSONArray; - } - } + public virtual bool AsBool + { + get + { + bool v = false; + if (bool.TryParse(Value, out v)) + return v; + return !string.IsNullOrEmpty(Value); + } + set + { + Value = (value) ? "true" : "false"; + Tag = JSONBinaryTag.BoolValue; - public virtual JSONClass AsObject { - get { - return this as JSONClass; - } - } + } + } + public virtual JSONArray AsArray + { + get + { + return this as JSONArray; + } + } - #endregion typecasting properties + public virtual JSONClass AsObject + { + get + { + return this as JSONClass; + } + } - #region operators - public static implicit operator JSONNode(string s) { - return new JSONData(s); - } + #endregion typecasting properties - public static implicit operator string(JSONNode d) { - return (d == null) ? null : d.Value; - } + #region operators - public static bool operator ==(JSONNode a, object b) { - if (b == null && a is JSONLazyCreator) - return true; - return System.Object.ReferenceEquals(a, b); - } + public static implicit operator JSONNode(string s) + { + return new JSONData(s); + } - public static bool operator !=(JSONNode a, object b) { - return !(a == b); - } + public static implicit operator string(JSONNode d) + { + return (d == null) ? null : d.Value; + } - public override bool Equals(object obj) { - return System.Object.ReferenceEquals(this, obj); - } + public static bool operator ==(JSONNode a, object b) + { + if (b == null && a is JSONLazyCreator) + return true; + return System.Object.ReferenceEquals(a, b); + } - public override int GetHashCode() { - return base.GetHashCode(); - } + public static bool operator !=(JSONNode a, object b) + { + return !(a == b); + } + public override bool Equals(object obj) + { + return System.Object.ReferenceEquals(this, obj); + } - #endregion operators - - internal static string Escape(string aText) { - string result = ""; - foreach (char c in aText) { - switch (c) { - case '\\': - result += "\\\\"; - break; - case '\"': - result += "\\\""; - break; - case '\n': - result += "\\n"; - break; - case '\r': - result += "\\r"; - break; - case '\t': - result += "\\t"; - break; - case '\b': - result += "\\b"; - break; - case '\f': - result += "\\f"; - break; - default: - result += c; - break; - } - } - return result; - } + public override int GetHashCode() + { + return base.GetHashCode(); + } - private static JSONData Numberize(string token) { - bool flag = false; - int integer = 0; - double real = 0; - if (int.TryParse(token, out integer)) { - return new JSONData(integer); - } + #endregion operators + + internal static string Escape(string aText) + { + string result = ""; + foreach (char c in aText) + { + switch (c) + { + case '\\': + result += "\\\\"; + break; + case '\"': + result += "\\\""; + break; + case '\n': + result += "\\n"; + break; + case '\r': + result += "\\r"; + break; + case '\t': + result += "\\t"; + break; + case '\b': + result += "\\b"; + break; + case '\f': + result += "\\f"; + break; + default: + result += c; + break; + } + } + return result; + } - if (double.TryParse(token, out real)) { - return new JSONData(real); - } + private static JSONData Numberize(string token) + { + bool flag = false; + int integer = 0; + double real = 0; - if (bool.TryParse(token, out flag)) { - return new JSONData(flag); - } + if (int.TryParse(token, out integer)) + { + return new JSONData(integer); + } - throw new NotImplementedException(token); - } + if (double.TryParse(token, out real)) + { + return new JSONData(real); + } - private static void AddElement(JSONNode ctx, string token, string tokenName, bool tokenIsString) { - if (tokenIsString) { - if (ctx is JSONArray) - ctx.Add(token); - else - ctx.Add(tokenName, token); // assume dictionary/object - } else { - JSONData number = Numberize(token); - if (ctx is JSONArray) - ctx.Add(number); - else - ctx.Add(tokenName, number); - - } - } + if (bool.TryParse(token, out flag)) + { + return new JSONData(flag); + } - public static JSONNode Parse(string aJSON) { - Stack stack = new Stack(); - JSONNode ctx = null; - int i = 0; - string Token = ""; - string TokenName = ""; - bool QuoteMode = false; - bool TokenIsString = false; - while (i < aJSON.Length) { - switch (aJSON[i]) { - case '{': - if (QuoteMode) { - Token += aJSON[i]; - break; - } - stack.Push(new JSONClass()); - if (ctx != null) { - TokenName = TokenName.Trim(); - if (ctx is JSONArray) - ctx.Add(stack.Peek()); - else if (TokenName != "") - ctx.Add(TokenName, stack.Peek()); - } - TokenName = ""; - Token = ""; - ctx = stack.Peek(); - break; - - case '[': - if (QuoteMode) { - Token += aJSON[i]; - break; - } - - stack.Push(new JSONArray()); - if (ctx != null) { - TokenName = TokenName.Trim(); - - if (ctx is JSONArray) - ctx.Add(stack.Peek()); - else if (TokenName != "") - ctx.Add(TokenName, stack.Peek()); - } - TokenName = ""; - Token = ""; - ctx = stack.Peek(); - break; - - case '}': - case ']': - if (QuoteMode) { - Token += aJSON[i]; - break; - } - if (stack.Count == 0) - throw new Exception("JSON Parse: Too many closing brackets"); - - stack.Pop(); - if (Token != "") { - TokenName = TokenName.Trim(); - /* - if (ctx is JSONArray) - ctx.Add (Token); - else if (TokenName != "") - ctx.Add (TokenName, Token); - */ - AddElement(ctx, Token, TokenName, TokenIsString); - TokenIsString = false; - } - TokenName = ""; - Token = ""; - if (stack.Count > 0) - ctx = stack.Peek(); - break; - - case ':': - if (QuoteMode) { - Token += aJSON[i]; - break; - } - TokenName = Token; - Token = ""; - TokenIsString = false; - break; - - case '"': - QuoteMode ^= true; - TokenIsString = QuoteMode == true ? true : TokenIsString; - break; - - case ',': - if (QuoteMode) { - Token += aJSON[i]; - break; - } - if (Token != "") { - /* - if (ctx is JSONArray) { - ctx.Add (Token); - } else if (TokenName != "") { - ctx.Add (TokenName, Token); - } - */ - AddElement(ctx, Token, TokenName, TokenIsString); - TokenIsString = false; + throw new NotImplementedException(token); + } + private static void AddElement(JSONNode ctx, string token, string tokenName, bool tokenIsString) + { + if (tokenIsString) + { + if (ctx is JSONArray) + ctx.Add(token); + else + ctx.Add(tokenName, token); // assume dictionary/object } - TokenName = ""; - Token = ""; - TokenIsString = false; - break; + else + { + JSONData number = Numberize(token); + if (ctx is JSONArray) + ctx.Add(number); + else + ctx.Add(tokenName, number); - case '\r': - case '\n': - break; + } + } - case ' ': - case '\t': + public static JSONNode Parse(string aJSON) + { + Stack stack = new Stack(); + JSONNode ctx = null; + int i = 0; + string Token = ""; + string TokenName = ""; + bool QuoteMode = false; + bool TokenIsString = false; + while (i < aJSON.Length) + { + switch (aJSON[i]) + { + case '{': + if (QuoteMode) + { + Token += aJSON[i]; + break; + } + stack.Push(new JSONClass()); + if (ctx != null) + { + TokenName = TokenName.Trim(); + if (ctx is JSONArray) + ctx.Add(stack.Peek()); + else if (TokenName != "") + ctx.Add(TokenName, stack.Peek()); + } + TokenName = ""; + Token = ""; + ctx = stack.Peek(); + break; + + case '[': + if (QuoteMode) + { + Token += aJSON[i]; + break; + } + + stack.Push(new JSONArray()); + if (ctx != null) + { + TokenName = TokenName.Trim(); + + if (ctx is JSONArray) + ctx.Add(stack.Peek()); + else if (TokenName != "") + ctx.Add(TokenName, stack.Peek()); + } + TokenName = ""; + Token = ""; + ctx = stack.Peek(); + break; + + case '}': + case ']': + if (QuoteMode) + { + Token += aJSON[i]; + break; + } + if (stack.Count == 0) + throw new Exception("JSON Parse: Too many closing brackets"); + + stack.Pop(); + if (Token != "") + { + TokenName = TokenName.Trim(); + /* + if (ctx is JSONArray) + ctx.Add (Token); + else if (TokenName != "") + ctx.Add (TokenName, Token); + */ + AddElement(ctx, Token, TokenName, TokenIsString); + TokenIsString = false; + } + TokenName = ""; + Token = ""; + if (stack.Count > 0) + ctx = stack.Peek(); + break; + + case ':': + if (QuoteMode) + { + Token += aJSON[i]; + break; + } + TokenName = Token; + Token = ""; + TokenIsString = false; + break; + + case '"': + QuoteMode ^= true; + TokenIsString = QuoteMode == true ? true : TokenIsString; + break; + + case ',': + if (QuoteMode) + { + Token += aJSON[i]; + break; + } + if (Token != "") + { + /* + if (ctx is JSONArray) { + ctx.Add (Token); + } else if (TokenName != "") { + ctx.Add (TokenName, Token); + } + */ + AddElement(ctx, Token, TokenName, TokenIsString); + TokenIsString = false; + + } + TokenName = ""; + Token = ""; + TokenIsString = false; + break; + + case '\r': + case '\n': + break; + + case ' ': + case '\t': + if (QuoteMode) + Token += aJSON[i]; + break; + + case '\\': + ++i; + if (QuoteMode) + { + char C = aJSON[i]; + switch (C) + { + case 't': + Token += '\t'; + break; + case 'r': + Token += '\r'; + break; + case 'n': + Token += '\n'; + break; + case 'b': + Token += '\b'; + break; + case 'f': + Token += '\f'; + break; + case 'u': + { + string s = aJSON.Substring(i + 1, 4); + Token += (char)int.Parse( + s, + System.Globalization.NumberStyles.AllowHexSpecifier); + i += 4; + break; + } + default: + Token += C; + break; + } + } + break; + + default: + Token += aJSON[i]; + break; + } + ++i; + } if (QuoteMode) - Token += aJSON[i]; - break; - - case '\\': - ++i; - if (QuoteMode) { - char C = aJSON[i]; - switch (C) { - case 't': - Token += '\t'; - break; - case 'r': - Token += '\r'; - break; - case 'n': - Token += '\n'; - break; - case 'b': - Token += '\b'; - break; - case 'f': - Token += '\f'; - break; - case 'u': { - string s = aJSON.Substring(i + 1, 4); - Token += (char) int.Parse( - s, - System.Globalization.NumberStyles.AllowHexSpecifier); - i += 4; - break; - } - default: - Token += C; - break; - } - } - break; - - default: - Token += aJSON[i]; - break; - } - ++i; - } - if (QuoteMode) { - throw new Exception("JSON Parse: Quotation marks seems to be messed up."); - } - return ctx; - } + { + throw new Exception("JSON Parse: Quotation marks seems to be messed up."); + } + return ctx; + } - public virtual void Serialize(System.IO.BinaryWriter aWriter) { - } + public virtual void Serialize(System.IO.BinaryWriter aWriter) + { + } - public void SaveToStream(System.IO.Stream aData) { - var W = new System.IO.BinaryWriter(aData); - Serialize(W); - } + public void SaveToStream(System.IO.Stream aData) + { + var W = new System.IO.BinaryWriter(aData); + Serialize(W); + } #if USE_SharpZipLib public void SaveToCompressedStream(System.IO.Stream aData) @@ -502,79 +567,97 @@ public string SaveToCompressedBase64() } #else - public void SaveToCompressedStream(System.IO.Stream aData) { - throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); - } + public void SaveToCompressedStream(System.IO.Stream aData) + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } - public void SaveToCompressedFile(string aFileName) { - throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); - } + public void SaveToCompressedFile(string aFileName) + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } - public string SaveToCompressedBase64() { - throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); - } + public string SaveToCompressedBase64() + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } #endif - public void SaveToFile(string aFileName) { + public void SaveToFile(string aFileName) + { #if USE_FileIO - System.IO.Directory.CreateDirectory((new System.IO.FileInfo(aFileName)).Directory.FullName); - using (var F = System.IO.File.OpenWrite(aFileName)) { - SaveToStream(F); - } + System.IO.Directory.CreateDirectory((new System.IO.FileInfo(aFileName)).Directory.FullName); + using (var F = System.IO.File.OpenWrite(aFileName)) + { + SaveToStream(F); + } #else throw new Exception ("Can't use File IO stuff in webplayer"); #endif - } + } - public string SaveToBase64() { - using (var stream = new System.IO.MemoryStream()) { - SaveToStream(stream); - stream.Position = 0; - return System.Convert.ToBase64String(stream.ToArray()); - } - } + public string SaveToBase64() + { + using (var stream = new System.IO.MemoryStream()) + { + SaveToStream(stream); + stream.Position = 0; + return System.Convert.ToBase64String(stream.ToArray()); + } + } - public static JSONNode Deserialize(System.IO.BinaryReader aReader) { - JSONBinaryTag type = (JSONBinaryTag) aReader.ReadByte(); - switch (type) { - case JSONBinaryTag.Array: { - int count = aReader.ReadInt32(); - JSONArray tmp = new JSONArray(); - for (int i = 0; i < count; i++) - tmp.Add(Deserialize(aReader)); - return tmp; - } - case JSONBinaryTag.Class: { - int count = aReader.ReadInt32(); - JSONClass tmp = new JSONClass(); - for (int i = 0; i < count; i++) { - string key = aReader.ReadString(); - var val = Deserialize(aReader); - tmp.Add(key, val); + public static JSONNode Deserialize(System.IO.BinaryReader aReader) + { + JSONBinaryTag type = (JSONBinaryTag)aReader.ReadByte(); + switch (type) + { + case JSONBinaryTag.Array: + { + int count = aReader.ReadInt32(); + JSONArray tmp = new JSONArray(); + for (int i = 0; i < count; i++) + tmp.Add(Deserialize(aReader)); + return tmp; + } + case JSONBinaryTag.Class: + { + int count = aReader.ReadInt32(); + JSONClass tmp = new JSONClass(); + for (int i = 0; i < count; i++) + { + string key = aReader.ReadString(); + var val = Deserialize(aReader); + tmp.Add(key, val); + } + return tmp; + } + case JSONBinaryTag.Value: + { + return new JSONData(aReader.ReadString()); + } + case JSONBinaryTag.IntValue: + { + return new JSONData(aReader.ReadInt32()); + } + case JSONBinaryTag.DoubleValue: + { + return new JSONData(aReader.ReadDouble()); + } + case JSONBinaryTag.BoolValue: + { + return new JSONData(aReader.ReadBoolean()); + } + case JSONBinaryTag.FloatValue: + { + return new JSONData(aReader.ReadSingle()); + } + + default: + { + throw new Exception("Error deserializing JSON. Unknown tag: " + type); + } } - return tmp; - } - case JSONBinaryTag.Value: { - return new JSONData(aReader.ReadString()); - } - case JSONBinaryTag.IntValue: { - return new JSONData(aReader.ReadInt32()); - } - case JSONBinaryTag.DoubleValue: { - return new JSONData(aReader.ReadDouble()); - } - case JSONBinaryTag.BoolValue: { - return new JSONData(aReader.ReadBoolean()); - } - case JSONBinaryTag.FloatValue: { - return new JSONData(aReader.ReadSingle()); - } - - default: { - throw new Exception("Error deserializing JSON. Unknown tag: " + type); - } - } - } + } #if USE_SharpZipLib public static JSONNode LoadFromCompressedStream(System.IO.Stream aData) @@ -601,535 +684,650 @@ public static JSONNode LoadFromCompressedBase64(string aBase64) return LoadFromCompressedStream(stream); } #else - public static JSONNode LoadFromCompressedFile(string aFileName) { - throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); - } + public static JSONNode LoadFromCompressedFile(string aFileName) + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } - public static JSONNode LoadFromCompressedStream(System.IO.Stream aData) { - throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); - } + public static JSONNode LoadFromCompressedStream(System.IO.Stream aData) + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } - public static JSONNode LoadFromCompressedBase64(string aBase64) { - throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); - } + public static JSONNode LoadFromCompressedBase64(string aBase64) + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } #endif - public static JSONNode LoadFromStream(System.IO.Stream aData) { - using (var R = new System.IO.BinaryReader(aData)) { - return Deserialize(R); - } - } + public static JSONNode LoadFromStream(System.IO.Stream aData) + { + using (var R = new System.IO.BinaryReader(aData)) + { + return Deserialize(R); + } + } - public static JSONNode LoadFromFile(string aFileName) { + public static JSONNode LoadFromFile(string aFileName) + { #if USE_FileIO - using (var F = System.IO.File.OpenRead(aFileName)) { - return LoadFromStream(F); - } + using (var F = System.IO.File.OpenRead(aFileName)) + { + return LoadFromStream(F); + } #else throw new Exception ("Can't use File IO stuff in webplayer"); #endif - } + } - public static JSONNode LoadFromBase64(string aBase64) { - var tmp = System.Convert.FromBase64String(aBase64); - var stream = new System.IO.MemoryStream(tmp); - stream.Position = 0; - return LoadFromStream(stream); - } - } - // End of JSONNode - - public class JSONArray : JSONNode, IEnumerable { - private List m_List = new List(); - - public override JSONNode this[int aIndex] { - get { - if (aIndex < 0 || aIndex >= m_List.Count) - return new JSONLazyCreator(this); - return m_List[aIndex]; - } - set { - if (aIndex < 0 || aIndex >= m_List.Count) - m_List.Add(value); - else - m_List[aIndex] = value; - } + public static JSONNode LoadFromBase64(string aBase64) + { + var tmp = System.Convert.FromBase64String(aBase64); + var stream = new System.IO.MemoryStream(tmp); + stream.Position = 0; + return LoadFromStream(stream); + } } + // End of JSONNode - public override JSONNode this[string aKey] { - get { return new JSONLazyCreator(this); } - set { m_List.Add(value); } - } + public class JSONArray : JSONNode, IEnumerable + { + private List m_List = new List(); - public override int Count { - get { return m_List.Count; } - } + public override JSONNode this[int aIndex] + { + get + { + if (aIndex < 0 || aIndex >= m_List.Count) + return new JSONLazyCreator(this); + return m_List[aIndex]; + } + set + { + if (aIndex < 0 || aIndex >= m_List.Count) + m_List.Add(value); + else + m_List[aIndex] = value; + } + } - public override void Add(string aKey, JSONNode aItem) { - m_List.Add(aItem); - } + public override JSONNode this[string aKey] + { + get { return new JSONLazyCreator(this); } + set { m_List.Add(value); } + } - public override JSONNode Remove(int aIndex) { - if (aIndex < 0 || aIndex >= m_List.Count) - return null; - JSONNode tmp = m_List[aIndex]; - m_List.RemoveAt(aIndex); - return tmp; - } + public override int Count + { + get { return m_List.Count; } + } - public override JSONNode Remove(JSONNode aNode) { - m_List.Remove(aNode); - return aNode; - } + public override void Add(string aKey, JSONNode aItem) + { + m_List.Add(aItem); + } - public override IEnumerable Children { - get { - foreach (JSONNode N in m_List) - yield return N; - } - } + public override JSONNode Remove(int aIndex) + { + if (aIndex < 0 || aIndex >= m_List.Count) + return null; + JSONNode tmp = m_List[aIndex]; + m_List.RemoveAt(aIndex); + return tmp; + } - public IEnumerator GetEnumerator() { - foreach (JSONNode N in m_List) - yield return N; - } + public override JSONNode Remove(JSONNode aNode) + { + m_List.Remove(aNode); + return aNode; + } - public override string ToString() { - string result = "[ "; - foreach (JSONNode N in m_List) { - if (result.Length > 2) - result += ", "; - result += N.ToString(); - } - result += " ]"; - return result; - } + public override IEnumerable Children + { + get + { + foreach (JSONNode N in m_List) + yield return N; + } + } - public override string ToString(string aPrefix) { - string result = "[ "; - foreach (JSONNode N in m_List) { - if (result.Length > 3) - result += ", "; - result += "\n" + aPrefix + " "; - result += N.ToString(aPrefix + " "); - } - result += "\n" + aPrefix + "]"; - return result; - } + public IEnumerator GetEnumerator() + { + foreach (JSONNode N in m_List) + yield return N; + } - public override string ToJSON(int prefix) { - string s = new string(' ', (prefix + 1) * 2); - string ret = "[ "; - foreach (JSONNode n in m_List) { - if (ret.Length > 3) - ret += ", "; - ret += "\n" + s; - ret += n.ToJSON(prefix + 1); - - } - ret += "\n" + s + "]"; - return ret; - } + public override string ToString() + { + string result = "[ "; + foreach (JSONNode N in m_List) + { + if (result.Length > 2) + result += ", "; + result += N.ToString(); + } + result += " ]"; + return result; + } - public override void Serialize(System.IO.BinaryWriter aWriter) { - aWriter.Write((byte) JSONBinaryTag.Array); - aWriter.Write(m_List.Count); - for (int i = 0; i < m_List.Count; i++) { - m_List[i].Serialize(aWriter); - } - } - } - // End of JSONArray + public override string ToString(string aPrefix) + { + string result = "[ "; + foreach (JSONNode N in m_List) + { + if (result.Length > 3) + result += ", "; + result += "\n" + aPrefix + " "; + result += N.ToString(aPrefix + " "); + } + result += "\n" + aPrefix + "]"; + return result; + } - public class JSONClass : JSONNode, IEnumerable { - private Dictionary m_Dict = new Dictionary(); + public override string ToJSON(int prefix) + { + string s = new string(' ', (prefix + 1) * 2); + string ret = "[ "; + foreach (JSONNode n in m_List) + { + if (ret.Length > 3) + ret += ", "; + ret += "\n" + s; + ret += n.ToJSON(prefix + 1); - public Dictionary Dict { - get { - return m_Dict; - } - } + } + ret += "\n" + s + "]"; + return ret; + } - public override JSONNode this[string aKey] { - get { - if (m_Dict.ContainsKey(aKey)) - return m_Dict[aKey]; - else - return new JSONLazyCreator(this, aKey); - } - set { - if (m_Dict.ContainsKey(aKey)) - m_Dict[aKey] = value; - else - m_Dict.Add(aKey, value); - } + public override void Serialize(System.IO.BinaryWriter aWriter) + { + aWriter.Write((byte)JSONBinaryTag.Array); + aWriter.Write(m_List.Count); + for (int i = 0; i < m_List.Count; i++) + { + m_List[i].Serialize(aWriter); + } + } } + // End of JSONArray - public override JSONNode this[int aIndex] { - get { - if (aIndex < 0 || aIndex >= m_Dict.Count) - return null; - return m_Dict.ElementAt(aIndex).Value; - } - set { - if (aIndex < 0 || aIndex >= m_Dict.Count) - return; - string key = m_Dict.ElementAt(aIndex).Key; - m_Dict[key] = value; - } - } + public class JSONClass : JSONNode, IEnumerable + { + private Dictionary m_Dict = new Dictionary(); - public override int Count { - get { return m_Dict.Count; } - } + public Dictionary Dict + { + get + { + return m_Dict; + } + } + public override JSONNode this[string aKey] + { + get + { + if (m_Dict.ContainsKey(aKey)) + return m_Dict[aKey]; + else + return new JSONLazyCreator(this, aKey); + } + set + { + if (m_Dict.ContainsKey(aKey)) + m_Dict[aKey] = value; + else + m_Dict.Add(aKey, value); + } + } - public override void Add(string aKey, JSONNode aItem) { - if (!string.IsNullOrEmpty(aKey)) { - if (m_Dict.ContainsKey(aKey)) - m_Dict[aKey] = aItem; - else - m_Dict.Add(aKey, aItem); - } else - m_Dict.Add(Guid.NewGuid().ToString(), aItem); - } + public override JSONNode this[int aIndex] + { + get + { + if (aIndex < 0 || aIndex >= m_Dict.Count) + return null; + return m_Dict.ElementAt(aIndex).Value; + } + set + { + if (aIndex < 0 || aIndex >= m_Dict.Count) + return; + string key = m_Dict.ElementAt(aIndex).Key; + m_Dict[key] = value; + } + } - public override JSONNode Remove(string aKey) { - if (!m_Dict.ContainsKey(aKey)) - return null; - JSONNode tmp = m_Dict[aKey]; - m_Dict.Remove(aKey); - return tmp; - } + public override int Count + { + get { return m_Dict.Count; } + } - public override JSONNode Remove(int aIndex) { - if (aIndex < 0 || aIndex >= m_Dict.Count) - return null; - var item = m_Dict.ElementAt(aIndex); - m_Dict.Remove(item.Key); - return item.Value; - } - public override JSONNode Remove(JSONNode aNode) { - try { - var item = m_Dict.Where(k => k.Value == aNode).First(); - m_Dict.Remove(item.Key); - return aNode; - } catch { - return null; - } - } + public override void Add(string aKey, JSONNode aItem) + { + if (!string.IsNullOrEmpty(aKey)) + { + if (m_Dict.ContainsKey(aKey)) + m_Dict[aKey] = aItem; + else + m_Dict.Add(aKey, aItem); + } + else + m_Dict.Add(Guid.NewGuid().ToString(), aItem); + } - public override IEnumerable Children { - get { - foreach (KeyValuePair N in m_Dict) - yield return N.Value; - } - } + public override JSONNode Remove(string aKey) + { + if (!m_Dict.ContainsKey(aKey)) + return null; + JSONNode tmp = m_Dict[aKey]; + m_Dict.Remove(aKey); + return tmp; + } - public IEnumerator GetEnumerator() { - foreach (KeyValuePair N in m_Dict) - yield return N; - } + public override JSONNode Remove(int aIndex) + { + if (aIndex < 0 || aIndex >= m_Dict.Count) + return null; + var item = m_Dict.ElementAt(aIndex); + m_Dict.Remove(item.Key); + return item.Value; + } - public override string ToString() { - string result = "{"; - foreach (KeyValuePair N in m_Dict) { - if (result.Length > 2) - result += ", "; - result += "\"" + Escape(N.Key) + "\":" + N.Value.ToString(); - } - result += "}"; - return result; - } + public override JSONNode Remove(JSONNode aNode) + { + try + { + var item = m_Dict.Where(k => k.Value == aNode).First(); + m_Dict.Remove(item.Key); + return aNode; + } + catch + { + return null; + } + } - public override string ToString(string aPrefix) { - string result = "{ "; - foreach (KeyValuePair N in m_Dict) { - if (result.Length > 3) - result += ", "; - result += "\n" + aPrefix + " "; - result += "\"" + Escape(N.Key) + "\" : " + N.Value.ToString(aPrefix + " "); - } - result += "\n" + aPrefix + "}"; - return result; - } + public override IEnumerable Children + { + get + { + foreach (KeyValuePair N in m_Dict) + yield return N.Value; + } + } - public override string ToJSON(int prefix) { - string s = new string(' ', (prefix + 1) * 2); - string ret = "{ "; - foreach (KeyValuePair n in m_Dict) { - if (ret.Length > 3) - ret += ", "; - ret += "\n" + s; - ret += string.Format("\"{0}\": {1}", n.Key, n.Value.ToJSON(prefix + 1)); - } - ret += "\n" + s + "}"; - return ret; - } + public IEnumerator GetEnumerator() + { + foreach (KeyValuePair N in m_Dict) + yield return N; + } - public override void Serialize(System.IO.BinaryWriter aWriter) { - aWriter.Write((byte) JSONBinaryTag.Class); - aWriter.Write(m_Dict.Count); - foreach (string K in m_Dict.Keys) { - aWriter.Write(K); - m_Dict[K].Serialize(aWriter); - } - } - } - // End of JSONClass + public override string ToString() + { + string result = "{"; + foreach (KeyValuePair N in m_Dict) + { + if (result.Length > 2) + result += ", "; + result += "\"" + Escape(N.Key) + "\":" + N.Value.ToString(); + } + result += "}"; + return result; + } - public class JSONData : JSONNode { - private string m_Data; + public override string ToString(string aPrefix) + { + string result = "{ "; + foreach (KeyValuePair N in m_Dict) + { + if (result.Length > 3) + result += ", "; + result += "\n" + aPrefix + " "; + result += "\"" + Escape(N.Key) + "\" : " + N.Value.ToString(aPrefix + " "); + } + result += "\n" + aPrefix + "}"; + return result; + } + public override string ToJSON(int prefix) + { + string s = new string(' ', (prefix + 1) * 2); + string ret = "{ "; + foreach (KeyValuePair n in m_Dict) + { + if (ret.Length > 3) + ret += ", "; + ret += "\n" + s; + ret += string.Format("\"{0}\": {1}", n.Key, n.Value.ToJSON(prefix + 1)); + } + ret += "\n" + s + "}"; + return ret; + } - public override string Value { - get { return m_Data; } - set { - m_Data = value; - Tag = JSONBinaryTag.Value; - } + public override void Serialize(System.IO.BinaryWriter aWriter) + { + aWriter.Write((byte)JSONBinaryTag.Class); + aWriter.Write(m_Dict.Count); + foreach (string K in m_Dict.Keys) + { + aWriter.Write(K); + m_Dict[K].Serialize(aWriter); + } + } } + // End of JSONClass - public JSONData(string aData) { - m_Data = aData; - Tag = JSONBinaryTag.Value; - } + public class JSONData : JSONNode + { + private string m_Data; - public JSONData(float aData) { - AsFloat = aData; - Tag = JSONBinaryTag.FloatValue; - } - public JSONData(double aData) { - AsDouble = aData; - Tag = JSONBinaryTag.DoubleValue; - } + public override string Value + { + get { return m_Data; } + set + { + m_Data = value; + Tag = JSONBinaryTag.Value; + } + } - public JSONData(bool aData) { - AsBool = aData; - Tag = JSONBinaryTag.BoolValue; - } + public JSONData(string aData) + { + m_Data = aData; + Tag = JSONBinaryTag.Value; + } - public JSONData(int aData) { - AsInt = aData; - Tag = JSONBinaryTag.IntValue; - } + public JSONData(float aData) + { + AsFloat = aData; + Tag = JSONBinaryTag.FloatValue; + } - public override string ToString() { - return "\"" + Escape(m_Data) + "\""; - } + public JSONData(double aData) + { + AsDouble = aData; + Tag = JSONBinaryTag.DoubleValue; + } - public override string ToString(string aPrefix) { - return "\"" + Escape(m_Data) + "\""; - } + public JSONData(bool aData) + { + AsBool = aData; + Tag = JSONBinaryTag.BoolValue; + } - public override string ToJSON(int prefix) { - switch (Tag) { - case JSONBinaryTag.DoubleValue: - case JSONBinaryTag.FloatValue: - case JSONBinaryTag.IntValue: - case JSONBinaryTag.BoolValue: - return m_Data; - case JSONBinaryTag.Value: - return string.Format("\"{0}\"", Escape(m_Data)); - default: - throw new NotSupportedException("This shouldn't be here: " + Tag.ToString()); - } - } + public JSONData(int aData) + { + AsInt = aData; + Tag = JSONBinaryTag.IntValue; + } - public override void Serialize(System.IO.BinaryWriter aWriter) { - var tmp = new JSONData(""); - - tmp.AsInt = AsInt; - if (tmp.m_Data == this.m_Data) { - aWriter.Write((byte) JSONBinaryTag.IntValue); - aWriter.Write(AsInt); - return; - } - tmp.AsFloat = AsFloat; - if (tmp.m_Data == this.m_Data) { - aWriter.Write((byte) JSONBinaryTag.FloatValue); - aWriter.Write(AsFloat); - return; - } - tmp.AsDouble = AsDouble; - if (tmp.m_Data == this.m_Data) { - aWriter.Write((byte) JSONBinaryTag.DoubleValue); - aWriter.Write(AsDouble); - return; - } - - tmp.AsBool = AsBool; - if (tmp.m_Data == this.m_Data) { - aWriter.Write((byte) JSONBinaryTag.BoolValue); - aWriter.Write(AsBool); - return; - } - aWriter.Write((byte) JSONBinaryTag.Value); - aWriter.Write(m_Data); - } - } - // End of JSONData + public override string ToString() + { + return "\"" + Escape(m_Data) + "\""; + } - internal class JSONLazyCreator : JSONNode { - private JSONNode m_Node = null; - private string m_Key = null; + public override string ToString(string aPrefix) + { + return "\"" + Escape(m_Data) + "\""; + } - public JSONLazyCreator(JSONNode aNode) { - m_Node = aNode; - m_Key = null; - } + public override string ToJSON(int prefix) + { + switch (Tag) + { + case JSONBinaryTag.DoubleValue: + case JSONBinaryTag.FloatValue: + case JSONBinaryTag.IntValue: + case JSONBinaryTag.BoolValue: + return m_Data; + case JSONBinaryTag.Value: + return string.Format("\"{0}\"", Escape(m_Data)); + default: + throw new NotSupportedException("This shouldn't be here: " + Tag.ToString()); + } + } - public JSONLazyCreator(JSONNode aNode, string aKey) { - m_Node = aNode; - m_Key = aKey; - } + public override void Serialize(System.IO.BinaryWriter aWriter) + { + var tmp = new JSONData(""); - private void Set(JSONNode aVal) { - if (m_Key == null) { - m_Node.Add(aVal); - } else { - m_Node.Add(m_Key, aVal); - } - m_Node = null; // Be GC friendly. - } + tmp.AsInt = AsInt; + if (tmp.m_Data == this.m_Data) + { + aWriter.Write((byte)JSONBinaryTag.IntValue); + aWriter.Write(AsInt); + return; + } + tmp.AsFloat = AsFloat; + if (tmp.m_Data == this.m_Data) + { + aWriter.Write((byte)JSONBinaryTag.FloatValue); + aWriter.Write(AsFloat); + return; + } + tmp.AsDouble = AsDouble; + if (tmp.m_Data == this.m_Data) + { + aWriter.Write((byte)JSONBinaryTag.DoubleValue); + aWriter.Write(AsDouble); + return; + } - public override JSONNode this[int aIndex] { - get { - return new JSONLazyCreator(this); - } - set { - var tmp = new JSONArray(); - tmp.Add(value); - Set(tmp); - } + tmp.AsBool = AsBool; + if (tmp.m_Data == this.m_Data) + { + aWriter.Write((byte)JSONBinaryTag.BoolValue); + aWriter.Write(AsBool); + return; + } + aWriter.Write((byte)JSONBinaryTag.Value); + aWriter.Write(m_Data); + } } + // End of JSONData - public override JSONNode this[string aKey] { - get { - return new JSONLazyCreator(this, aKey); - } - set { - var tmp = new JSONClass(); - tmp.Add(aKey, value); - Set(tmp); - } - } + internal class JSONLazyCreator : JSONNode + { + private JSONNode m_Node = null; + private string m_Key = null; - public override void Add(JSONNode aItem) { - var tmp = new JSONArray(); - tmp.Add(aItem); - Set(tmp); - } + public JSONLazyCreator(JSONNode aNode) + { + m_Node = aNode; + m_Key = null; + } - public override void Add(string aKey, JSONNode aItem) { - var tmp = new JSONClass(); - tmp.Add(aKey, aItem); - Set(tmp); - } + public JSONLazyCreator(JSONNode aNode, string aKey) + { + m_Node = aNode; + m_Key = aKey; + } - public static bool operator ==(JSONLazyCreator a, object b) { - if (b == null) - return true; - return System.Object.ReferenceEquals(a, b); - } + private void Set(JSONNode aVal) + { + if (m_Key == null) + { + m_Node.Add(aVal); + } + else + { + m_Node.Add(m_Key, aVal); + } + m_Node = null; // Be GC friendly. + } - public static bool operator !=(JSONLazyCreator a, object b) { - return !(a == b); - } + public override JSONNode this[int aIndex] + { + get + { + return new JSONLazyCreator(this); + } + set + { + var tmp = new JSONArray(); + tmp.Add(value); + Set(tmp); + } + } - public override bool Equals(object obj) { - if (obj == null) - return true; - return System.Object.ReferenceEquals(this, obj); - } + public override JSONNode this[string aKey] + { + get + { + return new JSONLazyCreator(this, aKey); + } + set + { + var tmp = new JSONClass(); + tmp.Add(aKey, value); + Set(tmp); + } + } - public override int GetHashCode() { - return base.GetHashCode(); - } + public override void Add(JSONNode aItem) + { + var tmp = new JSONArray(); + tmp.Add(aItem); + Set(tmp); + } - public override string ToString() { - return ""; - } + public override void Add(string aKey, JSONNode aItem) + { + var tmp = new JSONClass(); + tmp.Add(aKey, aItem); + Set(tmp); + } - public override string ToString(string aPrefix) { - return ""; - } + public static bool operator ==(JSONLazyCreator a, object b) + { + if (b == null) + return true; + return System.Object.ReferenceEquals(a, b); + } - public override string ToJSON(int prefix) { - return ""; - } + public static bool operator !=(JSONLazyCreator a, object b) + { + return !(a == b); + } - public override int AsInt { - get { - JSONData tmp = new JSONData(0); - Set(tmp); - return 0; - } - set { - JSONData tmp = new JSONData(value); - Set(tmp); - } - } + public override bool Equals(object obj) + { + if (obj == null) + return true; + return System.Object.ReferenceEquals(this, obj); + } - public override float AsFloat { - get { - JSONData tmp = new JSONData(0.0f); - Set(tmp); - return 0.0f; - } - set { - JSONData tmp = new JSONData(value); - Set(tmp); - } - } + public override int GetHashCode() + { + return base.GetHashCode(); + } - public override double AsDouble { - get { - JSONData tmp = new JSONData(0.0); - Set(tmp); - return 0.0; - } - set { - JSONData tmp = new JSONData(value); - Set(tmp); - } - } + public override string ToString() + { + return ""; + } - public override bool AsBool { - get { - JSONData tmp = new JSONData(false); - Set(tmp); - return false; - } - set { - JSONData tmp = new JSONData(value); - Set(tmp); - } - } + public override string ToString(string aPrefix) + { + return ""; + } - public override JSONArray AsArray { - get { - JSONArray tmp = new JSONArray(); - Set(tmp); - return tmp; - } - } + public override string ToJSON(int prefix) + { + return ""; + } + + public override int AsInt + { + get + { + JSONData tmp = new JSONData(0); + Set(tmp); + return 0; + } + set + { + JSONData tmp = new JSONData(value); + Set(tmp); + } + } - public override JSONClass AsObject { - get { - JSONClass tmp = new JSONClass(); - Set(tmp); - return tmp; - } + public override float AsFloat + { + get + { + JSONData tmp = new JSONData(0.0f); + Set(tmp); + return 0.0f; + } + set + { + JSONData tmp = new JSONData(value); + Set(tmp); + } + } + + public override double AsDouble + { + get + { + JSONData tmp = new JSONData(0.0); + Set(tmp); + return 0.0; + } + set + { + JSONData tmp = new JSONData(value); + Set(tmp); + } + } + + public override bool AsBool + { + get + { + JSONData tmp = new JSONData(false); + Set(tmp); + return false; + } + set + { + JSONData tmp = new JSONData(value); + Set(tmp); + } + } + + public override JSONArray AsArray + { + get + { + JSONArray tmp = new JSONArray(); + Set(tmp); + return tmp; + } + } + + public override JSONClass AsObject + { + get + { + JSONClass tmp = new JSONClass(); + Set(tmp); + return tmp; + } + } } - } - // End of JSONLazyCreator + // End of JSONLazyCreator - public static class JSON { - public static JSONNode Parse(string aJSON) { - return JSONNode.Parse(aJSON); + public static class JSON + { + public static JSONNode Parse(string aJSON) + { + return JSONNode.Parse(aJSON); + } } - } } diff --git a/Assets/Scripts/ThirdParty/GlTF/TextureScale.cs b/Assets/Scripts/ThirdParty/GlTF/TextureScale.cs index 63186753..6cb0d1a3 100644 --- a/Assets/Scripts/ThirdParty/GlTF/TextureScale.cs +++ b/Assets/Scripts/ThirdParty/GlTF/TextureScale.cs @@ -3,128 +3,156 @@ using System.Threading; using UnityEngine; -public class GltfTextureScale { - public class ThreadData { - public int start; - public int end; - public ThreadData(int s, int e) { - start = s; - end = e; +public class GltfTextureScale +{ + public class ThreadData + { + public int start; + public int end; + public ThreadData(int s, int e) + { + start = s; + end = e; + } } - } - private static Color[] texColors; - private static Color[] newColors; - private static int w; - private static float ratioX; - private static float ratioY; - private static int w2; - private static int finishCount; - private static Mutex mutex; + private static Color[] texColors; + private static Color[] newColors; + private static int w; + private static float ratioX; + private static float ratioY; + private static int w2; + private static int finishCount; + private static Mutex mutex; - public static void Point(Texture2D tex, int newWidth, int newHeight) { - ThreadedScale(tex, newWidth, newHeight, false); - } - - public static void Bilinear(Texture2D tex, int newWidth, int newHeight) { - ThreadedScale(tex, newWidth, newHeight, true); - } - - private static void ThreadedScale(Texture2D tex, int newWidth, int newHeight, bool useBilinear) { - texColors = tex.GetPixels(); - newColors = new Color[newWidth * newHeight]; - if (useBilinear) { - ratioX = 1.0f / ((float) newWidth / (tex.width - 1)); - ratioY = 1.0f / ((float) newHeight / (tex.height - 1)); - } else { - ratioX = ((float) tex.width) / newWidth; - ratioY = ((float) tex.height) / newHeight; + public static void Point(Texture2D tex, int newWidth, int newHeight) + { + ThreadedScale(tex, newWidth, newHeight, false); } - w = tex.width; - w2 = newWidth; - var cores = Mathf.Min(SystemInfo.processorCount, newHeight); - var slice = newHeight / cores; - finishCount = 0; - if (mutex == null) { - mutex = new Mutex(false); - } - if (cores > 1) { - int i = 0; - ThreadData threadData; - for (i = 0; i < cores - 1; i++) { - threadData = new ThreadData(slice * i, slice * (i + 1)); - ParameterizedThreadStart ts = useBilinear ? new ParameterizedThreadStart(BilinearScale) : new ParameterizedThreadStart(PointScale); - Thread thread = new Thread(ts); - thread.Start(threadData); - } - threadData = new ThreadData(slice * i, newHeight); - if (useBilinear) { - BilinearScale(threadData); - } else { - PointScale(threadData); - } - while (finishCount < cores) { - Thread.Sleep(1); - } - } else { - ThreadData threadData = new ThreadData(0, newHeight); - if (useBilinear) { - BilinearScale(threadData); - } else { - PointScale(threadData); - } + public static void Bilinear(Texture2D tex, int newWidth, int newHeight) + { + ThreadedScale(tex, newWidth, newHeight, true); } - tex.Resize(newWidth, newHeight); - tex.SetPixels(newColors); - tex.Apply(); + private static void ThreadedScale(Texture2D tex, int newWidth, int newHeight, bool useBilinear) + { + texColors = tex.GetPixels(); + newColors = new Color[newWidth * newHeight]; + if (useBilinear) + { + ratioX = 1.0f / ((float)newWidth / (tex.width - 1)); + ratioY = 1.0f / ((float)newHeight / (tex.height - 1)); + } + else + { + ratioX = ((float)tex.width) / newWidth; + ratioY = ((float)tex.height) / newHeight; + } + w = tex.width; + w2 = newWidth; + var cores = Mathf.Min(SystemInfo.processorCount, newHeight); + var slice = newHeight / cores; - texColors = null; - newColors = null; - } + finishCount = 0; + if (mutex == null) + { + mutex = new Mutex(false); + } + if (cores > 1) + { + int i = 0; + ThreadData threadData; + for (i = 0; i < cores - 1; i++) + { + threadData = new ThreadData(slice * i, slice * (i + 1)); + ParameterizedThreadStart ts = useBilinear ? new ParameterizedThreadStart(BilinearScale) : new ParameterizedThreadStart(PointScale); + Thread thread = new Thread(ts); + thread.Start(threadData); + } + threadData = new ThreadData(slice * i, newHeight); + if (useBilinear) + { + BilinearScale(threadData); + } + else + { + PointScale(threadData); + } + while (finishCount < cores) + { + Thread.Sleep(1); + } + } + else + { + ThreadData threadData = new ThreadData(0, newHeight); + if (useBilinear) + { + BilinearScale(threadData); + } + else + { + PointScale(threadData); + } + } - public static void BilinearScale(System.Object obj) { - ThreadData threadData = (ThreadData) obj; - for (var y = threadData.start; y < threadData.end; y++) { - int yFloor = (int) Mathf.Floor(y * ratioY); - var y1 = yFloor * w; - var y2 = (yFloor + 1) * w; - var yw = y * w2; + tex.Resize(newWidth, newHeight); + tex.SetPixels(newColors); + tex.Apply(); - for (var x = 0; x < w2; x++) { - int xFloor = (int) Mathf.Floor(x * ratioX); - var xLerp = x * ratioX - xFloor; - newColors[yw + x] = ColorLerpUnclamped(ColorLerpUnclamped(texColors[y1 + xFloor], texColors[y1 + xFloor + 1], xLerp), - ColorLerpUnclamped(texColors[y2 + xFloor], texColors[y2 + xFloor + 1], xLerp), - y * ratioY - yFloor); - } + texColors = null; + newColors = null; } - mutex.WaitOne(); - finishCount++; - mutex.ReleaseMutex(); - } + public static void BilinearScale(System.Object obj) + { + ThreadData threadData = (ThreadData)obj; + for (var y = threadData.start; y < threadData.end; y++) + { + int yFloor = (int)Mathf.Floor(y * ratioY); + var y1 = yFloor * w; + var y2 = (yFloor + 1) * w; + var yw = y * w2; + + for (var x = 0; x < w2; x++) + { + int xFloor = (int)Mathf.Floor(x * ratioX); + var xLerp = x * ratioX - xFloor; + newColors[yw + x] = ColorLerpUnclamped(ColorLerpUnclamped(texColors[y1 + xFloor], texColors[y1 + xFloor + 1], xLerp), + ColorLerpUnclamped(texColors[y2 + xFloor], texColors[y2 + xFloor + 1], xLerp), + y * ratioY - yFloor); + } + } - public static void PointScale(System.Object obj) { - ThreadData threadData = (ThreadData) obj; - for (var y = threadData.start; y < threadData.end; y++) { - var thisY = (int) (ratioY * y) * w; - var yw = y * w2; - for (var x = 0; x < w2; x++) { - newColors[yw + x] = texColors[(int) (thisY + ratioX * x)]; - } + mutex.WaitOne(); + finishCount++; + mutex.ReleaseMutex(); } - mutex.WaitOne(); - finishCount++; - mutex.ReleaseMutex(); - } + public static void PointScale(System.Object obj) + { + ThreadData threadData = (ThreadData)obj; + for (var y = threadData.start; y < threadData.end; y++) + { + var thisY = (int)(ratioY * y) * w; + var yw = y * w2; + for (var x = 0; x < w2; x++) + { + newColors[yw + x] = texColors[(int)(thisY + ratioX * x)]; + } + } - private static Color ColorLerpUnclamped(Color c1, Color c2, float value) { - return new Color(c1.r + (c2.r - c1.r) * value, - c1.g + (c2.g - c1.g) * value, - c1.b + (c2.b - c1.b) * value, - c1.a + (c2.a - c1.a) * value); - } + mutex.WaitOne(); + finishCount++; + mutex.ReleaseMutex(); + } + + private static Color ColorLerpUnclamped(Color c1, Color c2, float value) + { + return new Color(c1.r + (c2.r - c1.r) * value, + c1.g + (c2.g - c1.g) * value, + c1.b + (c2.b - c1.b) * value, + c1.a + (c2.a - c1.a) * value); + } } diff --git a/Assets/Scripts/ThirdParty/GlTF/google/GlTF_ScriptableExporter.cs b/Assets/Scripts/ThirdParty/GlTF/google/GlTF_ScriptableExporter.cs index 8c05f86c..47f1b499 100644 --- a/Assets/Scripts/ThirdParty/GlTF/google/GlTF_ScriptableExporter.cs +++ b/Assets/Scripts/ThirdParty/GlTF/google/GlTF_ScriptableExporter.cs @@ -10,661 +10,739 @@ // Provides the ability to convert TB geometry into glTF format and save to disk. See the class // GlTFEditorExporter for a simple usage example. -public class GlTFScriptableExporter { - - // Allows the caller to do a custom conversion on each object's transform as they are processed. - // This is an appropriate place to remove unwanted parent transforms. Note that the DX-to-GL - // (RH to LH) conversion will be done automatically and should not be performed by this delegate. - public delegate Matrix4x4 TransformFilter(Transform tr); - - private TransformFilter CurrentTransformFilter { get; set; } - - - // Total number of triangles exported. - public int NumTris { get; private set; } - - // Conversion factor to convert point positions (etc) from local units into meters. - public static float localUnitsToMeters = 1.0f; - - // List of all exported files (so far). - public HashSet exportedFiles; - - // Stores certain glTF elements to be included in the exported output. - public Preset preset; - - // Corresponds to the current named object being exported. The name will be used to construct the - // various dependent glTF components. If using ExportSimpleMesh(), this must be set first. If - // using ExportMesh(), there's no need to set it as that routine will do so. - public string BaseName { - set { namedObject.name = value; } - get { return namedObject.name; } - } - - public Matrix4x4 IdentityFilter(Transform tr) { - return tr.localToWorldMatrix; - } - - // Call this first, specifying output path in outPath, the glTF preset, and directory with - // existing assets to be included, sourceDir. - public void BeginExport(string outPath, Preset preset, TransformFilter xfFilter) { - CurrentTransformFilter = xfFilter; - this.outPath = outPath; - this.preset = preset; - writer = new GlTF_Writer(); - writer.Init(); - writer.OpenFiles(outPath); - NumTris = 0; - exportedFiles = new HashSet(); - } - - public void SetMetadata(string generator, string version, string copyright) { - Debug.Assert(writer != null); - writer.Generator = generator; - writer.Version = version; - writer.Copyright = copyright; - } - - // Call this last. - public void EndExport() { - // sanity check because this code is bug-riddled - foreach (var pair in GlTF_Writer.nodes) { - if (pair.Key != pair.Value.name) { - Debug.LogWarningFormat("Buggy key/value in nodes: {0} {1}", pair.Key, pair.Value.name); - } - } - writer.Write(); - writer.CloseFiles(); - exportedFiles.UnionWith(writer.exportedFiles); - Debug.LogFormat("Wrote files:\n {0}", String.Join("\n ", exportedFiles.ToArray())); - Debug.LogFormat("Saved {0} triangle(s) to {1}.", NumTris, outPath); - GameObject.Destroy(namedObject); - } - - private GlTF_Material GetMaterial() { - var mtlName = GlTF_Material.GetNameFromObject(namedObject); - Debug.Assert(GlTF_Writer.materials.ContainsKey(mtlName)); - return GlTF_Writer.materials[mtlName]; - } - - // Export a single shader float uniform - public void ExportShaderUniform(string name, float value) { - GlTF_Material mtl = GetMaterial(); - var float_val = new GlTF_Material.FloatValue(); - float_val.name = name; - float_val.value = value; - mtl.values.Add(float_val); - AddUniform(float_val.name, GlTF_Technique.Type.FLOAT, GlTF_Technique.Semantic.UNKNOWN); - } - - // Export a single shader color uniform - public void ExportShaderUniform(string name, Color value) { - GlTF_Material mtl = GetMaterial(); - var color_val = new GlTF_Material.ColorValue(); - color_val.name = name; - color_val.color = value; - mtl.values.Add(color_val); - AddUniform(color_val.name, GlTF_Technique.Type.FLOAT_VEC4, GlTF_Technique.Semantic.UNKNOWN); - } - - // Export a single shader vector uniform - public void ExportShaderUniform(string name, Vector4 value) { - GlTF_Material mtl = GetMaterial(); - var vec_val = new GlTF_Material.VectorValue(); - vec_val.name = name; - vec_val.vector = value; - mtl.values.Add(vec_val); - AddUniform(vec_val.name, GlTF_Technique.Type.FLOAT_VEC4, GlTF_Technique.Semantic.UNKNOWN); - } - - // Exports per-material specular parameters. - public void ExportSpecularParams(float shininess, Color color) { - var mtlName = GlTF_Material.GetNameFromObject(namedObject); - Debug.Assert(GlTF_Writer.materials.ContainsKey(mtlName)); - GlTF_Material mtl = GlTF_Writer.materials[mtlName]; - - var color_val = new GlTF_Material.ColorValue(); - color_val.name = "specular_color"; - color_val.color = color; - mtl.values.Add(color_val); - AddUniform(color_val.name, - GlTF_Technique.Type.FLOAT_VEC4, GlTF_Technique.Semantic.UNKNOWN); - - var float_val = new GlTF_Material.FloatValue(); - float_val.name = "specular_shininess"; - float_val.value = shininess; - mtl.values.Add(float_val); - AddUniform(float_val.name, - GlTF_Technique.Type.FLOAT, GlTF_Technique.Semantic.UNKNOWN); - } - - // Should be called per material. - public void ExportAmbientLight(Color color) { - var mtlName = GlTF_Material.GetNameFromObject(namedObject); - Debug.Assert(GlTF_Writer.materials.ContainsKey(mtlName)); - GlTF_Material mtl = GlTF_Writer.materials[mtlName]; - var val = new GlTF_Material.ColorValue(); - val.name = "ambient_light_color"; - val.color = color; - mtl.values.Add(val); - AddUniform(val.name, - GlTF_Technique.Type.FLOAT_VEC4, GlTF_Technique.Semantic.UNKNOWN); - } - - // Should be called per material. - public void ExportLight(Transform tr) { - Light unityLight = tr.GetComponent(); - Debug.Assert(unityLight != null); - Color lightColor = unityLight.color * unityLight.intensity; - lightColor.a = 1.0f; // No use for alpha with light color. - string lightName = GlTF_Writer.GetNameFromObject(tr); - switch (tr.GetComponent().type) { - case LightType.Point: - GlTF_PointLight pl = new GlTF_PointLight(); - pl.color = new GlTF_ColorRGB(lightColor); - pl.name = lightName; - GlTF_Writer.lights.Add(pl); - break; - case LightType.Spot: - GlTF_SpotLight sl = new GlTF_SpotLight(); - sl.color = new GlTF_ColorRGB(lightColor); - sl.name = lightName; - GlTF_Writer.lights.Add(sl); - break; - case LightType.Directional: - GlTF_DirectionalLight dl = new GlTF_DirectionalLight(); - dl.color = new GlTF_ColorRGB(lightColor); - dl.name = lightName; - GlTF_Writer.lights.Add(dl); - break; - case LightType.Area: - GlTF_AmbientLight al = new GlTF_AmbientLight(); - al.color = new GlTF_ColorRGB(lightColor); - al.name = lightName; - GlTF_Writer.lights.Add(al); - break; - } +public class GlTFScriptableExporter +{ + + // Allows the caller to do a custom conversion on each object's transform as they are processed. + // This is an appropriate place to remove unwanted parent transforms. Note that the DX-to-GL + // (RH to LH) conversion will be done automatically and should not be performed by this delegate. + public delegate Matrix4x4 TransformFilter(Transform tr); + + private TransformFilter CurrentTransformFilter { get; set; } + + + // Total number of triangles exported. + public int NumTris { get; private set; } + + // Conversion factor to convert point positions (etc) from local units into meters. + public static float localUnitsToMeters = 1.0f; + + // List of all exported files (so far). + public HashSet exportedFiles; + + // Stores certain glTF elements to be included in the exported output. + public Preset preset; + + // Corresponds to the current named object being exported. The name will be used to construct the + // various dependent glTF components. If using ExportSimpleMesh(), this must be set first. If + // using ExportMesh(), there's no need to set it as that routine will do so. + public string BaseName + { + set { namedObject.name = value; } + get { return namedObject.name; } + } + + public Matrix4x4 IdentityFilter(Transform tr) + { + return tr.localToWorldMatrix; + } + + // Call this first, specifying output path in outPath, the glTF preset, and directory with + // existing assets to be included, sourceDir. + public void BeginExport(string outPath, Preset preset, TransformFilter xfFilter) + { + CurrentTransformFilter = xfFilter; + this.outPath = outPath; + this.preset = preset; + writer = new GlTF_Writer(); + writer.Init(); + writer.OpenFiles(outPath); + NumTris = 0; + exportedFiles = new HashSet(); + } + + public void SetMetadata(string generator, string version, string copyright) + { + Debug.Assert(writer != null); + writer.Generator = generator; + writer.Version = version; + writer.Copyright = copyright; + } + + // Call this last. + public void EndExport() + { + // sanity check because this code is bug-riddled + foreach (var pair in GlTF_Writer.nodes) + { + if (pair.Key != pair.Value.name) + { + Debug.LogWarningFormat("Buggy key/value in nodes: {0} {1}", pair.Key, pair.Value.name); + } + } + writer.Write(); + writer.CloseFiles(); + exportedFiles.UnionWith(writer.exportedFiles); + Debug.LogFormat("Wrote files:\n {0}", String.Join("\n ", exportedFiles.ToArray())); + Debug.LogFormat("Saved {0} triangle(s) to {1}.", NumTris, outPath); + GameObject.Destroy(namedObject); + } + + private GlTF_Material GetMaterial() + { + var mtlName = GlTF_Material.GetNameFromObject(namedObject); + Debug.Assert(GlTF_Writer.materials.ContainsKey(mtlName)); + return GlTF_Writer.materials[mtlName]; + } + + // Export a single shader float uniform + public void ExportShaderUniform(string name, float value) + { + GlTF_Material mtl = GetMaterial(); + var float_val = new GlTF_Material.FloatValue(); + float_val.name = name; + float_val.value = value; + mtl.values.Add(float_val); + AddUniform(float_val.name, GlTF_Technique.Type.FLOAT, GlTF_Technique.Semantic.UNKNOWN); + } + + // Export a single shader color uniform + public void ExportShaderUniform(string name, Color value) + { + GlTF_Material mtl = GetMaterial(); + var color_val = new GlTF_Material.ColorValue(); + color_val.name = name; + color_val.color = value; + mtl.values.Add(color_val); + AddUniform(color_val.name, GlTF_Technique.Type.FLOAT_VEC4, GlTF_Technique.Semantic.UNKNOWN); + } + + // Export a single shader vector uniform + public void ExportShaderUniform(string name, Vector4 value) + { + GlTF_Material mtl = GetMaterial(); + var vec_val = new GlTF_Material.VectorValue(); + vec_val.name = name; + vec_val.vector = value; + mtl.values.Add(vec_val); + AddUniform(vec_val.name, GlTF_Technique.Type.FLOAT_VEC4, GlTF_Technique.Semantic.UNKNOWN); + } + + // Exports per-material specular parameters. + public void ExportSpecularParams(float shininess, Color color) + { + var mtlName = GlTF_Material.GetNameFromObject(namedObject); + Debug.Assert(GlTF_Writer.materials.ContainsKey(mtlName)); + GlTF_Material mtl = GlTF_Writer.materials[mtlName]; + + var color_val = new GlTF_Material.ColorValue(); + color_val.name = "specular_color"; + color_val.color = color; + mtl.values.Add(color_val); + AddUniform(color_val.name, + GlTF_Technique.Type.FLOAT_VEC4, GlTF_Technique.Semantic.UNKNOWN); + + var float_val = new GlTF_Material.FloatValue(); + float_val.name = "specular_shininess"; + float_val.value = shininess; + mtl.values.Add(float_val); + AddUniform(float_val.name, + GlTF_Technique.Type.FLOAT, GlTF_Technique.Semantic.UNKNOWN); + } + + // Should be called per material. + public void ExportAmbientLight(Color color) + { + var mtlName = GlTF_Material.GetNameFromObject(namedObject); + Debug.Assert(GlTF_Writer.materials.ContainsKey(mtlName)); + GlTF_Material mtl = GlTF_Writer.materials[mtlName]; + var val = new GlTF_Material.ColorValue(); + val.name = "ambient_light_color"; + val.color = color; + mtl.values.Add(val); + AddUniform(val.name, + GlTF_Technique.Type.FLOAT_VEC4, GlTF_Technique.Semantic.UNKNOWN); + } + + // Should be called per material. + public void ExportLight(Transform tr) + { + Light unityLight = tr.GetComponent(); + Debug.Assert(unityLight != null); + Color lightColor = unityLight.color * unityLight.intensity; + lightColor.a = 1.0f; // No use for alpha with light color. + string lightName = GlTF_Writer.GetNameFromObject(tr); + switch (tr.GetComponent().type) + { + case LightType.Point: + GlTF_PointLight pl = new GlTF_PointLight(); + pl.color = new GlTF_ColorRGB(lightColor); + pl.name = lightName; + GlTF_Writer.lights.Add(pl); + break; + case LightType.Spot: + GlTF_SpotLight sl = new GlTF_SpotLight(); + sl.color = new GlTF_ColorRGB(lightColor); + sl.name = lightName; + GlTF_Writer.lights.Add(sl); + break; + case LightType.Directional: + GlTF_DirectionalLight dl = new GlTF_DirectionalLight(); + dl.color = new GlTF_ColorRGB(lightColor); + dl.name = lightName; + GlTF_Writer.lights.Add(dl); + break; + case LightType.Area: + GlTF_AmbientLight al = new GlTF_AmbientLight(); + al.color = new GlTF_ColorRGB(lightColor); + al.name = lightName; + GlTF_Writer.lights.Add(al); + break; + } - // Add light matrix. - string nodeName = GlTF_Node.GetNameFromObject(tr); - string namePrefix = GlTF_Writer.GetNameFromObject(tr); - AddUniform(namePrefix + "_matrix", - GlTF_Technique.Type.FLOAT_MAT4, GlTF_Technique.Semantic.MODELVIEW, nodeName); - // This is yucky, but necessary unless we pull name-creation out of MakeNode - GlTF_Node node = MakeNode(tr); - if (!GlTF_Writer.nodes.ContainsKey(node.name)) { - node.lightName = lightName; - GlTF_Writer.nodes.Add(node.name, node); - } + // Add light matrix. + string nodeName = GlTF_Node.GetNameFromObject(tr); + string namePrefix = GlTF_Writer.GetNameFromObject(tr); + AddUniform(namePrefix + "_matrix", + GlTF_Technique.Type.FLOAT_MAT4, GlTF_Technique.Semantic.MODELVIEW, nodeName); + // This is yucky, but necessary unless we pull name-creation out of MakeNode + GlTF_Node node = MakeNode(tr); + if (!GlTF_Writer.nodes.ContainsKey(node.name)) + { + node.lightName = lightName; + GlTF_Writer.nodes.Add(node.name, node); + } - // Add light color. - var mtlName = GlTF_Material.GetNameFromObject(namedObject); - Debug.Assert(GlTF_Writer.materials.ContainsKey(mtlName)); - GlTF_Material mtl = GlTF_Writer.materials[mtlName]; - var val = new GlTF_Material.ColorValue(); - val.name = namePrefix + "_color"; - val.color = lightColor; - mtl.values.Add(val); - AddUniform(val.name, - GlTF_Technique.Type.FLOAT_VEC4, GlTF_Technique.Semantic.UNKNOWN, nodeName); - } - - // Exports camera into glTF. - public void ExportCamera(Transform tr) { - GlTF_Node node = MakeNode(tr); - Debug.Assert(tr.GetComponent() != null); - if (tr.GetComponent().orthographic) { - GlTF_Orthographic cam; - cam = new GlTF_Orthographic(); - cam.type = "orthographic"; - cam.zfar = tr.GetComponent().farClipPlane; - cam.znear = tr.GetComponent().nearClipPlane; - cam.name = tr.name; - GlTF_Writer.cameras.Add(cam); - } else { - GlTF_Perspective cam; - cam = new GlTF_Perspective(); - cam.type = "perspective"; - cam.zfar = tr.GetComponent().farClipPlane; - cam.znear = tr.GetComponent().nearClipPlane; - cam.aspect_ratio = tr.GetComponent().aspect; - cam.yfov = tr.GetComponent().fieldOfView; - cam.name = tr.name; - GlTF_Writer.cameras.Add(cam); - } - if (!GlTF_Writer.nodes.ContainsKey(tr.name)) { - GlTF_Writer.nodes.Add(tr.name, node); - } - } - - // Exports a single-material unityMesh that has Transform tr. Unlike ExportMesh(), this API - // has no dependency on a Renderer. However, the entire unityMesh must have a single - // material and a single fragment/vertex shader pair. - public void ExportSimpleMesh(Mesh unityMesh, Transform tr, GlTF_VertexLayout vertLayout) { - - // Zandria can't currently load custom attributes via the GLTFLoader (though glTF 1.0 does - // support this), so we pack them into uv1.w, assuming uv1 starts out as a three element UV - // channel. - if (vertLayout.hasVertexIds && vertLayout.uv1 == GlTF_VertexLayout.UvElementCount.Three) { - vertLayout.uv1 = GlTF_VertexLayout.UvElementCount.Four; - } else if (vertLayout.hasVertexIds) { - Debug.LogWarningFormat("{0} has vertex IDs, but does not follow the expected layout.", - unityMesh.name); + // Add light color. + var mtlName = GlTF_Material.GetNameFromObject(namedObject); + Debug.Assert(GlTF_Writer.materials.ContainsKey(mtlName)); + GlTF_Material mtl = GlTF_Writer.materials[mtlName]; + var val = new GlTF_Material.ColorValue(); + val.name = namePrefix + "_color"; + val.color = lightColor; + mtl.values.Add(val); + AddUniform(val.name, + GlTF_Technique.Type.FLOAT_VEC4, GlTF_Technique.Semantic.UNKNOWN, nodeName); + } + + // Exports camera into glTF. + public void ExportCamera(Transform tr) + { + GlTF_Node node = MakeNode(tr); + Debug.Assert(tr.GetComponent() != null); + if (tr.GetComponent().orthographic) + { + GlTF_Orthographic cam; + cam = new GlTF_Orthographic(); + cam.type = "orthographic"; + cam.zfar = tr.GetComponent().farClipPlane; + cam.znear = tr.GetComponent().nearClipPlane; + cam.name = tr.name; + GlTF_Writer.cameras.Add(cam); + } + else + { + GlTF_Perspective cam; + cam = new GlTF_Perspective(); + cam.type = "perspective"; + cam.zfar = tr.GetComponent().farClipPlane; + cam.znear = tr.GetComponent().nearClipPlane; + cam.aspect_ratio = tr.GetComponent().aspect; + cam.yfov = tr.GetComponent().fieldOfView; + cam.name = tr.name; + GlTF_Writer.cameras.Add(cam); + } + if (!GlTF_Writer.nodes.ContainsKey(tr.name)) + { + GlTF_Writer.nodes.Add(tr.name, node); + } } - Debug.Assert(unityMesh != null); - int meshTris = unityMesh.triangles.Length / 3; - if (meshTris < 1) { - return; - } - NumTris += meshTris; - GlTF_Mesh gltfMesh = new GlTF_Mesh(); - gltfMesh.name = GlTF_Mesh.GetNameFromObject(unityMesh); - PopulateAccessors(unityMesh, vertLayout); - AddMeshDependencies(unityMesh, 0, gltfMesh); - gltfMesh.Populate(unityMesh); - GlTF_Writer.meshes.Add(gltfMesh); - - // Yuck; would be better to pull name-creation out of MakeNode - GlTF_Node node = MakeNode(tr); - if (!GlTF_Writer.nodes.ContainsKey(node.name)) { - GlTF_Writer.nodes.Add(node.name, node); - } else { - // Throw away the old one - node = GlTF_Writer.nodes[node.name]; - } - node.meshNames.Add(gltfMesh.name); - } - - // Adds material and sets up its dependent technique, program, shaders. This should be called - // after adding meshes, but before populating lights, textures, etc. - public void AddMaterialWithDependencies() { - var mtlName = GlTF_Material.GetNameFromObject(namedObject); - Debug.Assert(!GlTF_Writer.materials.ContainsKey(mtlName)); - GlTF_Material gltfMtl = new GlTF_Material(); - gltfMtl.name = mtlName; - GlTF_Writer.materials.Add(gltfMtl.name, gltfMtl); - - // Set up technique. - GlTF_Technique tech = GlTF_Writer.CreateTechnique(namedObject); - gltfMtl.instanceTechniqueName = tech.name; - GlTF_Technique.States states = null; - if (preset.techniqueStates.ContainsKey(BaseName)) { - states = preset.techniqueStates[BaseName]; - } else if (preset.techniqueStates.ContainsKey("*")) { - states = preset.techniqueStates["*"]; - } + // Exports a single-material unityMesh that has Transform tr. Unlike ExportMesh(), this API + // has no dependency on a Renderer. However, the entire unityMesh must have a single + // material and a single fragment/vertex shader pair. + public void ExportSimpleMesh(Mesh unityMesh, Transform tr, GlTF_VertexLayout vertLayout) + { - if (states == null) { - // Unless otherwise specified the preset, enable z-buffering. - states = new GlTF_Technique.States(); - states.enable = new[] { GlTF_Technique.Enable.DEPTH_TEST }.ToList(); - } - tech.states = states; - AddAllAttributes(tech); - if (preset.techniqueExtras.ContainsKey(BaseName)) { - tech.materialExtra = preset.techniqueExtras[BaseName]; - } - tech.AddDefaultUniforms(writer.RTCCenter != null); - - // Add program. - GlTF_Program program = new GlTF_Program(); - program.name = GlTF_Program.GetNameFromObject(namedObject); - tech.program = program.name; - foreach (var attr in tech.attributes) { - program.attributes.Add(attr.name); - } - GlTF_Writer.programs.Add(program); - - // Add vertex and fragment shaders. - GlTF_Shader vertShader = new GlTF_Shader(); - vertShader.name = GlTF_Shader.GetNameFromObject(namedObject, GlTF_Shader.Type.Vertex); - program.vertexShader = vertShader.name; - vertShader.type = GlTF_Shader.Type.Vertex; - vertShader.uri = preset.GetVertexShader(BaseName); - GlTF_Writer.shaders.Add(vertShader); - - GlTF_Shader fragShader = new GlTF_Shader(); - fragShader.name = GlTF_Shader.GetNameFromObject(namedObject, GlTF_Shader.Type.Fragment); - program.fragmentShader = fragShader.name; - fragShader.type = GlTF_Shader.Type.Fragment; - fragShader.uri = preset.GetFragmentShader(BaseName); - GlTF_Writer.shaders.Add(fragShader); - } - - // Adds a texture reference to the export. - // The texture parameter will be referenced by the texParam string. - // texUri is a URI to the image file - public void ExportTexture(string texParam, string texUri) { - var mtlName = GlTF_Material.GetNameFromObject(namedObject); - Debug.Assert(GlTF_Writer.materials.ContainsKey(mtlName)); - var material = GlTF_Writer.materials[mtlName]; - var val = new GlTF_Material.StringValue(); - val.name = texParam; - string texName = null; - texName = GlTF_Texture.GetNameFromObject(namedObject) +"_" + texParam; - val.value = texName; - material.values.Add(val); - if (GlTF_Writer.textures.ContainsKey(texName)) { - return; - } - GlTF_Image img = new GlTF_Image(); - img.name = GlTF_Image.GetNameFromObject(namedObject) + "_" + texParam; - img.uri = texUri; - GlTF_Writer.images.Add(img); - - var sampler = new GlTF_Sampler(); - sampler.name = sampler.ComputeName(); - sampler.magFilter = GlTF_Sampler.MagFilter.LINEAR; - sampler.minFilter = GlTF_Sampler.MinFilter.LINEAR_MIPMAP_LINEAR; - - if (!GlTF_Writer.samplers.ContainsKey(sampler.name)) { - GlTF_Writer.samplers[sampler.name] = sampler; - } + // Zandria can't currently load custom attributes via the GLTFLoader (though glTF 1.0 does + // support this), so we pack them into uv1.w, assuming uv1 starts out as a three element UV + // channel. + if (vertLayout.hasVertexIds && vertLayout.uv1 == GlTF_VertexLayout.UvElementCount.Three) + { + vertLayout.uv1 = GlTF_VertexLayout.UvElementCount.Four; + } + else if (vertLayout.hasVertexIds) + { + Debug.LogWarningFormat("{0} has vertex IDs, but does not follow the expected layout.", + unityMesh.name); + } - GlTF_Texture texture = new GlTF_Texture(); - texture.name = texName; - texture.source = img.name; - texture.samplerName = sampler.name; - - GlTF_Writer.textures.Add(texName, texture); - - // Add texture-related parameter and uniform. - AddUniform(texParam, GlTF_Technique.Type.SAMPLER_2D, GlTF_Technique.Semantic.UNKNOWN, null); - } - - // Handles low-level write operations into glTF files. - private GlTF_Writer writer; - // Output path to .gltf file. - private string outPath; - - // Accessors for the supported glTF attributes. - private GlTF_Accessor positionAccessor; - private GlTF_Accessor normalAccessor; - private GlTF_Accessor colorAccessor; - private GlTF_Accessor tangentAccessor; - private GlTF_Accessor vertexIdAccessor; - private GlTF_Accessor uv0Accessor; - private GlTF_Accessor uv1Accessor; - private GlTF_Accessor uv2Accessor; - private GlTF_Accessor uv3Accessor; - - // Returns full path to assets. - private static string GetAssetFullPath(string path) { - if (path != null) { - path = path.Remove(0, "Assets".Length); - path = Application.dataPath + path; - } - return path; - } - - // Returns Unity Renderer, if any, given Transform. - private static Renderer GetRenderer(Transform tr) { - Debug.Assert(tr != null); - Renderer mr = tr.GetComponent(); - if (mr == null) { - mr = tr.GetComponent(); - } - return mr; - } - - // Returns a (Unity) Mesh, if any, given Transform tr. Note that tr must also have a - // Renderer. Otherwise, returns null. - private static Mesh GetMesh(Transform tr) { - Debug.Assert(tr != null); - var mr = GetRenderer(tr); - Mesh mesh = null; - if (mr != null) { - var t = mr.GetType(); - if (t == typeof(MeshRenderer)) { - MeshFilter mf = tr.GetComponent(); - if (mf == null) { - return null; - } - mesh = mf.sharedMesh; - } else if (t == typeof(SkinnedMeshRenderer)) { - SkinnedMeshRenderer smr = mr as SkinnedMeshRenderer; - mesh = smr.sharedMesh; - } - } - return mesh; - } - - private GlTF_BufferView GetBufferView(GlTF_Accessor accessor) { - switch (accessor.type) { - case GlTF_Accessor.Type.SCALAR: - return GlTF_BufferView.floatBufferView; - case GlTF_Accessor.Type.VEC2: - return GlTF_BufferView.vec2BufferView; - case GlTF_Accessor.Type.VEC3: - return GlTF_BufferView.vec3BufferView; - case GlTF_Accessor.Type.VEC4: - return GlTF_BufferView.vec4BufferView; - default: - throw new ArgumentException(string.Format("Unsupported accessor type {0}", accessor.type)); - } - } - - // Populates the accessor memberes based on the given mesh. - private void PopulateAccessors(Mesh unityMesh, GlTF_VertexLayout vertLayout) { - positionAccessor = - new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "position"), - GlTF_Accessor.Type.VEC3, GlTF_Accessor.ComponentType.FLOAT); - positionAccessor.bufferView = GetBufferView(positionAccessor); - GlTF_Writer.accessors.Add(positionAccessor); - - normalAccessor = null; - if (vertLayout.hasNormals) { - normalAccessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "normal"), - GlTF_Accessor.Type.VEC3, GlTF_Accessor.ComponentType.FLOAT); - normalAccessor.bufferView = GetBufferView(normalAccessor); - GlTF_Writer.accessors.Add(normalAccessor); - } + Debug.Assert(unityMesh != null); + int meshTris = unityMesh.triangles.Length / 3; + if (meshTris < 1) + { + return; + } + NumTris += meshTris; + GlTF_Mesh gltfMesh = new GlTF_Mesh(); + gltfMesh.name = GlTF_Mesh.GetNameFromObject(unityMesh); + PopulateAccessors(unityMesh, vertLayout); + AddMeshDependencies(unityMesh, 0, gltfMesh); + gltfMesh.Populate(unityMesh); + GlTF_Writer.meshes.Add(gltfMesh); + + // Yuck; would be better to pull name-creation out of MakeNode + GlTF_Node node = MakeNode(tr); + if (!GlTF_Writer.nodes.ContainsKey(node.name)) + { + GlTF_Writer.nodes.Add(node.name, node); + } + else + { + // Throw away the old one + node = GlTF_Writer.nodes[node.name]; + } + node.meshNames.Add(gltfMesh.name); + } + + // Adds material and sets up its dependent technique, program, shaders. This should be called + // after adding meshes, but before populating lights, textures, etc. + public void AddMaterialWithDependencies() + { + var mtlName = GlTF_Material.GetNameFromObject(namedObject); + Debug.Assert(!GlTF_Writer.materials.ContainsKey(mtlName)); + GlTF_Material gltfMtl = new GlTF_Material(); + gltfMtl.name = mtlName; + GlTF_Writer.materials.Add(gltfMtl.name, gltfMtl); + + // Set up technique. + GlTF_Technique tech = GlTF_Writer.CreateTechnique(namedObject); + gltfMtl.instanceTechniqueName = tech.name; + GlTF_Technique.States states = null; + if (preset.techniqueStates.ContainsKey(BaseName)) + { + states = preset.techniqueStates[BaseName]; + } + else if (preset.techniqueStates.ContainsKey("*")) + { + states = preset.techniqueStates["*"]; + } - colorAccessor = null; - if (vertLayout.hasColors) { - colorAccessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "color"), - GlTF_Accessor.Type.VEC4, GlTF_Accessor.ComponentType.FLOAT); - colorAccessor.bufferView = GetBufferView(colorAccessor); - GlTF_Writer.accessors.Add(colorAccessor); - } + if (states == null) + { + // Unless otherwise specified the preset, enable z-buffering. + states = new GlTF_Technique.States(); + states.enable = new[] { GlTF_Technique.Enable.DEPTH_TEST }.ToList(); + } + tech.states = states; + AddAllAttributes(tech); + if (preset.techniqueExtras.ContainsKey(BaseName)) + { + tech.materialExtra = preset.techniqueExtras[BaseName]; + } + tech.AddDefaultUniforms(writer.RTCCenter != null); + + // Add program. + GlTF_Program program = new GlTF_Program(); + program.name = GlTF_Program.GetNameFromObject(namedObject); + tech.program = program.name; + foreach (var attr in tech.attributes) + { + program.attributes.Add(attr.name); + } + GlTF_Writer.programs.Add(program); + + // Add vertex and fragment shaders. + GlTF_Shader vertShader = new GlTF_Shader(); + vertShader.name = GlTF_Shader.GetNameFromObject(namedObject, GlTF_Shader.Type.Vertex); + program.vertexShader = vertShader.name; + vertShader.type = GlTF_Shader.Type.Vertex; + vertShader.uri = preset.GetVertexShader(BaseName); + GlTF_Writer.shaders.Add(vertShader); + + GlTF_Shader fragShader = new GlTF_Shader(); + fragShader.name = GlTF_Shader.GetNameFromObject(namedObject, GlTF_Shader.Type.Fragment); + program.fragmentShader = fragShader.name; + fragShader.type = GlTF_Shader.Type.Fragment; + fragShader.uri = preset.GetFragmentShader(BaseName); + GlTF_Writer.shaders.Add(fragShader); + } + + // Adds a texture reference to the export. + // The texture parameter will be referenced by the texParam string. + // texUri is a URI to the image file + public void ExportTexture(string texParam, string texUri) + { + var mtlName = GlTF_Material.GetNameFromObject(namedObject); + Debug.Assert(GlTF_Writer.materials.ContainsKey(mtlName)); + var material = GlTF_Writer.materials[mtlName]; + var val = new GlTF_Material.StringValue(); + val.name = texParam; + string texName = null; + texName = GlTF_Texture.GetNameFromObject(namedObject) + "_" + texParam; + val.value = texName; + material.values.Add(val); + if (GlTF_Writer.textures.ContainsKey(texName)) + { + return; + } + GlTF_Image img = new GlTF_Image(); + img.name = GlTF_Image.GetNameFromObject(namedObject) + "_" + texParam; + img.uri = texUri; + GlTF_Writer.images.Add(img); + + var sampler = new GlTF_Sampler(); + sampler.name = sampler.ComputeName(); + sampler.magFilter = GlTF_Sampler.MagFilter.LINEAR; + sampler.minFilter = GlTF_Sampler.MinFilter.LINEAR_MIPMAP_LINEAR; + + if (!GlTF_Writer.samplers.ContainsKey(sampler.name)) + { + GlTF_Writer.samplers[sampler.name] = sampler; + } - tangentAccessor = null; - if (vertLayout.hasTangents) { - tangentAccessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "tangent"), - GlTF_Accessor.Type.VEC4, GlTF_Accessor.ComponentType.FLOAT); - tangentAccessor.bufferView = GetBufferView(tangentAccessor); - GlTF_Writer.accessors.Add(tangentAccessor); + GlTF_Texture texture = new GlTF_Texture(); + texture.name = texName; + texture.source = img.name; + texture.samplerName = sampler.name; + + GlTF_Writer.textures.Add(texName, texture); + + // Add texture-related parameter and uniform. + AddUniform(texParam, GlTF_Technique.Type.SAMPLER_2D, GlTF_Technique.Semantic.UNKNOWN, null); + } + + // Handles low-level write operations into glTF files. + private GlTF_Writer writer; + // Output path to .gltf file. + private string outPath; + + // Accessors for the supported glTF attributes. + private GlTF_Accessor positionAccessor; + private GlTF_Accessor normalAccessor; + private GlTF_Accessor colorAccessor; + private GlTF_Accessor tangentAccessor; + private GlTF_Accessor vertexIdAccessor; + private GlTF_Accessor uv0Accessor; + private GlTF_Accessor uv1Accessor; + private GlTF_Accessor uv2Accessor; + private GlTF_Accessor uv3Accessor; + + // Returns full path to assets. + private static string GetAssetFullPath(string path) + { + if (path != null) + { + path = path.Remove(0, "Assets".Length); + path = Application.dataPath + path; + } + return path; } - vertexIdAccessor = null; - if (vertLayout.hasVertexIds) { - vertexIdAccessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "vertexId"), - GlTF_Accessor.Type.SCALAR, GlTF_Accessor.ComponentType.FLOAT); - vertexIdAccessor.bufferView = GetBufferView(vertexIdAccessor); - GlTF_Writer.accessors.Add(vertexIdAccessor); + // Returns Unity Renderer, if any, given Transform. + private static Renderer GetRenderer(Transform tr) + { + Debug.Assert(tr != null); + Renderer mr = tr.GetComponent(); + if (mr == null) + { + mr = tr.GetComponent(); + } + return mr; + } + + // Returns a (Unity) Mesh, if any, given Transform tr. Note that tr must also have a + // Renderer. Otherwise, returns null. + private static Mesh GetMesh(Transform tr) + { + Debug.Assert(tr != null); + var mr = GetRenderer(tr); + Mesh mesh = null; + if (mr != null) + { + var t = mr.GetType(); + if (t == typeof(MeshRenderer)) + { + MeshFilter mf = tr.GetComponent(); + if (mf == null) + { + return null; + } + mesh = mf.sharedMesh; + } + else if (t == typeof(SkinnedMeshRenderer)) + { + SkinnedMeshRenderer smr = mr as SkinnedMeshRenderer; + mesh = smr.sharedMesh; + } + } + return mesh; + } + + private GlTF_BufferView GetBufferView(GlTF_Accessor accessor) + { + switch (accessor.type) + { + case GlTF_Accessor.Type.SCALAR: + return GlTF_BufferView.floatBufferView; + case GlTF_Accessor.Type.VEC2: + return GlTF_BufferView.vec2BufferView; + case GlTF_Accessor.Type.VEC3: + return GlTF_BufferView.vec3BufferView; + case GlTF_Accessor.Type.VEC4: + return GlTF_BufferView.vec4BufferView; + default: + throw new ArgumentException(string.Format("Unsupported accessor type {0}", accessor.type)); + } } - uv0Accessor = null; - if (vertLayout.uv0 != GlTF_VertexLayout.UvElementCount.None) { - uv0Accessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "uv0"), - GlTF_VertexLayout.GetUvType(vertLayout.uv0), GlTF_Accessor.ComponentType.FLOAT); - uv0Accessor.bufferView = GetBufferView(uv0Accessor); - GlTF_Writer.accessors.Add(uv0Accessor); - } + // Populates the accessor memberes based on the given mesh. + private void PopulateAccessors(Mesh unityMesh, GlTF_VertexLayout vertLayout) + { + positionAccessor = + new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "position"), + GlTF_Accessor.Type.VEC3, GlTF_Accessor.ComponentType.FLOAT); + positionAccessor.bufferView = GetBufferView(positionAccessor); + GlTF_Writer.accessors.Add(positionAccessor); + + normalAccessor = null; + if (vertLayout.hasNormals) + { + normalAccessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "normal"), + GlTF_Accessor.Type.VEC3, GlTF_Accessor.ComponentType.FLOAT); + normalAccessor.bufferView = GetBufferView(normalAccessor); + GlTF_Writer.accessors.Add(normalAccessor); + } - uv1Accessor = null; - if (vertLayout.uv1 != GlTF_VertexLayout.UvElementCount.None) { - uv1Accessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "uv1"), - GlTF_VertexLayout.GetUvType(vertLayout.uv1), GlTF_Accessor.ComponentType.FLOAT); - uv1Accessor.bufferView = GetBufferView(uv1Accessor); - GlTF_Writer.accessors.Add(uv1Accessor); - } + colorAccessor = null; + if (vertLayout.hasColors) + { + colorAccessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "color"), + GlTF_Accessor.Type.VEC4, GlTF_Accessor.ComponentType.FLOAT); + colorAccessor.bufferView = GetBufferView(colorAccessor); + GlTF_Writer.accessors.Add(colorAccessor); + } - uv2Accessor = null; - if (vertLayout.uv2 != GlTF_VertexLayout.UvElementCount.None) { - uv2Accessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "uv2"), - GlTF_VertexLayout.GetUvType(vertLayout.uv2), GlTF_Accessor.ComponentType.FLOAT); - uv2Accessor.bufferView = GetBufferView(uv2Accessor); - GlTF_Writer.accessors.Add(uv2Accessor); - } + tangentAccessor = null; + if (vertLayout.hasTangents) + { + tangentAccessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "tangent"), + GlTF_Accessor.Type.VEC4, GlTF_Accessor.ComponentType.FLOAT); + tangentAccessor.bufferView = GetBufferView(tangentAccessor); + GlTF_Writer.accessors.Add(tangentAccessor); + } - uv3Accessor = null; - if (vertLayout.uv3 != GlTF_VertexLayout.UvElementCount.None) { - uv3Accessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "uv3"), - GlTF_VertexLayout.GetUvType(vertLayout.uv3), GlTF_Accessor.ComponentType.FLOAT); - uv3Accessor.bufferView = GetBufferView(uv3Accessor); - GlTF_Writer.accessors.Add(uv3Accessor); - } - } - - // Adds a glTF attribute, as described by name, type, and semantic, to the given technique tech. - private static void AddAttribute(string name, GlTF_Technique.Type type, - GlTF_Technique.Semantic semantic, GlTF_Technique tech) { - GlTF_Technique.Parameter tParam = new GlTF_Technique.Parameter(); - tParam.name = name; - tParam.type = type; - tParam.semantic = semantic; - tech.parameters.Add(tParam); - GlTF_Technique.Attribute tAttr = new GlTF_Technique.Attribute(); - tAttr.name = "a_" + name; - tAttr.param = tParam.name; - tech.attributes.Add(tAttr); - } - - // Adds a glTF uniform, as described by name, type, and semantic, to the given technique tech. If - // node is non-null, that is also included (e.g. for lights). - private void AddUniform(string name, GlTF_Technique.Type type, - GlTF_Technique.Semantic semantic, string node = null) { - //var techName = GlTF_Technique.GetNameFromObject(namedObject); - var tech = GlTF_Writer.GetTechnique(namedObject); - GlTF_Technique.Parameter tParam = new GlTF_Technique.Parameter(); - tParam.name = name; - tParam.type = type; - tParam.semantic = semantic; - if (node != null) { - tParam.node = node; - } - tech.parameters.Add(tParam); - GlTF_Technique.Uniform tUniform = new GlTF_Technique.Uniform(); - tUniform.name = "u_" + name; - tUniform.param = tParam.name; - tech.uniforms.Add(tUniform); - } - - GlTF_Technique.Type GetTechniqueType(GlTF_Accessor accessor) { - switch (accessor.type) { - case GlTF_Accessor.Type.SCALAR: - return GlTF_Technique.Type.FLOAT; - case GlTF_Accessor.Type.VEC2: - return GlTF_Technique.Type.FLOAT_VEC2; - case GlTF_Accessor.Type.VEC3: - return GlTF_Technique.Type.FLOAT_VEC3; - case GlTF_Accessor.Type.VEC4: - return GlTF_Technique.Type.FLOAT_VEC4; - default: - throw new ArgumentException(string.Format("Unsupported accessor type {0}", accessor.type)); - } - } - - // Updates glTF technique tech by adding all relevant attributes. - private void AddAllAttributes(GlTF_Technique tech) { - AddAttribute("position", GetTechniqueType(positionAccessor), - GlTF_Technique.Semantic.POSITION, tech); - if (normalAccessor != null) { - AddAttribute("normal", GetTechniqueType(normalAccessor), - GlTF_Technique.Semantic.NORMAL, tech); - } - if (colorAccessor != null) { - AddAttribute("color", GetTechniqueType(colorAccessor), - GlTF_Technique.Semantic.COLOR, tech); - } - if (tangentAccessor != null) { - AddAttribute("tangent", GetTechniqueType(tangentAccessor), - GlTF_Technique.Semantic.TANGENT, tech); - } - if (vertexIdAccessor != null) { - AddAttribute("vertexId", GetTechniqueType(vertexIdAccessor), - GlTF_Technique.Semantic.UNKNOWN, tech); - } - if (uv0Accessor != null) { - AddAttribute("texcoord0", GetTechniqueType(uv0Accessor), - GlTF_Technique.Semantic.TEXCOORD_0, tech); - } - if (uv1Accessor != null) { - AddAttribute("texcoord1", GetTechniqueType(uv1Accessor), - GlTF_Technique.Semantic.TEXCOORD_1, tech); + vertexIdAccessor = null; + if (vertLayout.hasVertexIds) + { + vertexIdAccessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "vertexId"), + GlTF_Accessor.Type.SCALAR, GlTF_Accessor.ComponentType.FLOAT); + vertexIdAccessor.bufferView = GetBufferView(vertexIdAccessor); + GlTF_Writer.accessors.Add(vertexIdAccessor); + } + + uv0Accessor = null; + if (vertLayout.uv0 != GlTF_VertexLayout.UvElementCount.None) + { + uv0Accessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "uv0"), + GlTF_VertexLayout.GetUvType(vertLayout.uv0), GlTF_Accessor.ComponentType.FLOAT); + uv0Accessor.bufferView = GetBufferView(uv0Accessor); + GlTF_Writer.accessors.Add(uv0Accessor); + } + + uv1Accessor = null; + if (vertLayout.uv1 != GlTF_VertexLayout.UvElementCount.None) + { + uv1Accessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "uv1"), + GlTF_VertexLayout.GetUvType(vertLayout.uv1), GlTF_Accessor.ComponentType.FLOAT); + uv1Accessor.bufferView = GetBufferView(uv1Accessor); + GlTF_Writer.accessors.Add(uv1Accessor); + } + + uv2Accessor = null; + if (vertLayout.uv2 != GlTF_VertexLayout.UvElementCount.None) + { + uv2Accessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "uv2"), + GlTF_VertexLayout.GetUvType(vertLayout.uv2), GlTF_Accessor.ComponentType.FLOAT); + uv2Accessor.bufferView = GetBufferView(uv2Accessor); + GlTF_Writer.accessors.Add(uv2Accessor); + } + + uv3Accessor = null; + if (vertLayout.uv3 != GlTF_VertexLayout.UvElementCount.None) + { + uv3Accessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, "uv3"), + GlTF_VertexLayout.GetUvType(vertLayout.uv3), GlTF_Accessor.ComponentType.FLOAT); + uv3Accessor.bufferView = GetBufferView(uv3Accessor); + GlTF_Writer.accessors.Add(uv3Accessor); + } } - if (uv2Accessor != null) { - AddAttribute("texcoord2", GetTechniqueType(uv2Accessor), - GlTF_Technique.Semantic.TEXCOORD_2, tech); + + // Adds a glTF attribute, as described by name, type, and semantic, to the given technique tech. + private static void AddAttribute(string name, GlTF_Technique.Type type, + GlTF_Technique.Semantic semantic, GlTF_Technique tech) + { + GlTF_Technique.Parameter tParam = new GlTF_Technique.Parameter(); + tParam.name = name; + tParam.type = type; + tParam.semantic = semantic; + tech.parameters.Add(tParam); + GlTF_Technique.Attribute tAttr = new GlTF_Technique.Attribute(); + tAttr.name = "a_" + name; + tAttr.param = tParam.name; + tech.attributes.Add(tAttr); + } + + // Adds a glTF uniform, as described by name, type, and semantic, to the given technique tech. If + // node is non-null, that is also included (e.g. for lights). + private void AddUniform(string name, GlTF_Technique.Type type, + GlTF_Technique.Semantic semantic, string node = null) + { + //var techName = GlTF_Technique.GetNameFromObject(namedObject); + var tech = GlTF_Writer.GetTechnique(namedObject); + GlTF_Technique.Parameter tParam = new GlTF_Technique.Parameter(); + tParam.name = name; + tParam.type = type; + tParam.semantic = semantic; + if (node != null) + { + tParam.node = node; + } + tech.parameters.Add(tParam); + GlTF_Technique.Uniform tUniform = new GlTF_Technique.Uniform(); + tUniform.name = "u_" + name; + tUniform.param = tParam.name; + tech.uniforms.Add(tUniform); + } + + GlTF_Technique.Type GetTechniqueType(GlTF_Accessor accessor) + { + switch (accessor.type) + { + case GlTF_Accessor.Type.SCALAR: + return GlTF_Technique.Type.FLOAT; + case GlTF_Accessor.Type.VEC2: + return GlTF_Technique.Type.FLOAT_VEC2; + case GlTF_Accessor.Type.VEC3: + return GlTF_Technique.Type.FLOAT_VEC3; + case GlTF_Accessor.Type.VEC4: + return GlTF_Technique.Type.FLOAT_VEC4; + default: + throw new ArgumentException(string.Format("Unsupported accessor type {0}", accessor.type)); + } } - if (uv3Accessor != null) { - AddAttribute("texcoord3", GetTechniqueType(uv3Accessor), - GlTF_Technique.Semantic.TEXCOORD_3, tech); + + // Updates glTF technique tech by adding all relevant attributes. + private void AddAllAttributes(GlTF_Technique tech) + { + AddAttribute("position", GetTechniqueType(positionAccessor), + GlTF_Technique.Semantic.POSITION, tech); + if (normalAccessor != null) + { + AddAttribute("normal", GetTechniqueType(normalAccessor), + GlTF_Technique.Semantic.NORMAL, tech); + } + if (colorAccessor != null) + { + AddAttribute("color", GetTechniqueType(colorAccessor), + GlTF_Technique.Semantic.COLOR, tech); + } + if (tangentAccessor != null) + { + AddAttribute("tangent", GetTechniqueType(tangentAccessor), + GlTF_Technique.Semantic.TANGENT, tech); + } + if (vertexIdAccessor != null) + { + AddAttribute("vertexId", GetTechniqueType(vertexIdAccessor), + GlTF_Technique.Semantic.UNKNOWN, tech); + } + if (uv0Accessor != null) + { + AddAttribute("texcoord0", GetTechniqueType(uv0Accessor), + GlTF_Technique.Semantic.TEXCOORD_0, tech); + } + if (uv1Accessor != null) + { + AddAttribute("texcoord1", GetTechniqueType(uv1Accessor), + GlTF_Technique.Semantic.TEXCOORD_1, tech); + } + if (uv2Accessor != null) + { + AddAttribute("texcoord2", GetTechniqueType(uv2Accessor), + GlTF_Technique.Semantic.TEXCOORD_2, tech); + } + if (uv3Accessor != null) + { + AddAttribute("texcoord3", GetTechniqueType(uv3Accessor), + GlTF_Technique.Semantic.TEXCOORD_3, tech); + } } - } - - // Attaches glTF attributes to the given glTF primitive. - private void AttachAttributes(GlTF_Primitive primitive) { - GlTF_Attributes attributes = new GlTF_Attributes(); - attributes.positionAccessor = positionAccessor; - attributes.normalAccessor = normalAccessor; - attributes.colorAccessor = colorAccessor; - attributes.tangentAccessor = tangentAccessor; - attributes.vertexIdAccessor = vertexIdAccessor; - attributes.texCoord0Accessor = uv0Accessor; - attributes.texCoord1Accessor = uv1Accessor; - attributes.texCoord2Accessor = uv2Accessor; - attributes.texCoord3Accessor = uv3Accessor; - primitive.attributes = attributes; - } - - // Adds to gltfMesh the glTF dependencies (primitive, material, technique, program, shaders) - // required by unityMesh, using the index value and BaseName for naming the various glTF - // components. This does not add any geometry from the mesh (that's done separately using - // GlTF_Mesh.Populate()). - private void AddMeshDependencies(Mesh unityMesh, int index, GlTF_Mesh gltfMesh) { - GlTF_Primitive primitive = new GlTF_Primitive(); - primitive.name = GlTF_Primitive.GetNameFromObject(unityMesh, index); - primitive.index = index; - AttachAttributes(primitive); - - GlTF_Accessor indexAccessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, - "indices_" + index), GlTF_Accessor.Type.SCALAR, GlTF_Accessor.ComponentType.USHORT); - indexAccessor.bufferView = GlTF_Writer.ushortBufferView; - GlTF_Writer.accessors.Add(indexAccessor); - primitive.indices = indexAccessor; - gltfMesh.primitives.Add(primitive); - - var mtlName = GlTF_Material.GetNameFromObject(namedObject); - primitive.materialName = mtlName; - } - - // Creates new node based on given transform. - private GlTF_Node MakeNode(Transform tr) { - Debug.Assert(tr != null); - GlTF_Node node = new GlTF_Node(); - - var mat = CurrentTransformFilter.Invoke(tr); - - // Convert from DirectX/LeftHanded/(fwd=z,rt=x,up=y) -> OpenGL/RightHanded/(fwd=-z,rt=x,up=y) - // Matrix4x4 basis = Matrix4x4.zero; - // basis[0, 0] = 1; - // basis[1, 1] = 1; - // basis[2, 2] = -1; - // basis[3, 3] = 1; - // mat = basis * mat * basis.inverse; - // - // The change of basis above is equivalent to the following: - mat[2, 0] *= -1; - mat[2, 1] *= -1; - mat[0, 2] *= -1; - mat[1, 2] *= -1; - - node.matrix = new GlTF_Matrix(mat); - node.name = GlTF_Node.GetNameFromObject(tr); - return node; - } - - private GameObject namedObject = new GameObject(); + + // Attaches glTF attributes to the given glTF primitive. + private void AttachAttributes(GlTF_Primitive primitive) + { + GlTF_Attributes attributes = new GlTF_Attributes(); + attributes.positionAccessor = positionAccessor; + attributes.normalAccessor = normalAccessor; + attributes.colorAccessor = colorAccessor; + attributes.tangentAccessor = tangentAccessor; + attributes.vertexIdAccessor = vertexIdAccessor; + attributes.texCoord0Accessor = uv0Accessor; + attributes.texCoord1Accessor = uv1Accessor; + attributes.texCoord2Accessor = uv2Accessor; + attributes.texCoord3Accessor = uv3Accessor; + primitive.attributes = attributes; + } + + // Adds to gltfMesh the glTF dependencies (primitive, material, technique, program, shaders) + // required by unityMesh, using the index value and BaseName for naming the various glTF + // components. This does not add any geometry from the mesh (that's done separately using + // GlTF_Mesh.Populate()). + private void AddMeshDependencies(Mesh unityMesh, int index, GlTF_Mesh gltfMesh) + { + GlTF_Primitive primitive = new GlTF_Primitive(); + primitive.name = GlTF_Primitive.GetNameFromObject(unityMesh, index); + primitive.index = index; + AttachAttributes(primitive); + + GlTF_Accessor indexAccessor = new GlTF_Accessor(GlTF_Accessor.GetNameFromObject(unityMesh, + "indices_" + index), GlTF_Accessor.Type.SCALAR, GlTF_Accessor.ComponentType.USHORT); + indexAccessor.bufferView = GlTF_Writer.ushortBufferView; + GlTF_Writer.accessors.Add(indexAccessor); + primitive.indices = indexAccessor; + gltfMesh.primitives.Add(primitive); + + var mtlName = GlTF_Material.GetNameFromObject(namedObject); + primitive.materialName = mtlName; + } + + // Creates new node based on given transform. + private GlTF_Node MakeNode(Transform tr) + { + Debug.Assert(tr != null); + GlTF_Node node = new GlTF_Node(); + + var mat = CurrentTransformFilter.Invoke(tr); + + // Convert from DirectX/LeftHanded/(fwd=z,rt=x,up=y) -> OpenGL/RightHanded/(fwd=-z,rt=x,up=y) + // Matrix4x4 basis = Matrix4x4.zero; + // basis[0, 0] = 1; + // basis[1, 1] = 1; + // basis[2, 2] = -1; + // basis[3, 3] = 1; + // mat = basis * mat * basis.inverse; + // + // The change of basis above is equivalent to the following: + mat[2, 0] *= -1; + mat[2, 1] *= -1; + mat[0, 2] *= -1; + mat[1, 2] *= -1; + + node.matrix = new GlTF_Matrix(mat); + node.name = GlTF_Node.GetNameFromObject(tr); + return node; + } + + private GameObject namedObject = new GameObject(); } diff --git a/Assets/Scripts/ThirdParty/GlTF/google/GlTF_VertexLayout.cs b/Assets/Scripts/ThirdParty/GlTF/google/GlTF_VertexLayout.cs index b6135251..f18d9ffe 100644 --- a/Assets/Scripts/ThirdParty/GlTF/google/GlTF_VertexLayout.cs +++ b/Assets/Scripts/ThirdParty/GlTF/google/GlTF_VertexLayout.cs @@ -2,42 +2,46 @@ // A descriptive structure that allows Unity mesh vertex attributes to be inspected. // Unfortunately, Unity provides no such interface. -public struct GlTF_VertexLayout { +public struct GlTF_VertexLayout +{ - // The number of elements per-value in a UV channel. - // e.g. a typical Vec2 UV is UvElementCount.Two. - public enum UvElementCount { - None, - One, - Two, - Three, - Four - } + // The number of elements per-value in a UV channel. + // e.g. a typical Vec2 UV is UvElementCount.Two. + public enum UvElementCount + { + None, + One, + Two, + Three, + Four + } - public UvElementCount uv0; - public UvElementCount uv1; - public UvElementCount uv2; - public UvElementCount uv3; + public UvElementCount uv0; + public UvElementCount uv1; + public UvElementCount uv2; + public UvElementCount uv3; - public bool hasNormals; - public bool hasTangents; - public bool hasColors; - public bool hasVertexIds; + public bool hasNormals; + public bool hasTangents; + public bool hasColors; + public bool hasVertexIds; - // Convert a UvElementCount to an Accessor Type. - public static GlTF_Accessor.Type GetUvType(UvElementCount elements) { - switch (elements) { - case UvElementCount.One: - return GlTF_Accessor.Type.SCALAR; - case UvElementCount.Two: - return GlTF_Accessor.Type.VEC2; - case UvElementCount.Three: - return GlTF_Accessor.Type.VEC3; - case UvElementCount.Four: - return GlTF_Accessor.Type.VEC4; - default: - throw new System.ArgumentException( - string.Format("Invalid elements value {0}", (int)elements)); + // Convert a UvElementCount to an Accessor Type. + public static GlTF_Accessor.Type GetUvType(UvElementCount elements) + { + switch (elements) + { + case UvElementCount.One: + return GlTF_Accessor.Type.SCALAR; + case UvElementCount.Two: + return GlTF_Accessor.Type.VEC2; + case UvElementCount.Three: + return GlTF_Accessor.Type.VEC3; + case UvElementCount.Four: + return GlTF_Accessor.Type.VEC4; + default: + throw new System.ArgumentException( + string.Format("Invalid elements value {0}", (int)elements)); + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/ThirdParty/OVR/Editor/OVRBuild.cs b/Assets/Scripts/ThirdParty/OVR/Editor/OVRBuild.cs index 6e51d864..590dec79 100644 --- a/Assets/Scripts/ThirdParty/OVR/Editor/OVRBuild.cs +++ b/Assets/Scripts/ThirdParty/OVR/Editor/OVRBuild.cs @@ -27,44 +27,44 @@ limitations under the License. /// partial class OculusBuildApp { - static void SetPCTarget() - { - if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.StandaloneWindows) - { + static void SetPCTarget() + { + if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.StandaloneWindows) + { #if UNITY_5_6_OR_NEWER EditorUserBuildSettings.SwitchActiveBuildTarget (BuildTargetGroup.Standalone, BuildTarget.StandaloneWindows); #else - EditorUserBuildSettings.SwitchActiveBuildTarget (BuildTarget.StandaloneWindows); + EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTarget.StandaloneWindows); #endif - } + } #if UNITY_5_5_OR_NEWER UnityEditorInternal.VR.VREditor.SetVREnabledOnTargetGroup(BuildTargetGroup.Standalone, true); #elif UNITY_5_4_OR_NEWER UnityEditorInternal.VR.VREditor.SetVREnabled(BuildTargetGroup.Standalone, true); #endif - PlayerSettings.virtualRealitySupported = true; - AssetDatabase.SaveAssets(); - } + PlayerSettings.virtualRealitySupported = true; + AssetDatabase.SaveAssets(); + } - static void SetAndroidTarget() - { - EditorUserBuildSettings.androidBuildSubtarget = MobileTextureSubtarget.ASTC; + static void SetAndroidTarget() + { + EditorUserBuildSettings.androidBuildSubtarget = MobileTextureSubtarget.ASTC; - if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.Android) - { + if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.Android) + { #if UNITY_5_6_OR_NEWER EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Android, BuildTarget.Android); #else - EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTarget.Android); + EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTarget.Android); #endif - } + } #if UNITY_5_5_OR_NEWER UnityEditorInternal.VR.VREditor.SetVREnabledOnTargetGroup(BuildTargetGroup.Standalone, true); #elif UNITY_5_4_OR_NEWER UnityEditorInternal.VR.VREditor.SetVREnabled(BuildTargetGroup.Android, true); #endif - PlayerSettings.virtualRealitySupported = true; - AssetDatabase.SaveAssets(); - } + PlayerSettings.virtualRealitySupported = true; + AssetDatabase.SaveAssets(); + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Editor/OVRManifestPreprocessor.cs b/Assets/Scripts/ThirdParty/OVR/Editor/OVRManifestPreprocessor.cs index 994c91df..453de672 100644 --- a/Assets/Scripts/ThirdParty/OVR/Editor/OVRManifestPreprocessor.cs +++ b/Assets/Scripts/ThirdParty/OVR/Editor/OVRManifestPreprocessor.cs @@ -2,34 +2,34 @@ using UnityEditor; using System.IO; -class OVRManifestPreprocessor +class OVRManifestPreprocessor { - [MenuItem("Tools/Oculus/Create store-compatible AndroidManifest.xml", false, 100000)] - static void GenerateManifestForSubmission() - { - string srcFile = Application.dataPath + "/OVR/Editor/AndroidManifest.OVRSubmission.xml"; + [MenuItem("Tools/Oculus/Create store-compatible AndroidManifest.xml", false, 100000)] + static void GenerateManifestForSubmission() + { + string srcFile = Application.dataPath + "/OVR/Editor/AndroidManifest.OVRSubmission.xml"; - if (!File.Exists(srcFile)) - { - Debug.LogError ("Cannot find Android manifest template for submission." + - " Please delete the OVR folder and reimport the Oculus Utilities."); - return; - } + if (!File.Exists(srcFile)) + { + Debug.LogError("Cannot find Android manifest template for submission." + + " Please delete the OVR folder and reimport the Oculus Utilities."); + return; + } - string manifestFolder = Application.dataPath + "/Plugins/Android"; + string manifestFolder = Application.dataPath + "/Plugins/Android"; - if (!Directory.Exists(manifestFolder)) - Directory.CreateDirectory(manifestFolder); + if (!Directory.Exists(manifestFolder)) + Directory.CreateDirectory(manifestFolder); - string dstFile = manifestFolder + "/AndroidManifest.xml"; + string dstFile = manifestFolder + "/AndroidManifest.xml"; - if (File.Exists(dstFile)) - { - Debug.LogWarning("Cannot create Oculus store-compatible manifest due to conflicting file: \"" - + dstFile + "\". Please remove it and try again."); - return; - } + if (File.Exists(dstFile)) + { + Debug.LogWarning("Cannot create Oculus store-compatible manifest due to conflicting file: \"" + + dstFile + "\". Please remove it and try again."); + return; + } - File.Copy(srcFile, dstFile); + File.Copy(srcFile, dstFile); } } diff --git a/Assets/Scripts/ThirdParty/OVR/Editor/OVRMoonlightLoader.cs b/Assets/Scripts/ThirdParty/OVR/Editor/OVRMoonlightLoader.cs index f77828e4..3b81cbf1 100644 --- a/Assets/Scripts/ThirdParty/OVR/Editor/OVRMoonlightLoader.cs +++ b/Assets/Scripts/ThirdParty/OVR/Editor/OVRMoonlightLoader.cs @@ -29,71 +29,71 @@ limitations under the License. class OVRMoonlightLoader { static OVRMoonlightLoader() - { - EnforceInputManagerBindings(); + { + EnforceInputManagerBindings(); #if UNITY_ANDROID EditorApplication.delayCall += EnforceOSIG; #endif - EditorApplication.update += EnforceBundleId; - EditorApplication.update += EnforceVRSupport; - EditorApplication.update += EnforceInstallLocation; - - if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.Android) - return; - - if (PlayerSettings.defaultInterfaceOrientation != UIOrientation.LandscapeLeft) - { - Debug.Log("MoonlightLoader: Setting orientation to Landscape Left"); - // Default screen orientation must be set to landscape left. - PlayerSettings.defaultInterfaceOrientation = UIOrientation.LandscapeLeft; - } - - if (!PlayerSettings.virtualRealitySupported) - { - // NOTE: This value should not affect the main window surface - // when Built-in VR support is enabled. - - // NOTE: On Adreno Lollipop, it is an error to have antiAliasing set on the - // main window surface with front buffer rendering enabled. The view will - // render black. - // On Adreno KitKat, some tiling control modes will cause the view to render - // black. - if (QualitySettings.antiAliasing != 0 && QualitySettings.antiAliasing != 1) - { - Debug.Log("MoonlightLoader: Disabling antiAliasing"); - QualitySettings.antiAliasing = 1; - } - } - - if (QualitySettings.vSyncCount != 0) - { - Debug.Log("MoonlightLoader: Setting vsyncCount to 0"); - // We sync in the TimeWarp, so we don't want unity syncing elsewhere. - QualitySettings.vSyncCount = 0; - } - } - - static void EnforceVRSupport() - { - if (PlayerSettings.virtualRealitySupported) - return; - - var mgrs = GameObject.FindObjectsOfType(); - for (int i = 0; i < mgrs.Length; ++i) - { - if (mgrs [i].isActiveAndEnabled) - { - Debug.Log ("Enabling Unity VR support"); - PlayerSettings.virtualRealitySupported = true; - return; - } - } - } - - private static void EnforceBundleId() - { - if (!PlayerSettings.virtualRealitySupported) - return; + EditorApplication.update += EnforceBundleId; + EditorApplication.update += EnforceVRSupport; + EditorApplication.update += EnforceInstallLocation; + + if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.Android) + return; + + if (PlayerSettings.defaultInterfaceOrientation != UIOrientation.LandscapeLeft) + { + Debug.Log("MoonlightLoader: Setting orientation to Landscape Left"); + // Default screen orientation must be set to landscape left. + PlayerSettings.defaultInterfaceOrientation = UIOrientation.LandscapeLeft; + } + + if (!PlayerSettings.virtualRealitySupported) + { + // NOTE: This value should not affect the main window surface + // when Built-in VR support is enabled. + + // NOTE: On Adreno Lollipop, it is an error to have antiAliasing set on the + // main window surface with front buffer rendering enabled. The view will + // render black. + // On Adreno KitKat, some tiling control modes will cause the view to render + // black. + if (QualitySettings.antiAliasing != 0 && QualitySettings.antiAliasing != 1) + { + Debug.Log("MoonlightLoader: Disabling antiAliasing"); + QualitySettings.antiAliasing = 1; + } + } + + if (QualitySettings.vSyncCount != 0) + { + Debug.Log("MoonlightLoader: Setting vsyncCount to 0"); + // We sync in the TimeWarp, so we don't want unity syncing elsewhere. + QualitySettings.vSyncCount = 0; + } + } + + static void EnforceVRSupport() + { + if (PlayerSettings.virtualRealitySupported) + return; + + var mgrs = GameObject.FindObjectsOfType(); + for (int i = 0; i < mgrs.Length; ++i) + { + if (mgrs[i].isActiveAndEnabled) + { + Debug.Log("Enabling Unity VR support"); + PlayerSettings.virtualRealitySupported = true; + return; + } + } + } + + private static void EnforceBundleId() + { + if (!PlayerSettings.virtualRealitySupported) + return; #if UNITY_5_6_OR_NEWER if (PlayerSettings.applicationIdentifier == "" || PlayerSettings.applicationIdentifier == "com.Company.ProductName") @@ -103,120 +103,120 @@ private static void EnforceBundleId() PlayerSettings.applicationIdentifier = defaultBundleId; } #else - if (PlayerSettings.bundleIdentifier == "" || PlayerSettings.bundleIdentifier == "com.Company.ProductName") - { - string defaultBundleId = "com.oculus.UnitySample"; - Debug.LogWarning("\"" + PlayerSettings.bundleIdentifier + "\" is not a valid bundle identifier. Defaulting to \"" + defaultBundleId + "\"."); - PlayerSettings.bundleIdentifier = defaultBundleId; - } + if (PlayerSettings.bundleIdentifier == "" || PlayerSettings.bundleIdentifier == "com.Company.ProductName") + { + string defaultBundleId = "com.oculus.UnitySample"; + Debug.LogWarning("\"" + PlayerSettings.bundleIdentifier + "\" is not a valid bundle identifier. Defaulting to \"" + defaultBundleId + "\"."); + PlayerSettings.bundleIdentifier = defaultBundleId; + } #endif - } - - private static void EnforceInstallLocation() - { - PlayerSettings.Android.preferredInstallLocation = AndroidPreferredInstallLocation.Auto; - } - - private static void EnforceInputManagerBindings() - { - try - { - BindAxis(new Axis() { name = "Oculus_GearVR_LThumbstickX", axis = 0, }); - BindAxis(new Axis() { name = "Oculus_GearVR_LThumbstickY", axis = 1, invert = true }); - BindAxis(new Axis() { name = "Oculus_GearVR_RThumbstickX", axis = 2, }); - BindAxis(new Axis() { name = "Oculus_GearVR_RThumbstickY", axis = 3, invert = true }); - BindAxis(new Axis() { name = "Oculus_GearVR_DpadX", axis = 4, }); - BindAxis(new Axis() { name = "Oculus_GearVR_DpadY", axis = 5, invert = true }); - BindAxis(new Axis() { name = "Oculus_GearVR_LIndexTrigger", axis = 12, }); - BindAxis(new Axis() { name = "Oculus_GearVR_RIndexTrigger", axis = 11, }); - } - catch - { - Debug.LogError("Failed to apply Oculus GearVR input manager bindings."); - } - } - - private static void EnforceOSIG() - { - // Don't bug the user in play mode. - if (Application.isPlaying) - return; - - // Don't warn if the project may be set up for submission or global signing. - if (File.Exists("Assets/Plugins/Android/AndroidManifest.xml")) - return; - - var files = Directory.GetFiles("Assets/Plugins/Android/assets"); - bool foundPossibleOsig = false; - for (int i = 0; i < files.Length; ++i) - { - if (!files[i].Contains(".txt")) - { - foundPossibleOsig = true; - break; - } - } - - if (!foundPossibleOsig) - Debug.LogWarning("Missing Gear VR OSIG at Assets/Plugins/Android/assets. Please see https://dashboard.oculus.com/tools/osig-generator"); - } - - private class Axis - { - public string name = String.Empty; - public string descriptiveName = String.Empty; - public string descriptiveNegativeName = String.Empty; - public string negativeButton = String.Empty; - public string positiveButton = String.Empty; - public string altNegativeButton = String.Empty; - public string altPositiveButton = String.Empty; - public float gravity = 0.0f; - public float dead = 0.001f; - public float sensitivity = 1.0f; - public bool snap = false; - public bool invert = false; - public int type = 2; - public int axis = 0; - public int joyNum = 0; - } - - private static void BindAxis(Axis axis) - { - SerializedObject serializedObject = new SerializedObject(AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/InputManager.asset")[0]); - SerializedProperty axesProperty = serializedObject.FindProperty("m_Axes"); - - SerializedProperty axisIter = axesProperty.Copy(); - axisIter.Next(true); - axisIter.Next(true); - while (axisIter.Next(false)) - { - if (axisIter.FindPropertyRelative("m_Name").stringValue == axis.name) - { - // Axis already exists. Don't create binding. - return; - } - } - - axesProperty.arraySize++; - serializedObject.ApplyModifiedProperties(); - - SerializedProperty axisProperty = axesProperty.GetArrayElementAtIndex(axesProperty.arraySize - 1); - axisProperty.FindPropertyRelative("m_Name").stringValue = axis.name; - axisProperty.FindPropertyRelative("descriptiveName").stringValue = axis.descriptiveName; - axisProperty.FindPropertyRelative("descriptiveNegativeName").stringValue = axis.descriptiveNegativeName; - axisProperty.FindPropertyRelative("negativeButton").stringValue = axis.negativeButton; - axisProperty.FindPropertyRelative("positiveButton").stringValue = axis.positiveButton; - axisProperty.FindPropertyRelative("altNegativeButton").stringValue = axis.altNegativeButton; - axisProperty.FindPropertyRelative("altPositiveButton").stringValue = axis.altPositiveButton; - axisProperty.FindPropertyRelative("gravity").floatValue = axis.gravity; - axisProperty.FindPropertyRelative("dead").floatValue = axis.dead; - axisProperty.FindPropertyRelative("sensitivity").floatValue = axis.sensitivity; - axisProperty.FindPropertyRelative("snap").boolValue = axis.snap; - axisProperty.FindPropertyRelative("invert").boolValue = axis.invert; - axisProperty.FindPropertyRelative("type").intValue = axis.type; - axisProperty.FindPropertyRelative("axis").intValue = axis.axis; - axisProperty.FindPropertyRelative("joyNum").intValue = axis.joyNum; - serializedObject.ApplyModifiedProperties(); - } + } + + private static void EnforceInstallLocation() + { + PlayerSettings.Android.preferredInstallLocation = AndroidPreferredInstallLocation.Auto; + } + + private static void EnforceInputManagerBindings() + { + try + { + BindAxis(new Axis() { name = "Oculus_GearVR_LThumbstickX", axis = 0, }); + BindAxis(new Axis() { name = "Oculus_GearVR_LThumbstickY", axis = 1, invert = true }); + BindAxis(new Axis() { name = "Oculus_GearVR_RThumbstickX", axis = 2, }); + BindAxis(new Axis() { name = "Oculus_GearVR_RThumbstickY", axis = 3, invert = true }); + BindAxis(new Axis() { name = "Oculus_GearVR_DpadX", axis = 4, }); + BindAxis(new Axis() { name = "Oculus_GearVR_DpadY", axis = 5, invert = true }); + BindAxis(new Axis() { name = "Oculus_GearVR_LIndexTrigger", axis = 12, }); + BindAxis(new Axis() { name = "Oculus_GearVR_RIndexTrigger", axis = 11, }); + } + catch + { + Debug.LogError("Failed to apply Oculus GearVR input manager bindings."); + } + } + + private static void EnforceOSIG() + { + // Don't bug the user in play mode. + if (Application.isPlaying) + return; + + // Don't warn if the project may be set up for submission or global signing. + if (File.Exists("Assets/Plugins/Android/AndroidManifest.xml")) + return; + + var files = Directory.GetFiles("Assets/Plugins/Android/assets"); + bool foundPossibleOsig = false; + for (int i = 0; i < files.Length; ++i) + { + if (!files[i].Contains(".txt")) + { + foundPossibleOsig = true; + break; + } + } + + if (!foundPossibleOsig) + Debug.LogWarning("Missing Gear VR OSIG at Assets/Plugins/Android/assets. Please see https://dashboard.oculus.com/tools/osig-generator"); + } + + private class Axis + { + public string name = String.Empty; + public string descriptiveName = String.Empty; + public string descriptiveNegativeName = String.Empty; + public string negativeButton = String.Empty; + public string positiveButton = String.Empty; + public string altNegativeButton = String.Empty; + public string altPositiveButton = String.Empty; + public float gravity = 0.0f; + public float dead = 0.001f; + public float sensitivity = 1.0f; + public bool snap = false; + public bool invert = false; + public int type = 2; + public int axis = 0; + public int joyNum = 0; + } + + private static void BindAxis(Axis axis) + { + SerializedObject serializedObject = new SerializedObject(AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/InputManager.asset")[0]); + SerializedProperty axesProperty = serializedObject.FindProperty("m_Axes"); + + SerializedProperty axisIter = axesProperty.Copy(); + axisIter.Next(true); + axisIter.Next(true); + while (axisIter.Next(false)) + { + if (axisIter.FindPropertyRelative("m_Name").stringValue == axis.name) + { + // Axis already exists. Don't create binding. + return; + } + } + + axesProperty.arraySize++; + serializedObject.ApplyModifiedProperties(); + + SerializedProperty axisProperty = axesProperty.GetArrayElementAtIndex(axesProperty.arraySize - 1); + axisProperty.FindPropertyRelative("m_Name").stringValue = axis.name; + axisProperty.FindPropertyRelative("descriptiveName").stringValue = axis.descriptiveName; + axisProperty.FindPropertyRelative("descriptiveNegativeName").stringValue = axis.descriptiveNegativeName; + axisProperty.FindPropertyRelative("negativeButton").stringValue = axis.negativeButton; + axisProperty.FindPropertyRelative("positiveButton").stringValue = axis.positiveButton; + axisProperty.FindPropertyRelative("altNegativeButton").stringValue = axis.altNegativeButton; + axisProperty.FindPropertyRelative("altPositiveButton").stringValue = axis.altPositiveButton; + axisProperty.FindPropertyRelative("gravity").floatValue = axis.gravity; + axisProperty.FindPropertyRelative("dead").floatValue = axis.dead; + axisProperty.FindPropertyRelative("sensitivity").floatValue = axis.sensitivity; + axisProperty.FindPropertyRelative("snap").boolValue = axis.snap; + axisProperty.FindPropertyRelative("invert").boolValue = axis.invert; + axisProperty.FindPropertyRelative("type").intValue = axis.type; + axisProperty.FindPropertyRelative("axis").intValue = axis.axis; + axisProperty.FindPropertyRelative("joyNum").intValue = axis.joyNum; + serializedObject.ApplyModifiedProperties(); + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Editor/OVRScreenshotWizard.cs b/Assets/Scripts/ThirdParty/OVR/Editor/OVRScreenshotWizard.cs index a4ecb777..8c10db06 100644 --- a/Assets/Scripts/ThirdParty/OVR/Editor/OVRScreenshotWizard.cs +++ b/Assets/Scripts/ThirdParty/OVR/Editor/OVRScreenshotWizard.cs @@ -6,181 +6,182 @@ /// From the selected transform, takes a cubemap screenshot that can be submitted with the application /// as a screenshot (or additionally used for reflection shaders). /// -class OVRScreenshotWizard : ScriptableWizard +class OVRScreenshotWizard : ScriptableWizard { - public enum TexFormat - { - JPEG, // 512kb at 1k x 1k resolution vs - PNG, // 5.3mb - } - - public enum SaveMode { - SaveCubemapScreenshot, - SaveUnityCubemap, - SaveBoth, - } - - public GameObject renderFrom = null; - public int size = 2048; - public SaveMode saveMode = SaveMode.SaveUnityCubemap; - public string cubeMapFolder = "Assets/Textures/Cubemaps"; - public TexFormat textureFormat = TexFormat.PNG; - - /// - /// Validates the user's input - /// - void OnWizardUpdate() - { - helpString = "Select a game object positioned in the place where\nyou want to render the cubemap screenshot from: "; - isValid = (renderFrom != null); - } - - /// - /// Create the asset path if it is not available. - /// Assuming the newFolderPath is stated with "Assets", which is a requirement. - /// - static bool CreateAssetPath( string newFolderPath ) - { - const int maxFoldersCount = 32; - string currentPath; - string[] pathFolders; - - pathFolders = newFolderPath.Split (new char[]{ '/' }, maxFoldersCount); - - if (!string.Equals ("Assets", pathFolders [0], System.StringComparison.OrdinalIgnoreCase)) - { - Debug.LogError( "Folder path has to be started with \" Assets \" " ); - return false; - } - - currentPath = "Assets"; - for (int i = 1; i < pathFolders.Length; i++) - { - if (!string.IsNullOrEmpty(pathFolders[i])) - { - string newPath = currentPath + "/" + pathFolders[i]; - if (!AssetDatabase.IsValidFolder(newPath)) - AssetDatabase.CreateFolder(currentPath, pathFolders[i]); - currentPath = newPath; - } - } - - Debug.Log( "Created path: " + currentPath ); - return true; - } - - /// - /// Renders the cubemap - /// - void OnWizardCreate() - { - if ( !AssetDatabase.IsValidFolder( cubeMapFolder ) ) - { - if (!CreateAssetPath(cubeMapFolder)) - { - Debug.LogError( "Created path failed: " + cubeMapFolder ); - return; - } - } - - bool existingCamera = true; - bool existingCameraStateSave = true; - Camera camera = renderFrom.GetComponent(); - if (camera == null) - { - camera = renderFrom.AddComponent(); - camera.farClipPlane = 10000f; - existingCamera = false; - } - else - { - existingCameraStateSave = camera.enabled; - camera.enabled = true; - } - // find the last screenshot saved - if (cubeMapFolder[cubeMapFolder.Length-1] != '/') - { - cubeMapFolder += "/"; - } - int idx = 0; - string[] fileNames = Directory.GetFiles(cubeMapFolder); - foreach(string fileName in fileNames) - { - if (!fileName.ToLower().EndsWith(".cubemap")) - { - continue; - } - string temp = fileName.Replace(cubeMapFolder + "vr_screenshot_", string.Empty); - temp = temp.Replace(".cubemap", string.Empty); - int tempIdx = 0; - if (int.TryParse( temp, out tempIdx )) - { - if (tempIdx > idx) - { - idx = tempIdx; - } - } - } - string pathName = string.Format("{0}vr_screenshot_{1}.cubemap", cubeMapFolder, (++idx).ToString("d2")); - Cubemap cubemap = new Cubemap(size, TextureFormat.RGB24, false); - - // render into cubemap - if ((camera != null) && (cubemap != null)) - { - // set up cubemap defaults - OVRCubemapCapture.RenderIntoCubemap(camera, cubemap); - if (existingCamera) - { - camera.enabled = existingCameraStateSave; - } - else - { - DestroyImmediate(camera); - } - // generate a regular texture as well? - if ( ( saveMode == SaveMode.SaveCubemapScreenshot ) || ( saveMode == SaveMode.SaveBoth ) ) - { - GenerateTexture(cubemap, pathName); - } - - if ( ( saveMode == SaveMode.SaveUnityCubemap ) || ( saveMode == SaveMode.SaveBoth ) ) - { - Debug.Log( "Saving: " + pathName ); - // by default the unity cubemap isn't saved - AssetDatabase.CreateAsset( cubemap, pathName ); - // reimport as necessary - AssetDatabase.SaveAssets(); - // select it in the project tree so developers can find it - EditorGUIUtility.PingObject( cubemap ); - Selection.activeObject = cubemap; - } - AssetDatabase.Refresh(); - } - } - - /// - /// Generates a NPOT 6x1 cubemap in the following format PX NX PY NY PZ NZ - /// - void GenerateTexture(Cubemap cubemap, string pathName) - { - // Encode the texture and save it to disk - pathName = pathName.Replace(".cubemap", (textureFormat == TexFormat.PNG) ? ".png" : ".jpg" ).ToLower(); - pathName = pathName.Replace( cubeMapFolder.ToLower(), "" ); - string format = textureFormat.ToString(); - string fullPath = EditorUtility.SaveFilePanel( string.Format( "Save Cubemap Screenshot as {0}", format ), "", pathName, format.ToLower() ); - if ( !string.IsNullOrEmpty( fullPath ) ) + public enum TexFormat + { + JPEG, // 512kb at 1k x 1k resolution vs + PNG, // 5.3mb + } + + public enum SaveMode + { + SaveCubemapScreenshot, + SaveUnityCubemap, + SaveBoth, + } + + public GameObject renderFrom = null; + public int size = 2048; + public SaveMode saveMode = SaveMode.SaveUnityCubemap; + public string cubeMapFolder = "Assets/Textures/Cubemaps"; + public TexFormat textureFormat = TexFormat.PNG; + + /// + /// Validates the user's input + /// + void OnWizardUpdate() + { + helpString = "Select a game object positioned in the place where\nyou want to render the cubemap screenshot from: "; + isValid = (renderFrom != null); + } + + /// + /// Create the asset path if it is not available. + /// Assuming the newFolderPath is stated with "Assets", which is a requirement. + /// + static bool CreateAssetPath(string newFolderPath) + { + const int maxFoldersCount = 32; + string currentPath; + string[] pathFolders; + + pathFolders = newFolderPath.Split(new char[] { '/' }, maxFoldersCount); + + if (!string.Equals("Assets", pathFolders[0], System.StringComparison.OrdinalIgnoreCase)) + { + Debug.LogError("Folder path has to be started with \" Assets \" "); + return false; + } + + currentPath = "Assets"; + for (int i = 1; i < pathFolders.Length; i++) + { + if (!string.IsNullOrEmpty(pathFolders[i])) + { + string newPath = currentPath + "/" + pathFolders[i]; + if (!AssetDatabase.IsValidFolder(newPath)) + AssetDatabase.CreateFolder(currentPath, pathFolders[i]); + currentPath = newPath; + } + } + + Debug.Log("Created path: " + currentPath); + return true; + } + + /// + /// Renders the cubemap + /// + void OnWizardCreate() + { + if (!AssetDatabase.IsValidFolder(cubeMapFolder)) + { + if (!CreateAssetPath(cubeMapFolder)) + { + Debug.LogError("Created path failed: " + cubeMapFolder); + return; + } + } + + bool existingCamera = true; + bool existingCameraStateSave = true; + Camera camera = renderFrom.GetComponent(); + if (camera == null) + { + camera = renderFrom.AddComponent(); + camera.farClipPlane = 10000f; + existingCamera = false; + } + else + { + existingCameraStateSave = camera.enabled; + camera.enabled = true; + } + // find the last screenshot saved + if (cubeMapFolder[cubeMapFolder.Length - 1] != '/') + { + cubeMapFolder += "/"; + } + int idx = 0; + string[] fileNames = Directory.GetFiles(cubeMapFolder); + foreach (string fileName in fileNames) + { + if (!fileName.ToLower().EndsWith(".cubemap")) + { + continue; + } + string temp = fileName.Replace(cubeMapFolder + "vr_screenshot_", string.Empty); + temp = temp.Replace(".cubemap", string.Empty); + int tempIdx = 0; + if (int.TryParse(temp, out tempIdx)) + { + if (tempIdx > idx) + { + idx = tempIdx; + } + } + } + string pathName = string.Format("{0}vr_screenshot_{1}.cubemap", cubeMapFolder, (++idx).ToString("d2")); + Cubemap cubemap = new Cubemap(size, TextureFormat.RGB24, false); + + // render into cubemap + if ((camera != null) && (cubemap != null)) { - Debug.Log( "Saving: " + fullPath ); - OVRCubemapCapture.SaveCubemapCapture(cubemap, fullPath); - } - } - - /// - /// Unity Editor menu option to take a screenshot - /// - [MenuItem("Tools/Oculus/OVR Screenshot Wizard",false,100000)] - static void TakeOVRScreenshot() - { + // set up cubemap defaults + OVRCubemapCapture.RenderIntoCubemap(camera, cubemap); + if (existingCamera) + { + camera.enabled = existingCameraStateSave; + } + else + { + DestroyImmediate(camera); + } + // generate a regular texture as well? + if ((saveMode == SaveMode.SaveCubemapScreenshot) || (saveMode == SaveMode.SaveBoth)) + { + GenerateTexture(cubemap, pathName); + } + + if ((saveMode == SaveMode.SaveUnityCubemap) || (saveMode == SaveMode.SaveBoth)) + { + Debug.Log("Saving: " + pathName); + // by default the unity cubemap isn't saved + AssetDatabase.CreateAsset(cubemap, pathName); + // reimport as necessary + AssetDatabase.SaveAssets(); + // select it in the project tree so developers can find it + EditorGUIUtility.PingObject(cubemap); + Selection.activeObject = cubemap; + } + AssetDatabase.Refresh(); + } + } + + /// + /// Generates a NPOT 6x1 cubemap in the following format PX NX PY NY PZ NZ + /// + void GenerateTexture(Cubemap cubemap, string pathName) + { + // Encode the texture and save it to disk + pathName = pathName.Replace(".cubemap", (textureFormat == TexFormat.PNG) ? ".png" : ".jpg").ToLower(); + pathName = pathName.Replace(cubeMapFolder.ToLower(), ""); + string format = textureFormat.ToString(); + string fullPath = EditorUtility.SaveFilePanel(string.Format("Save Cubemap Screenshot as {0}", format), "", pathName, format.ToLower()); + if (!string.IsNullOrEmpty(fullPath)) + { + Debug.Log("Saving: " + fullPath); + OVRCubemapCapture.SaveCubemapCapture(cubemap, fullPath); + } + } + + /// + /// Unity Editor menu option to take a screenshot + /// + [MenuItem("Tools/Oculus/OVR Screenshot Wizard", false, 100000)] + static void TakeOVRScreenshot() + { OVRScreenshotWizard wizard = ScriptableWizard.DisplayWizard("OVR Screenshot Wizard", "Render Cubemap"); if (wizard != null) { diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRBoundary.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRBoundary.cs index 5352af18..6bfaeb8c 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRBoundary.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRBoundary.cs @@ -9,177 +9,177 @@ /// public class OVRBoundary { - /// - /// Specifies a tracked node that can be queried through the boundary system. - /// - public enum Node - { - HandLeft = OVRPlugin.Node.HandLeft, ///< Tracks the left hand node. - HandRight = OVRPlugin.Node.HandRight, ///< Tracks the right hand node. - Head = OVRPlugin.Node.Head, ///< Tracks the head node. + /// + /// Specifies a tracked node that can be queried through the boundary system. + /// + public enum Node + { + HandLeft = OVRPlugin.Node.HandLeft, ///< Tracks the left hand node. + HandRight = OVRPlugin.Node.HandRight, ///< Tracks the right hand node. + Head = OVRPlugin.Node.Head, ///< Tracks the head node. } - /// - /// Specifies a boundary type surface. - /// - public enum BoundaryType - { - OuterBoundary = OVRPlugin.BoundaryType.OuterBoundary, ///< Outer boundary that closely matches the user's configured walls. - PlayArea = OVRPlugin.BoundaryType.PlayArea, ///< Smaller convex area inset within the outer boundary. + /// + /// Specifies a boundary type surface. + /// + public enum BoundaryType + { + OuterBoundary = OVRPlugin.BoundaryType.OuterBoundary, ///< Outer boundary that closely matches the user's configured walls. + PlayArea = OVRPlugin.BoundaryType.PlayArea, ///< Smaller convex area inset within the outer boundary. } - /// - /// Provides test results of boundary system queries. - /// - public struct BoundaryTestResult - { - public bool IsTriggering; ///< Returns true if the queried test would violate and/or trigger the tested boundary types. + /// + /// Provides test results of boundary system queries. + /// + public struct BoundaryTestResult + { + public bool IsTriggering; ///< Returns true if the queried test would violate and/or trigger the tested boundary types. public float ClosestDistance; ///< Returns the distance between the queried test object and the closest tested boundary type. public Vector3 ClosestPoint; ///< Returns the closest point to the queried test object. public Vector3 ClosestPointNormal; ///< Returns the normal of the closest point to the queried test object. } - /// - /// Specifies the boundary system parameters that can be configured. Can be overridden by the system or user. - /// - public struct BoundaryLookAndFeel - { - public Color Color; - } - - /// - /// Returns true if the boundary system is currently configured with valid boundary data. - /// - public bool GetConfigured() - { - return OVRPlugin.GetBoundaryConfigured(); - } - - /// - /// Returns the results of testing a tracked node against the specified boundary type. - /// All points are returned in local tracking space shared by tracked nodes and accessible through OVRCameraRig's trackingSpace anchor. - /// - public OVRBoundary.BoundaryTestResult TestNode(OVRBoundary.Node node, OVRBoundary.BoundaryType boundaryType) - { - OVRPlugin.BoundaryTestResult ovrpRes = OVRPlugin.TestBoundaryNode((OVRPlugin.Node)node, (OVRPlugin.BoundaryType)boundaryType); - - OVRBoundary.BoundaryTestResult res = new OVRBoundary.BoundaryTestResult() - { - IsTriggering = (ovrpRes.IsTriggering == OVRPlugin.Bool.True), - ClosestDistance = ovrpRes.ClosestDistance, - ClosestPoint = ovrpRes.ClosestPoint.FromFlippedZVector3f(), - ClosestPointNormal = ovrpRes.ClosestPointNormal.FromFlippedZVector3f(), - }; - - return res; - } - - /// - /// Returns the results of testing a 3d point against the specified boundary type. - /// The test point is expected in local tracking space. - /// All points are returned in local tracking space shared by tracked nodes and accessible through OVRCameraRig's trackingSpace anchor. - /// - public OVRBoundary.BoundaryTestResult TestPoint(Vector3 point, OVRBoundary.BoundaryType boundaryType) - { - OVRPlugin.BoundaryTestResult ovrpRes = OVRPlugin.TestBoundaryPoint(point.ToFlippedZVector3f(), (OVRPlugin.BoundaryType)boundaryType); - - OVRBoundary.BoundaryTestResult res = new OVRBoundary.BoundaryTestResult() - { - IsTriggering = (ovrpRes.IsTriggering == OVRPlugin.Bool.True), - ClosestDistance = ovrpRes.ClosestDistance, - ClosestPoint = ovrpRes.ClosestPoint.FromFlippedZVector3f(), - ClosestPointNormal = ovrpRes.ClosestPointNormal.FromFlippedZVector3f(), - }; - - return res; - } - - /// - /// Requests that the visual look and feel of the boundary system be changed as specified. Can be overridden by the system or user. - /// - public void SetLookAndFeel(OVRBoundary.BoundaryLookAndFeel lookAndFeel) - { - OVRPlugin.BoundaryLookAndFeel lf = new OVRPlugin.BoundaryLookAndFeel() - { - Color = lookAndFeel.Color.ToColorf() - }; - - OVRPlugin.SetBoundaryLookAndFeel(lf); - } - - /// - /// Resets the visual look and feel of the boundary system to the initial system settings. - /// - public void ResetLookAndFeel() - { - OVRPlugin.ResetBoundaryLookAndFeel(); - } - - private static int cachedVector3fSize = Marshal.SizeOf(typeof(OVRPlugin.Vector3f)); - private static OVRNativeBuffer cachedGeometryNativeBuffer = new OVRNativeBuffer(0); - private static float[] cachedGeometryManagedBuffer = new float[0]; - /// - /// Returns an array of 3d points (in clockwise order) that define the specified boundary type. - /// All points are returned in local tracking space shared by tracked nodes and accessible through OVRCameraRig's trackingSpace anchor. - /// - public Vector3[] GetGeometry(OVRBoundary.BoundaryType boundaryType) - { - int pointsCount = 0; - if (OVRPlugin.GetBoundaryGeometry2((OVRPlugin.BoundaryType)boundaryType, IntPtr.Zero, ref pointsCount)) - { - int requiredNativeBufferCapacity = pointsCount * cachedVector3fSize; - if (cachedGeometryNativeBuffer.GetCapacity() < requiredNativeBufferCapacity) - cachedGeometryNativeBuffer.Reset(requiredNativeBufferCapacity); - - int requiredManagedBufferCapacity = pointsCount * 3; - if (cachedGeometryManagedBuffer.Length < requiredManagedBufferCapacity) - cachedGeometryManagedBuffer = new float[requiredManagedBufferCapacity]; - - if (OVRPlugin.GetBoundaryGeometry2((OVRPlugin.BoundaryType)boundaryType, cachedGeometryNativeBuffer.GetPointer(), ref pointsCount)) - { - Marshal.Copy(cachedGeometryNativeBuffer.GetPointer(), cachedGeometryManagedBuffer, 0, requiredManagedBufferCapacity); - - Vector3[] points = new Vector3[pointsCount]; - - for (int i = 0; i < pointsCount; i++) - { - points[i] = new OVRPlugin.Vector3f() - { - x = cachedGeometryManagedBuffer[3 * i + 0], - y = cachedGeometryManagedBuffer[3 * i + 1], - z = cachedGeometryManagedBuffer[3 * i + 2], - }.FromFlippedZVector3f(); - } - - return points; - } - } - - return new Vector3[0]; - } - - /// - /// Returns a vector that indicates the spatial dimensions of the specified boundary type. (x = width, y = height, z = depth) - /// - public Vector3 GetDimensions(OVRBoundary.BoundaryType boundaryType) - { - return OVRPlugin.GetBoundaryDimensions((OVRPlugin.BoundaryType)boundaryType).FromVector3f(); - } - - /// - /// Returns true if the boundary system is currently visible. - /// - public bool GetVisible() - { - return OVRPlugin.GetBoundaryVisible(); - } - - /// - /// Requests that the boundary system visibility be set to the specified value. - /// The actual visibility can be overridden by the system (i.e., proximity trigger) or by the user (boundary system disabled) - /// - public void SetVisible(bool value) - { - OVRPlugin.SetBoundaryVisible(value); - } + /// + /// Specifies the boundary system parameters that can be configured. Can be overridden by the system or user. + /// + public struct BoundaryLookAndFeel + { + public Color Color; + } + + /// + /// Returns true if the boundary system is currently configured with valid boundary data. + /// + public bool GetConfigured() + { + return OVRPlugin.GetBoundaryConfigured(); + } + + /// + /// Returns the results of testing a tracked node against the specified boundary type. + /// All points are returned in local tracking space shared by tracked nodes and accessible through OVRCameraRig's trackingSpace anchor. + /// + public OVRBoundary.BoundaryTestResult TestNode(OVRBoundary.Node node, OVRBoundary.BoundaryType boundaryType) + { + OVRPlugin.BoundaryTestResult ovrpRes = OVRPlugin.TestBoundaryNode((OVRPlugin.Node)node, (OVRPlugin.BoundaryType)boundaryType); + + OVRBoundary.BoundaryTestResult res = new OVRBoundary.BoundaryTestResult() + { + IsTriggering = (ovrpRes.IsTriggering == OVRPlugin.Bool.True), + ClosestDistance = ovrpRes.ClosestDistance, + ClosestPoint = ovrpRes.ClosestPoint.FromFlippedZVector3f(), + ClosestPointNormal = ovrpRes.ClosestPointNormal.FromFlippedZVector3f(), + }; + + return res; + } + + /// + /// Returns the results of testing a 3d point against the specified boundary type. + /// The test point is expected in local tracking space. + /// All points are returned in local tracking space shared by tracked nodes and accessible through OVRCameraRig's trackingSpace anchor. + /// + public OVRBoundary.BoundaryTestResult TestPoint(Vector3 point, OVRBoundary.BoundaryType boundaryType) + { + OVRPlugin.BoundaryTestResult ovrpRes = OVRPlugin.TestBoundaryPoint(point.ToFlippedZVector3f(), (OVRPlugin.BoundaryType)boundaryType); + + OVRBoundary.BoundaryTestResult res = new OVRBoundary.BoundaryTestResult() + { + IsTriggering = (ovrpRes.IsTriggering == OVRPlugin.Bool.True), + ClosestDistance = ovrpRes.ClosestDistance, + ClosestPoint = ovrpRes.ClosestPoint.FromFlippedZVector3f(), + ClosestPointNormal = ovrpRes.ClosestPointNormal.FromFlippedZVector3f(), + }; + + return res; + } + + /// + /// Requests that the visual look and feel of the boundary system be changed as specified. Can be overridden by the system or user. + /// + public void SetLookAndFeel(OVRBoundary.BoundaryLookAndFeel lookAndFeel) + { + OVRPlugin.BoundaryLookAndFeel lf = new OVRPlugin.BoundaryLookAndFeel() + { + Color = lookAndFeel.Color.ToColorf() + }; + + OVRPlugin.SetBoundaryLookAndFeel(lf); + } + + /// + /// Resets the visual look and feel of the boundary system to the initial system settings. + /// + public void ResetLookAndFeel() + { + OVRPlugin.ResetBoundaryLookAndFeel(); + } + + private static int cachedVector3fSize = Marshal.SizeOf(typeof(OVRPlugin.Vector3f)); + private static OVRNativeBuffer cachedGeometryNativeBuffer = new OVRNativeBuffer(0); + private static float[] cachedGeometryManagedBuffer = new float[0]; + /// + /// Returns an array of 3d points (in clockwise order) that define the specified boundary type. + /// All points are returned in local tracking space shared by tracked nodes and accessible through OVRCameraRig's trackingSpace anchor. + /// + public Vector3[] GetGeometry(OVRBoundary.BoundaryType boundaryType) + { + int pointsCount = 0; + if (OVRPlugin.GetBoundaryGeometry2((OVRPlugin.BoundaryType)boundaryType, IntPtr.Zero, ref pointsCount)) + { + int requiredNativeBufferCapacity = pointsCount * cachedVector3fSize; + if (cachedGeometryNativeBuffer.GetCapacity() < requiredNativeBufferCapacity) + cachedGeometryNativeBuffer.Reset(requiredNativeBufferCapacity); + + int requiredManagedBufferCapacity = pointsCount * 3; + if (cachedGeometryManagedBuffer.Length < requiredManagedBufferCapacity) + cachedGeometryManagedBuffer = new float[requiredManagedBufferCapacity]; + + if (OVRPlugin.GetBoundaryGeometry2((OVRPlugin.BoundaryType)boundaryType, cachedGeometryNativeBuffer.GetPointer(), ref pointsCount)) + { + Marshal.Copy(cachedGeometryNativeBuffer.GetPointer(), cachedGeometryManagedBuffer, 0, requiredManagedBufferCapacity); + + Vector3[] points = new Vector3[pointsCount]; + + for (int i = 0; i < pointsCount; i++) + { + points[i] = new OVRPlugin.Vector3f() + { + x = cachedGeometryManagedBuffer[3 * i + 0], + y = cachedGeometryManagedBuffer[3 * i + 1], + z = cachedGeometryManagedBuffer[3 * i + 2], + }.FromFlippedZVector3f(); + } + + return points; + } + } + + return new Vector3[0]; + } + + /// + /// Returns a vector that indicates the spatial dimensions of the specified boundary type. (x = width, y = height, z = depth) + /// + public Vector3 GetDimensions(OVRBoundary.BoundaryType boundaryType) + { + return OVRPlugin.GetBoundaryDimensions((OVRPlugin.BoundaryType)boundaryType).FromVector3f(); + } + + /// + /// Returns true if the boundary system is currently visible. + /// + public bool GetVisible() + { + return OVRPlugin.GetBoundaryVisible(); + } + + /// + /// Requests that the boundary system visibility be set to the specified value. + /// The actual visibility can be overridden by the system (i.e., proximity trigger) or by the user (boundary system disabled) + /// + public void SetVisible(bool value) + { + OVRPlugin.SetBoundaryVisible(value); + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRCameraRig.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRCameraRig.cs index dea65eee..3d9f38b5 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRCameraRig.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRCameraRig.cs @@ -31,301 +31,301 @@ limitations under the License. [ExecuteInEditMode] public class OVRCameraRig : MonoBehaviour { - /// - /// The left eye camera. - /// - public Camera leftEyeCamera { get { return (usePerEyeCameras) ? _leftEyeCamera : _centerEyeCamera; } } - /// - /// The right eye camera. - /// - public Camera rightEyeCamera { get { return (usePerEyeCameras) ? _rightEyeCamera : _centerEyeCamera; } } - /// - /// Provides a root transform for all anchors in tracking space. - /// - public Transform trackingSpace { get; private set; } - /// - /// Always coincides with the pose of the left eye. - /// - public Transform leftEyeAnchor { get; private set; } - /// - /// Always coincides with average of the left and right eye poses. - /// - public Transform centerEyeAnchor { get; private set; } - /// - /// Always coincides with the pose of the right eye. - /// - public Transform rightEyeAnchor { get; private set; } - /// - /// Always coincides with the pose of the left hand. - /// - public Transform leftHandAnchor { get; private set; } - /// - /// Always coincides with the pose of the right hand. - /// - public Transform rightHandAnchor { get; private set; } - /// - /// Always coincides with the pose of the sensor. - /// - public Transform trackerAnchor { get; private set; } - /// - /// Occurs when the eye pose anchors have been set. - /// - public event System.Action UpdatedAnchors; - - /// - /// If true, separate cameras will be used for the left and right eyes. - /// - public bool usePerEyeCameras = false; - - /// - /// If true, all tracked anchors are updated in FixedUpdate instead of Update to favor physics fidelity. - /// \note: If the fixed update rate doesn't match the rendering framerate (OVRManager.display.appFramerate), the anchors will visibly judder. - /// - public bool useFixedUpdateForTracking = false; - - private bool _skipUpdate = false; - - private readonly string trackingSpaceName = "TrackingSpace"; - private readonly string trackerAnchorName = "TrackerAnchor"; - private readonly string eyeAnchorName = "EyeAnchor"; - private readonly string handAnchorName = "HandAnchor"; - private readonly string legacyEyeAnchorName = "Camera"; - private Camera _centerEyeCamera; - private Camera _leftEyeCamera; - private Camera _rightEyeCamera; - -#region Unity Messages - private void Awake() - { - _skipUpdate = true; - EnsureGameObjectIntegrity(); - } - - private void Start() - { - UpdateAnchors(); - } - - private void FixedUpdate() - { - if (useFixedUpdateForTracking) - UpdateAnchors(); - } - - private void Update() - { - _skipUpdate = false; - - if (!useFixedUpdateForTracking) - UpdateAnchors(); - } - -#endregion - - private void UpdateAnchors() - { - EnsureGameObjectIntegrity(); - - if (!Application.isPlaying) - return; - - if (_skipUpdate) - { - centerEyeAnchor.FromOVRPose(OVRPose.identity, true); - leftEyeAnchor.FromOVRPose(OVRPose.identity, true); - rightEyeAnchor.FromOVRPose(OVRPose.identity, true); - - return; - } - - bool monoscopic = OVRManager.instance.monoscopic; - - OVRPose tracker = OVRManager.tracker.GetPose(); - - trackerAnchor.localRotation = tracker.orientation; - centerEyeAnchor.localRotation = UnityEngine.XR.InputTracking.GetLocalRotation(UnityEngine.XR.XRNode.CenterEye); + /// + /// The left eye camera. + /// + public Camera leftEyeCamera { get { return (usePerEyeCameras) ? _leftEyeCamera : _centerEyeCamera; } } + /// + /// The right eye camera. + /// + public Camera rightEyeCamera { get { return (usePerEyeCameras) ? _rightEyeCamera : _centerEyeCamera; } } + /// + /// Provides a root transform for all anchors in tracking space. + /// + public Transform trackingSpace { get; private set; } + /// + /// Always coincides with the pose of the left eye. + /// + public Transform leftEyeAnchor { get; private set; } + /// + /// Always coincides with average of the left and right eye poses. + /// + public Transform centerEyeAnchor { get; private set; } + /// + /// Always coincides with the pose of the right eye. + /// + public Transform rightEyeAnchor { get; private set; } + /// + /// Always coincides with the pose of the left hand. + /// + public Transform leftHandAnchor { get; private set; } + /// + /// Always coincides with the pose of the right hand. + /// + public Transform rightHandAnchor { get; private set; } + /// + /// Always coincides with the pose of the sensor. + /// + public Transform trackerAnchor { get; private set; } + /// + /// Occurs when the eye pose anchors have been set. + /// + public event System.Action UpdatedAnchors; + + /// + /// If true, separate cameras will be used for the left and right eyes. + /// + public bool usePerEyeCameras = false; + + /// + /// If true, all tracked anchors are updated in FixedUpdate instead of Update to favor physics fidelity. + /// \note: If the fixed update rate doesn't match the rendering framerate (OVRManager.display.appFramerate), the anchors will visibly judder. + /// + public bool useFixedUpdateForTracking = false; + + private bool _skipUpdate = false; + + private readonly string trackingSpaceName = "TrackingSpace"; + private readonly string trackerAnchorName = "TrackerAnchor"; + private readonly string eyeAnchorName = "EyeAnchor"; + private readonly string handAnchorName = "HandAnchor"; + private readonly string legacyEyeAnchorName = "Camera"; + private Camera _centerEyeCamera; + private Camera _leftEyeCamera; + private Camera _rightEyeCamera; + + #region Unity Messages + private void Awake() + { + _skipUpdate = true; + EnsureGameObjectIntegrity(); + } + + private void Start() + { + UpdateAnchors(); + } + + private void FixedUpdate() + { + if (useFixedUpdateForTracking) + UpdateAnchors(); + } + + private void Update() + { + _skipUpdate = false; + + if (!useFixedUpdateForTracking) + UpdateAnchors(); + } + + #endregion + + private void UpdateAnchors() + { + EnsureGameObjectIntegrity(); + + if (!Application.isPlaying) + return; + + if (_skipUpdate) + { + centerEyeAnchor.FromOVRPose(OVRPose.identity, true); + leftEyeAnchor.FromOVRPose(OVRPose.identity, true); + rightEyeAnchor.FromOVRPose(OVRPose.identity, true); + + return; + } + + bool monoscopic = OVRManager.instance.monoscopic; + + OVRPose tracker = OVRManager.tracker.GetPose(); + + trackerAnchor.localRotation = tracker.orientation; + centerEyeAnchor.localRotation = UnityEngine.XR.InputTracking.GetLocalRotation(UnityEngine.XR.XRNode.CenterEye); leftEyeAnchor.localRotation = monoscopic ? centerEyeAnchor.localRotation : UnityEngine.XR.InputTracking.GetLocalRotation(UnityEngine.XR.XRNode.LeftEye); - rightEyeAnchor.localRotation = monoscopic ? centerEyeAnchor.localRotation : UnityEngine.XR.InputTracking.GetLocalRotation(UnityEngine.XR.XRNode.RightEye); - leftHandAnchor.localRotation = OVRInput.GetLocalControllerRotation(OVRInput.Controller.LTouch); + rightEyeAnchor.localRotation = monoscopic ? centerEyeAnchor.localRotation : UnityEngine.XR.InputTracking.GetLocalRotation(UnityEngine.XR.XRNode.RightEye); + leftHandAnchor.localRotation = OVRInput.GetLocalControllerRotation(OVRInput.Controller.LTouch); rightHandAnchor.localRotation = OVRInput.GetLocalControllerRotation(OVRInput.Controller.RTouch); - trackerAnchor.localPosition = tracker.position; - centerEyeAnchor.localPosition = UnityEngine.XR.InputTracking.GetLocalPosition(UnityEngine.XR.XRNode.CenterEye); - leftEyeAnchor.localPosition = monoscopic ? centerEyeAnchor.localPosition : UnityEngine.XR.InputTracking.GetLocalPosition(UnityEngine.XR.XRNode.LeftEye); - rightEyeAnchor.localPosition = monoscopic ? centerEyeAnchor.localPosition : UnityEngine.XR.InputTracking.GetLocalPosition(UnityEngine.XR.XRNode.RightEye); + trackerAnchor.localPosition = tracker.position; + centerEyeAnchor.localPosition = UnityEngine.XR.InputTracking.GetLocalPosition(UnityEngine.XR.XRNode.CenterEye); + leftEyeAnchor.localPosition = monoscopic ? centerEyeAnchor.localPosition : UnityEngine.XR.InputTracking.GetLocalPosition(UnityEngine.XR.XRNode.LeftEye); + rightEyeAnchor.localPosition = monoscopic ? centerEyeAnchor.localPosition : UnityEngine.XR.InputTracking.GetLocalPosition(UnityEngine.XR.XRNode.RightEye); leftHandAnchor.localPosition = OVRInput.GetLocalControllerPosition(OVRInput.Controller.LTouch); rightHandAnchor.localPosition = OVRInput.GetLocalControllerPosition(OVRInput.Controller.RTouch); - if (UpdatedAnchors != null) - { - UpdatedAnchors(this); - } - } + if (UpdatedAnchors != null) + { + UpdatedAnchors(this); + } + } - public void EnsureGameObjectIntegrity() - { - if (trackingSpace == null) - trackingSpace = ConfigureRootAnchor(trackingSpaceName); + public void EnsureGameObjectIntegrity() + { + if (trackingSpace == null) + trackingSpace = ConfigureRootAnchor(trackingSpaceName); - if (leftEyeAnchor == null) + if (leftEyeAnchor == null) leftEyeAnchor = ConfigureEyeAnchor(trackingSpace, UnityEngine.XR.XRNode.LeftEye); - if (centerEyeAnchor == null) + if (centerEyeAnchor == null) centerEyeAnchor = ConfigureEyeAnchor(trackingSpace, UnityEngine.XR.XRNode.CenterEye); - if (rightEyeAnchor == null) + if (rightEyeAnchor == null) rightEyeAnchor = ConfigureEyeAnchor(trackingSpace, UnityEngine.XR.XRNode.RightEye); - if (leftHandAnchor == null) + if (leftHandAnchor == null) leftHandAnchor = ConfigureHandAnchor(trackingSpace, OVRPlugin.Node.HandLeft); - if (rightHandAnchor == null) + if (rightHandAnchor == null) rightHandAnchor = ConfigureHandAnchor(trackingSpace, OVRPlugin.Node.HandRight); - if (trackerAnchor == null) - trackerAnchor = ConfigureTrackerAnchor(trackingSpace); + if (trackerAnchor == null) + trackerAnchor = ConfigureTrackerAnchor(trackingSpace); - if (_centerEyeCamera == null || _leftEyeCamera == null || _rightEyeCamera == null) - { - _centerEyeCamera = centerEyeAnchor.GetComponent(); - _leftEyeCamera = leftEyeAnchor.GetComponent(); - _rightEyeCamera = rightEyeAnchor.GetComponent(); + if (_centerEyeCamera == null || _leftEyeCamera == null || _rightEyeCamera == null) + { + _centerEyeCamera = centerEyeAnchor.GetComponent(); + _leftEyeCamera = leftEyeAnchor.GetComponent(); + _rightEyeCamera = rightEyeAnchor.GetComponent(); - if (_centerEyeCamera == null) - { - _centerEyeCamera = centerEyeAnchor.gameObject.AddComponent(); - _centerEyeCamera.tag = "MainCamera"; - } + if (_centerEyeCamera == null) + { + _centerEyeCamera = centerEyeAnchor.gameObject.AddComponent(); + _centerEyeCamera.tag = "MainCamera"; + } - if (_leftEyeCamera == null) - { - _leftEyeCamera = leftEyeAnchor.gameObject.AddComponent(); - _leftEyeCamera.tag = "MainCamera"; + if (_leftEyeCamera == null) + { + _leftEyeCamera = leftEyeAnchor.gameObject.AddComponent(); + _leftEyeCamera.tag = "MainCamera"; #if !UNITY_5_4_OR_NEWER - usePerEyeCameras = false; - Debug.Log("Please set left eye Camera's Target Eye to Left before using."); + usePerEyeCameras = false; + Debug.Log("Please set left eye Camera's Target Eye to Left before using."); #endif - } + } - if (_rightEyeCamera == null) - { - _rightEyeCamera = rightEyeAnchor.gameObject.AddComponent(); - _rightEyeCamera.tag = "MainCamera"; + if (_rightEyeCamera == null) + { + _rightEyeCamera = rightEyeAnchor.gameObject.AddComponent(); + _rightEyeCamera.tag = "MainCamera"; #if !UNITY_5_4_OR_NEWER - usePerEyeCameras = false; - Debug.Log("Please set right eye Camera's Target Eye to Right before using."); + usePerEyeCameras = false; + Debug.Log("Please set right eye Camera's Target Eye to Right before using."); #endif - } + } #if UNITY_5_4_OR_NEWER _centerEyeCamera.stereoTargetEye = StereoTargetEyeMask.Both; _leftEyeCamera.stereoTargetEye = StereoTargetEyeMask.Left; _rightEyeCamera.stereoTargetEye = StereoTargetEyeMask.Right; #endif - } - - if (_centerEyeCamera.enabled == usePerEyeCameras || - _leftEyeCamera.enabled == !usePerEyeCameras || - _rightEyeCamera.enabled == !usePerEyeCameras) - { - _skipUpdate = true; - } - - _centerEyeCamera.enabled = !usePerEyeCameras; - _leftEyeCamera.enabled = usePerEyeCameras; - _rightEyeCamera.enabled = usePerEyeCameras; - } - - private Transform ConfigureRootAnchor(string name) - { - Transform root = transform.Find(name); - - if (root == null) - { - root = new GameObject(name).transform; - } - - root.parent = transform; - root.localScale = Vector3.one; - root.localPosition = Vector3.zero; - root.localRotation = Quaternion.identity; - - return root; - } - - private Transform ConfigureEyeAnchor(Transform root, UnityEngine.XR.XRNode eye) - { - string eyeName = (eye == UnityEngine.XR.XRNode.CenterEye) ? "Center" : (eye == UnityEngine.XR.XRNode.LeftEye) ? "Left" : "Right"; - string name = eyeName + eyeAnchorName; - Transform anchor = transform.Find(root.name + "/" + name); - - if (anchor == null) - { - anchor = transform.Find(name); - } - - if (anchor == null) - { - string legacyName = legacyEyeAnchorName + eye.ToString(); - anchor = transform.Find(legacyName); - } - - if (anchor == null) - { - anchor = new GameObject(name).transform; - } - - anchor.name = name; - anchor.parent = root; - anchor.localScale = Vector3.one; - anchor.localPosition = Vector3.zero; - anchor.localRotation = Quaternion.identity; - - return anchor; - } - - private Transform ConfigureHandAnchor(Transform root, OVRPlugin.Node hand) - { - string handName = (hand == OVRPlugin.Node.HandLeft) ? "Left" : "Right"; - string name = handName + handAnchorName; - Transform anchor = transform.Find(root.name + "/" + name); - - if (anchor == null) - { - anchor = transform.Find(name); - } - - if (anchor == null) - { - anchor = new GameObject(name).transform; - } - - anchor.name = name; - anchor.parent = root; - anchor.localScale = Vector3.one; - anchor.localPosition = Vector3.zero; - anchor.localRotation = Quaternion.identity; - - return anchor; - } - - private Transform ConfigureTrackerAnchor(Transform root) - { - string name = trackerAnchorName; - Transform anchor = transform.Find(root.name + "/" + name); - - if (anchor == null) - { - anchor = new GameObject(name).transform; - } - - anchor.parent = root; - anchor.localScale = Vector3.one; - anchor.localPosition = Vector3.zero; - anchor.localRotation = Quaternion.identity; - - return anchor; - } + } + + if (_centerEyeCamera.enabled == usePerEyeCameras || + _leftEyeCamera.enabled == !usePerEyeCameras || + _rightEyeCamera.enabled == !usePerEyeCameras) + { + _skipUpdate = true; + } + + _centerEyeCamera.enabled = !usePerEyeCameras; + _leftEyeCamera.enabled = usePerEyeCameras; + _rightEyeCamera.enabled = usePerEyeCameras; + } + + private Transform ConfigureRootAnchor(string name) + { + Transform root = transform.Find(name); + + if (root == null) + { + root = new GameObject(name).transform; + } + + root.parent = transform; + root.localScale = Vector3.one; + root.localPosition = Vector3.zero; + root.localRotation = Quaternion.identity; + + return root; + } + + private Transform ConfigureEyeAnchor(Transform root, UnityEngine.XR.XRNode eye) + { + string eyeName = (eye == UnityEngine.XR.XRNode.CenterEye) ? "Center" : (eye == UnityEngine.XR.XRNode.LeftEye) ? "Left" : "Right"; + string name = eyeName + eyeAnchorName; + Transform anchor = transform.Find(root.name + "/" + name); + + if (anchor == null) + { + anchor = transform.Find(name); + } + + if (anchor == null) + { + string legacyName = legacyEyeAnchorName + eye.ToString(); + anchor = transform.Find(legacyName); + } + + if (anchor == null) + { + anchor = new GameObject(name).transform; + } + + anchor.name = name; + anchor.parent = root; + anchor.localScale = Vector3.one; + anchor.localPosition = Vector3.zero; + anchor.localRotation = Quaternion.identity; + + return anchor; + } + + private Transform ConfigureHandAnchor(Transform root, OVRPlugin.Node hand) + { + string handName = (hand == OVRPlugin.Node.HandLeft) ? "Left" : "Right"; + string name = handName + handAnchorName; + Transform anchor = transform.Find(root.name + "/" + name); + + if (anchor == null) + { + anchor = transform.Find(name); + } + + if (anchor == null) + { + anchor = new GameObject(name).transform; + } + + anchor.name = name; + anchor.parent = root; + anchor.localScale = Vector3.one; + anchor.localPosition = Vector3.zero; + anchor.localRotation = Quaternion.identity; + + return anchor; + } + + private Transform ConfigureTrackerAnchor(Transform root) + { + string name = trackerAnchorName; + Transform anchor = transform.Find(root.name + "/" + name); + + if (anchor == null) + { + anchor = new GameObject(name).transform; + } + + anchor.parent = root; + anchor.localScale = Vector3.one; + anchor.localPosition = Vector3.zero; + anchor.localRotation = Quaternion.identity; + + return anchor; + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRCommon.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRCommon.cs index f98b7f75..aeddac13 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRCommon.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRCommon.cs @@ -29,123 +29,123 @@ limitations under the License. /// public static class OVRExtensions { - /// - /// Converts the given world-space transform to an OVRPose in tracking space. - /// - public static OVRPose ToTrackingSpacePose(this Transform transform) - { - OVRPose headPose; - headPose.position = UnityEngine.XR.InputTracking.GetLocalPosition(UnityEngine.XR.XRNode.Head); - headPose.orientation = UnityEngine.XR.InputTracking.GetLocalRotation(UnityEngine.XR.XRNode.Head); - - var ret = headPose * transform.ToHeadSpacePose(); - - return ret; - } - - /// - /// Converts the given world-space transform to an OVRPose in head space. - /// - public static OVRPose ToHeadSpacePose(this Transform transform) - { - return Camera.current.transform.ToOVRPose().Inverse() * transform.ToOVRPose(); - } - - internal static OVRPose ToOVRPose(this Transform t, bool isLocal = false) - { - OVRPose pose; - pose.orientation = (isLocal) ? t.localRotation : t.rotation; - pose.position = (isLocal) ? t.localPosition : t.position; - return pose; - } - - internal static void FromOVRPose(this Transform t, OVRPose pose, bool isLocal = false) - { - if (isLocal) - { - t.localRotation = pose.orientation; - t.localPosition = pose.position; - } - else - { - t.rotation = pose.orientation; - t.position = pose.position; - } - } - - internal static OVRPose ToOVRPose(this OVRPlugin.Posef p) - { - return new OVRPose() - { - position = new Vector3(p.Position.x, p.Position.y, -p.Position.z), - orientation = new Quaternion(-p.Orientation.x, -p.Orientation.y, p.Orientation.z, p.Orientation.w) - }; - } - - internal static OVRTracker.Frustum ToFrustum(this OVRPlugin.Frustumf f) - { - return new OVRTracker.Frustum() - { - nearZ = f.zNear, - farZ = f.zFar, - - fov = new Vector2() - { - x = Mathf.Rad2Deg * f.fovX, - y = Mathf.Rad2Deg * f.fovY - } - }; - } - - internal static Color FromColorf(this OVRPlugin.Colorf c) - { - return new Color() { r = c.r, g = c.g, b = c.b, a = c.a }; - } - - internal static OVRPlugin.Colorf ToColorf(this Color c) - { - return new OVRPlugin.Colorf() { r = c.r, g = c.g, b = c.b, a = c.a }; - } - - internal static Vector3 FromVector3f(this OVRPlugin.Vector3f v) - { - return new Vector3() { x = v.x, y = v.y, z = v.z }; - } - - internal static Vector3 FromFlippedZVector3f(this OVRPlugin.Vector3f v) - { - return new Vector3() { x = v.x, y = v.y, z = -v.z }; - } - - internal static OVRPlugin.Vector3f ToVector3f(this Vector3 v) - { - return new OVRPlugin.Vector3f() { x = v.x, y = v.y, z = v.z }; - } - - internal static OVRPlugin.Vector3f ToFlippedZVector3f(this Vector3 v) - { - return new OVRPlugin.Vector3f() { x = v.x, y = v.y, z = -v.z }; - } - - internal static Quaternion FromQuatf(this OVRPlugin.Quatf q) - { - return new Quaternion() { x = q.x, y = q.y, z = q.z, w = q.w }; - } - - internal static Quaternion FromFlippedZQuatf(this OVRPlugin.Quatf q) - { - return new Quaternion() { x = -q.x, y = -q.y, z = q.z, w = q.w }; - } - - internal static OVRPlugin.Quatf ToQuatf(this Quaternion q) - { - return new OVRPlugin.Quatf() { x = q.x, y = q.y, z = q.z, w = q.w }; - } - - internal static OVRPlugin.Quatf ToFlippedZQuatf(this Quaternion q) - { - return new OVRPlugin.Quatf() { x = -q.x, y = -q.y, z = q.z, w = q.w }; - } + /// + /// Converts the given world-space transform to an OVRPose in tracking space. + /// + public static OVRPose ToTrackingSpacePose(this Transform transform) + { + OVRPose headPose; + headPose.position = UnityEngine.XR.InputTracking.GetLocalPosition(UnityEngine.XR.XRNode.Head); + headPose.orientation = UnityEngine.XR.InputTracking.GetLocalRotation(UnityEngine.XR.XRNode.Head); + + var ret = headPose * transform.ToHeadSpacePose(); + + return ret; + } + + /// + /// Converts the given world-space transform to an OVRPose in head space. + /// + public static OVRPose ToHeadSpacePose(this Transform transform) + { + return Camera.current.transform.ToOVRPose().Inverse() * transform.ToOVRPose(); + } + + internal static OVRPose ToOVRPose(this Transform t, bool isLocal = false) + { + OVRPose pose; + pose.orientation = (isLocal) ? t.localRotation : t.rotation; + pose.position = (isLocal) ? t.localPosition : t.position; + return pose; + } + + internal static void FromOVRPose(this Transform t, OVRPose pose, bool isLocal = false) + { + if (isLocal) + { + t.localRotation = pose.orientation; + t.localPosition = pose.position; + } + else + { + t.rotation = pose.orientation; + t.position = pose.position; + } + } + + internal static OVRPose ToOVRPose(this OVRPlugin.Posef p) + { + return new OVRPose() + { + position = new Vector3(p.Position.x, p.Position.y, -p.Position.z), + orientation = new Quaternion(-p.Orientation.x, -p.Orientation.y, p.Orientation.z, p.Orientation.w) + }; + } + + internal static OVRTracker.Frustum ToFrustum(this OVRPlugin.Frustumf f) + { + return new OVRTracker.Frustum() + { + nearZ = f.zNear, + farZ = f.zFar, + + fov = new Vector2() + { + x = Mathf.Rad2Deg * f.fovX, + y = Mathf.Rad2Deg * f.fovY + } + }; + } + + internal static Color FromColorf(this OVRPlugin.Colorf c) + { + return new Color() { r = c.r, g = c.g, b = c.b, a = c.a }; + } + + internal static OVRPlugin.Colorf ToColorf(this Color c) + { + return new OVRPlugin.Colorf() { r = c.r, g = c.g, b = c.b, a = c.a }; + } + + internal static Vector3 FromVector3f(this OVRPlugin.Vector3f v) + { + return new Vector3() { x = v.x, y = v.y, z = v.z }; + } + + internal static Vector3 FromFlippedZVector3f(this OVRPlugin.Vector3f v) + { + return new Vector3() { x = v.x, y = v.y, z = -v.z }; + } + + internal static OVRPlugin.Vector3f ToVector3f(this Vector3 v) + { + return new OVRPlugin.Vector3f() { x = v.x, y = v.y, z = v.z }; + } + + internal static OVRPlugin.Vector3f ToFlippedZVector3f(this Vector3 v) + { + return new OVRPlugin.Vector3f() { x = v.x, y = v.y, z = -v.z }; + } + + internal static Quaternion FromQuatf(this OVRPlugin.Quatf q) + { + return new Quaternion() { x = q.x, y = q.y, z = q.z, w = q.w }; + } + + internal static Quaternion FromFlippedZQuatf(this OVRPlugin.Quatf q) + { + return new Quaternion() { x = -q.x, y = -q.y, z = q.z, w = q.w }; + } + + internal static OVRPlugin.Quatf ToQuatf(this Quaternion q) + { + return new OVRPlugin.Quatf() { x = q.x, y = q.y, z = q.z, w = q.w }; + } + + internal static OVRPlugin.Quatf ToFlippedZQuatf(this Quaternion q) + { + return new OVRPlugin.Quatf() { x = -q.x, y = -q.y, z = q.z, w = q.w }; + } } /// @@ -154,92 +154,93 @@ internal static OVRPlugin.Quatf ToFlippedZQuatf(this Quaternion q) [System.Serializable] public struct OVRPose { - /// - /// A pose with no translation or rotation. - /// - public static OVRPose identity - { - get { - return new OVRPose() - { - position = Vector3.zero, - orientation = Quaternion.identity - }; - } - } - - public override bool Equals(System.Object obj) - { - return obj is OVRPose && this == (OVRPose)obj; - } - - public override int GetHashCode() - { - return position.GetHashCode() ^ orientation.GetHashCode(); - } - - public static bool operator ==(OVRPose x, OVRPose y) - { - return x.position == y.position && x.orientation == y.orientation; - } - - public static bool operator !=(OVRPose x, OVRPose y) - { - return !(x == y); - } - - /// - /// The position. - /// - public Vector3 position; - - /// - /// The orientation. - /// - public Quaternion orientation; - - /// - /// Multiplies two poses. - /// - public static OVRPose operator*(OVRPose lhs, OVRPose rhs) - { - var ret = new OVRPose(); - ret.position = lhs.position + lhs.orientation * rhs.position; - ret.orientation = lhs.orientation * rhs.orientation; - return ret; - } - - /// - /// Computes the inverse of the given pose. - /// - public OVRPose Inverse() - { - OVRPose ret; - ret.orientation = Quaternion.Inverse(orientation); - ret.position = ret.orientation * -position; - return ret; - } - - /// - /// Converts the pose from left- to right-handed or vice-versa. - /// - internal OVRPose flipZ() - { - var ret = this; - ret.position.z = -ret.position.z; - ret.orientation.z = -ret.orientation.z; - ret.orientation.w = -ret.orientation.w; - return ret; - } - - internal OVRPlugin.Posef ToPosef() - { - return new OVRPlugin.Posef() - { - Position = position.ToVector3f(), - Orientation = orientation.ToQuatf() - }; - } + /// + /// A pose with no translation or rotation. + /// + public static OVRPose identity + { + get + { + return new OVRPose() + { + position = Vector3.zero, + orientation = Quaternion.identity + }; + } + } + + public override bool Equals(System.Object obj) + { + return obj is OVRPose && this == (OVRPose)obj; + } + + public override int GetHashCode() + { + return position.GetHashCode() ^ orientation.GetHashCode(); + } + + public static bool operator ==(OVRPose x, OVRPose y) + { + return x.position == y.position && x.orientation == y.orientation; + } + + public static bool operator !=(OVRPose x, OVRPose y) + { + return !(x == y); + } + + /// + /// The position. + /// + public Vector3 position; + + /// + /// The orientation. + /// + public Quaternion orientation; + + /// + /// Multiplies two poses. + /// + public static OVRPose operator *(OVRPose lhs, OVRPose rhs) + { + var ret = new OVRPose(); + ret.position = lhs.position + lhs.orientation * rhs.position; + ret.orientation = lhs.orientation * rhs.orientation; + return ret; + } + + /// + /// Computes the inverse of the given pose. + /// + public OVRPose Inverse() + { + OVRPose ret; + ret.orientation = Quaternion.Inverse(orientation); + ret.position = ret.orientation * -position; + return ret; + } + + /// + /// Converts the pose from left- to right-handed or vice-versa. + /// + internal OVRPose flipZ() + { + var ret = this; + ret.position.z = -ret.position.z; + ret.orientation.z = -ret.orientation.z; + ret.orientation.w = -ret.orientation.w; + return ret; + } + + internal OVRPlugin.Posef ToPosef() + { + return new OVRPlugin.Posef() + { + Position = position.ToVector3f(), + Orientation = orientation.ToQuatf() + }; + } } /// @@ -247,105 +248,105 @@ internal OVRPlugin.Posef ToPosef() /// public class OVRNativeBuffer : IDisposable { - private bool disposed = false; - private int m_numBytes = 0; - private IntPtr m_ptr = IntPtr.Zero; - - /// - /// Creates a buffer of the specified size. - /// - public OVRNativeBuffer(int numBytes) - { - Reallocate(numBytes); - } - - /// - /// Releases unmanaged resources and performs other cleanup operations before the is - /// reclaimed by garbage collection. - /// - ~OVRNativeBuffer() - { - Dispose(false); - } - - /// - /// Reallocates the buffer with the specified new size. - /// - public void Reset(int numBytes) - { - Reallocate(numBytes); - } - - /// - /// The current number of bytes in the buffer. - /// - public int GetCapacity() - { - return m_numBytes; - } - - /// - /// A pointer to the unmanaged memory in the buffer, starting at the given offset in bytes. - /// - public IntPtr GetPointer(int byteOffset = 0) - { - if (byteOffset < 0 || byteOffset >= m_numBytes) - return IntPtr.Zero; - return (byteOffset == 0) ? m_ptr : new IntPtr(m_ptr.ToInt64() + byteOffset); - } - - /// - /// Releases all resource used by the object. - /// - /// Call when you are finished using the . The - /// method leaves the in an unusable state. After calling , you must - /// release all references to the so the garbage collector can reclaim the memory that - /// the was occupying. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (disposed) - return; - - if (disposing) - { - // dispose managed resources - } - - // dispose unmanaged resources - Release(); - - disposed = true; - } - - private void Reallocate(int numBytes) - { - Release(); - - if (numBytes > 0) - { - m_ptr = Marshal.AllocHGlobal(numBytes); - m_numBytes = numBytes; - } - else - { - m_ptr = IntPtr.Zero; - m_numBytes = 0; - } - } - - private void Release() - { - if (m_ptr != IntPtr.Zero) - { - Marshal.FreeHGlobal(m_ptr); - m_ptr = IntPtr.Zero; - m_numBytes = 0; - } - } + private bool disposed = false; + private int m_numBytes = 0; + private IntPtr m_ptr = IntPtr.Zero; + + /// + /// Creates a buffer of the specified size. + /// + public OVRNativeBuffer(int numBytes) + { + Reallocate(numBytes); + } + + /// + /// Releases unmanaged resources and performs other cleanup operations before the is + /// reclaimed by garbage collection. + /// + ~OVRNativeBuffer() + { + Dispose(false); + } + + /// + /// Reallocates the buffer with the specified new size. + /// + public void Reset(int numBytes) + { + Reallocate(numBytes); + } + + /// + /// The current number of bytes in the buffer. + /// + public int GetCapacity() + { + return m_numBytes; + } + + /// + /// A pointer to the unmanaged memory in the buffer, starting at the given offset in bytes. + /// + public IntPtr GetPointer(int byteOffset = 0) + { + if (byteOffset < 0 || byteOffset >= m_numBytes) + return IntPtr.Zero; + return (byteOffset == 0) ? m_ptr : new IntPtr(m_ptr.ToInt64() + byteOffset); + } + + /// + /// Releases all resource used by the object. + /// + /// Call when you are finished using the . The + /// method leaves the in an unusable state. After calling , you must + /// release all references to the so the garbage collector can reclaim the memory that + /// the was occupying. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposed) + return; + + if (disposing) + { + // dispose managed resources + } + + // dispose unmanaged resources + Release(); + + disposed = true; + } + + private void Reallocate(int numBytes) + { + Release(); + + if (numBytes > 0) + { + m_ptr = Marshal.AllocHGlobal(numBytes); + m_numBytes = numBytes; + } + else + { + m_ptr = IntPtr.Zero; + m_numBytes = 0; + } + } + + private void Release() + { + if (m_ptr != IntPtr.Zero) + { + Marshal.FreeHGlobal(m_ptr); + m_ptr = IntPtr.Zero; + m_numBytes = 0; + } + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRDebugHeadController.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRDebugHeadController.cs index 4b6f3f4d..47af7855 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRDebugHeadController.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRDebugHeadController.cs @@ -37,85 +37,85 @@ limitations under the License. /// platform. /// In general, this behavior should be disabled when not debugging. /// -public class OVRDebugHeadController : MonoBehaviour +public class OVRDebugHeadController : MonoBehaviour { - [SerializeField] - public bool AllowPitchLook = false; - [SerializeField] - public bool AllowYawLook = true; - [SerializeField] - public bool InvertPitch = false; - [SerializeField] - public float GamePad_PitchDegreesPerSec = 90.0f; - [SerializeField] - public float GamePad_YawDegreesPerSec = 90.0f; - [SerializeField] - public bool AllowMovement = false; - [SerializeField] - public float ForwardSpeed = 2.0f; - [SerializeField] - public float StrafeSpeed = 2.0f; - - protected OVRCameraRig CameraRig = null; - - void Awake() - { - // locate the camera rig so we can use it to get the current camera transform each frame - OVRCameraRig[] CameraRigs = gameObject.GetComponentsInChildren(); - - if( CameraRigs.Length == 0 ) - Debug.LogWarning("OVRCamParent: No OVRCameraRig attached."); - else if (CameraRigs.Length > 1) - Debug.LogWarning("OVRCamParent: More then 1 OVRCameraRig attached."); - else - CameraRig = CameraRigs[0]; - } - - // Use this for initialization - void Start () - { - - } - - // Update is called once per frame - void Update () - { - if ( AllowMovement ) - { - float gamePad_FwdAxis = OVRInput.Get(OVRInput.RawAxis2D.LThumbstick).y; - float gamePad_StrafeAxis = OVRInput.Get(OVRInput.RawAxis2D.LThumbstick).x; - - Vector3 fwdMove = ( CameraRig.centerEyeAnchor.rotation * Vector3.forward ) * gamePad_FwdAxis * Time.deltaTime * ForwardSpeed; - Vector3 strafeMove = ( CameraRig.centerEyeAnchor.rotation * Vector3.right ) * gamePad_StrafeAxis * Time.deltaTime * StrafeSpeed; - transform.position += fwdMove + strafeMove; - } - - if ( !UnityEngine.XR.XRDevice.isPresent && ( AllowYawLook || AllowPitchLook ) ) - { - Quaternion r = transform.rotation; - if ( AllowYawLook ) - { - float gamePadYaw = OVRInput.Get(OVRInput.RawAxis2D.RThumbstick).x; - float yawAmount = gamePadYaw * Time.deltaTime * GamePad_YawDegreesPerSec; - Quaternion yawRot = Quaternion.AngleAxis( yawAmount, Vector3.up ); - r = yawRot * r; - } - if ( AllowPitchLook ) - { - float gamePadPitch = OVRInput.Get(OVRInput.RawAxis2D.RThumbstick).y; - if ( Mathf.Abs( gamePadPitch ) > 0.0001f ) - { - if ( InvertPitch ) - { - gamePadPitch *= -1.0f; - } - float pitchAmount = gamePadPitch * Time.deltaTime * GamePad_PitchDegreesPerSec; - Quaternion pitchRot = Quaternion.AngleAxis( pitchAmount, Vector3.left ); - r = r * pitchRot; - } - } - - transform.rotation = r; - } - } + [SerializeField] + public bool AllowPitchLook = false; + [SerializeField] + public bool AllowYawLook = true; + [SerializeField] + public bool InvertPitch = false; + [SerializeField] + public float GamePad_PitchDegreesPerSec = 90.0f; + [SerializeField] + public float GamePad_YawDegreesPerSec = 90.0f; + [SerializeField] + public bool AllowMovement = false; + [SerializeField] + public float ForwardSpeed = 2.0f; + [SerializeField] + public float StrafeSpeed = 2.0f; + + protected OVRCameraRig CameraRig = null; + + void Awake() + { + // locate the camera rig so we can use it to get the current camera transform each frame + OVRCameraRig[] CameraRigs = gameObject.GetComponentsInChildren(); + + if (CameraRigs.Length == 0) + Debug.LogWarning("OVRCamParent: No OVRCameraRig attached."); + else if (CameraRigs.Length > 1) + Debug.LogWarning("OVRCamParent: More then 1 OVRCameraRig attached."); + else + CameraRig = CameraRigs[0]; + } + + // Use this for initialization + void Start() + { + + } + + // Update is called once per frame + void Update() + { + if (AllowMovement) + { + float gamePad_FwdAxis = OVRInput.Get(OVRInput.RawAxis2D.LThumbstick).y; + float gamePad_StrafeAxis = OVRInput.Get(OVRInput.RawAxis2D.LThumbstick).x; + + Vector3 fwdMove = (CameraRig.centerEyeAnchor.rotation * Vector3.forward) * gamePad_FwdAxis * Time.deltaTime * ForwardSpeed; + Vector3 strafeMove = (CameraRig.centerEyeAnchor.rotation * Vector3.right) * gamePad_StrafeAxis * Time.deltaTime * StrafeSpeed; + transform.position += fwdMove + strafeMove; + } + + if (!UnityEngine.XR.XRDevice.isPresent && (AllowYawLook || AllowPitchLook)) + { + Quaternion r = transform.rotation; + if (AllowYawLook) + { + float gamePadYaw = OVRInput.Get(OVRInput.RawAxis2D.RThumbstick).x; + float yawAmount = gamePadYaw * Time.deltaTime * GamePad_YawDegreesPerSec; + Quaternion yawRot = Quaternion.AngleAxis(yawAmount, Vector3.up); + r = yawRot * r; + } + if (AllowPitchLook) + { + float gamePadPitch = OVRInput.Get(OVRInput.RawAxis2D.RThumbstick).y; + if (Mathf.Abs(gamePadPitch) > 0.0001f) + { + if (InvertPitch) + { + gamePadPitch *= -1.0f; + } + float pitchAmount = gamePadPitch * Time.deltaTime * GamePad_PitchDegreesPerSec; + Quaternion pitchRot = Quaternion.AngleAxis(pitchAmount, Vector3.left); + r = r * pitchRot; + } + } + + transform.rotation = r; + } + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRDisplay.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRDisplay.cs index bd08c121..03f8562a 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRDisplay.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRDisplay.cs @@ -30,94 +30,95 @@ limitations under the License. /// public class OVRDisplay { - /// - /// Specifies the size and field-of-view for one eye texture. - /// - public struct EyeRenderDesc - { - /// - /// The horizontal and vertical size of the texture. - /// - public Vector2 resolution; - - /// - /// The angle of the horizontal and vertical field of view in degrees. - /// - public Vector2 fov; - } - - /// - /// Contains latency measurements for a single frame of rendering. - /// - public struct LatencyData - { - /// - /// The time it took to render both eyes in seconds. - /// - public float render; - - /// - /// The time it took to perform TimeWarp in seconds. - /// - public float timeWarp; - - /// - /// The time between the end of TimeWarp and scan-out in seconds. - /// - public float postPresent; - public float renderError; - public float timeWarpError; - } - - private bool needsConfigureTexture; - private EyeRenderDesc[] eyeDescs = new EyeRenderDesc[2]; - - /// - /// Creates an instance of OVRDisplay. Called by OVRManager. - /// - public OVRDisplay() - { - UpdateTextures(); - } - - /// - /// Updates the internal state of the OVRDisplay. Called by OVRManager. - /// - public void Update() - { - UpdateTextures(); - } - - /// - /// Occurs when the head pose is reset. - /// - public event System.Action RecenteredPose; - - /// - /// Recenters the head pose. - /// - public void RecenterPose() - { + /// + /// Specifies the size and field-of-view for one eye texture. + /// + public struct EyeRenderDesc + { + /// + /// The horizontal and vertical size of the texture. + /// + public Vector2 resolution; + + /// + /// The angle of the horizontal and vertical field of view in degrees. + /// + public Vector2 fov; + } + + /// + /// Contains latency measurements for a single frame of rendering. + /// + public struct LatencyData + { + /// + /// The time it took to render both eyes in seconds. + /// + public float render; + + /// + /// The time it took to perform TimeWarp in seconds. + /// + public float timeWarp; + + /// + /// The time between the end of TimeWarp and scan-out in seconds. + /// + public float postPresent; + public float renderError; + public float timeWarpError; + } + + private bool needsConfigureTexture; + private EyeRenderDesc[] eyeDescs = new EyeRenderDesc[2]; + + /// + /// Creates an instance of OVRDisplay. Called by OVRManager. + /// + public OVRDisplay() + { + UpdateTextures(); + } + + /// + /// Updates the internal state of the OVRDisplay. Called by OVRManager. + /// + public void Update() + { + UpdateTextures(); + } + + /// + /// Occurs when the head pose is reset. + /// + public event System.Action RecenteredPose; + + /// + /// Recenters the head pose. + /// + public void RecenterPose() + { UnityEngine.XR.InputTracking.Recenter(); - if (RecenteredPose != null) - { - RecenteredPose(); - } - } - - /// - /// Gets the current linear acceleration of the head. - /// - public Vector3 acceleration - { - get { - if (!OVRManager.isHmdPresent) - return Vector3.zero; - - return OVRPlugin.GetNodeAcceleration(OVRPlugin.Node.None, OVRPlugin.Step.Render).FromFlippedZVector3f(); - } - } + if (RecenteredPose != null) + { + RecenteredPose(); + } + } + + /// + /// Gets the current linear acceleration of the head. + /// + public Vector3 acceleration + { + get + { + if (!OVRManager.isHmdPresent) + return Vector3.zero; + + return OVRPlugin.GetNodeAcceleration(OVRPlugin.Node.None, OVRPlugin.Step.Render).FromFlippedZVector3f(); + } + } /// /// Gets the current angular acceleration of the head. @@ -127,9 +128,9 @@ public Vector3 angularAcceleration get { if (!OVRManager.isHmdPresent) - return Vector3.zero; + return Vector3.zero; - return OVRPlugin.GetNodeAngularAcceleration(OVRPlugin.Node.None, OVRPlugin.Step.Render).FromFlippedZVector3f() * Mathf.Rad2Deg; + return OVRPlugin.GetNodeAngularAcceleration(OVRPlugin.Node.None, OVRPlugin.Step.Render).FromFlippedZVector3f() * Mathf.Rad2Deg; } } @@ -143,39 +144,41 @@ public Vector3 velocity if (!OVRManager.isHmdPresent) return Vector3.zero; - return OVRPlugin.GetNodeVelocity(OVRPlugin.Node.None, OVRPlugin.Step.Render).FromFlippedZVector3f(); + return OVRPlugin.GetNodeVelocity(OVRPlugin.Node.None, OVRPlugin.Step.Render).FromFlippedZVector3f(); } } - - /// - /// Gets the current angular velocity of the head. - /// - public Vector3 angularVelocity - { - get { - if (!OVRManager.isHmdPresent) - return Vector3.zero; - - return OVRPlugin.GetNodeAngularVelocity(OVRPlugin.Node.None, OVRPlugin.Step.Render).FromFlippedZVector3f() * Mathf.Rad2Deg; - } - } - - /// - /// Gets the resolution and field of view for the given eye. - /// + + /// + /// Gets the current angular velocity of the head. + /// + public Vector3 angularVelocity + { + get + { + if (!OVRManager.isHmdPresent) + return Vector3.zero; + + return OVRPlugin.GetNodeAngularVelocity(OVRPlugin.Node.None, OVRPlugin.Step.Render).FromFlippedZVector3f() * Mathf.Rad2Deg; + } + } + + /// + /// Gets the resolution and field of view for the given eye. + /// public EyeRenderDesc GetEyeRenderDesc(UnityEngine.XR.XRNode eye) - { - return eyeDescs[(int)eye]; - } - - /// - /// Gets the current measured latency values. - /// - public LatencyData latency - { - get { - if (!OVRManager.isHmdPresent) - return new LatencyData(); + { + return eyeDescs[(int)eye]; + } + + /// + /// Gets the current measured latency values. + /// + public LatencyData latency + { + get + { + if (!OVRManager.isHmdPresent) + return new LatencyData(); string latency = OVRPlugin.latency; @@ -188,61 +191,61 @@ public LatencyData latency { ret.render = float.Parse(match.Groups[1].Value); ret.timeWarp = float.Parse(match.Groups[2].Value); - ret.postPresent = float.Parse(match.Groups[3].Value); + ret.postPresent = float.Parse(match.Groups[3].Value); } return ret; - } - } - - /// - /// Gets application's frame rate reported by oculus plugin - /// - public float appFramerate - { - get - { - if (!OVRManager.isHmdPresent) - return 0; - - return OVRPlugin.GetAppFramerate(); - } - } - - /// - /// Gets the recommended MSAA level for optimal quality/performance the current device. - /// - public int recommendedMSAALevel - { - get - { - int result = OVRPlugin.recommendedMSAALevel; - - if (result == 1) - result = 0; - - return result; - } - } - - private void UpdateTextures() - { - ConfigureEyeDesc(UnityEngine.XR.XRNode.LeftEye); + } + } + + /// + /// Gets application's frame rate reported by oculus plugin + /// + public float appFramerate + { + get + { + if (!OVRManager.isHmdPresent) + return 0; + + return OVRPlugin.GetAppFramerate(); + } + } + + /// + /// Gets the recommended MSAA level for optimal quality/performance the current device. + /// + public int recommendedMSAALevel + { + get + { + int result = OVRPlugin.recommendedMSAALevel; + + if (result == 1) + result = 0; + + return result; + } + } + + private void UpdateTextures() + { + ConfigureEyeDesc(UnityEngine.XR.XRNode.LeftEye); ConfigureEyeDesc(UnityEngine.XR.XRNode.RightEye); - } + } private void ConfigureEyeDesc(UnityEngine.XR.XRNode eye) - { - if (!OVRManager.isHmdPresent) - return; + { + if (!OVRManager.isHmdPresent) + return; - OVRPlugin.Sizei size = OVRPlugin.GetEyeTextureSize((OVRPlugin.Eye)eye); - OVRPlugin.Frustumf frust = OVRPlugin.GetEyeFrustum((OVRPlugin.Eye)eye); + OVRPlugin.Sizei size = OVRPlugin.GetEyeTextureSize((OVRPlugin.Eye)eye); + OVRPlugin.Frustumf frust = OVRPlugin.GetEyeFrustum((OVRPlugin.Eye)eye); - eyeDescs[(int)eye] = new EyeRenderDesc() - { - resolution = new Vector2(size.w, size.h), + eyeDescs[(int)eye] = new EyeRenderDesc() + { + resolution = new Vector2(size.w, size.h), fov = Mathf.Rad2Deg * new Vector2(frust.fovX, frust.fovY), - }; - } + }; + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRHaptics.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRHaptics.cs index fbbf8192..a8bf2c7d 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRHaptics.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRHaptics.cs @@ -10,357 +10,357 @@ /// public static class OVRHaptics { - public readonly static OVRHapticsChannel[] Channels; - public readonly static OVRHapticsChannel LeftChannel; - public readonly static OVRHapticsChannel RightChannel; - - private readonly static OVRHapticsOutput[] m_outputs; - - static OVRHaptics() - { - Config.Load(); - - m_outputs = new OVRHapticsOutput[] - { - new OVRHapticsOutput((uint)OVRPlugin.Controller.LTouch), - new OVRHapticsOutput((uint)OVRPlugin.Controller.RTouch), - }; - - Channels = new OVRHapticsChannel[] - { - LeftChannel = new OVRHapticsChannel(0), - RightChannel = new OVRHapticsChannel(1), - }; - } - - /// - /// Determines the target format for haptics data on a specific device. - /// - public static class Config - { - public static int SampleRateHz { get; private set; } - public static int SampleSizeInBytes { get; private set; } - public static int MinimumSafeSamplesQueued { get; private set; } - public static int MinimumBufferSamplesCount { get; private set; } - public static int OptimalBufferSamplesCount { get; private set; } - public static int MaximumBufferSamplesCount { get; private set; } - - static Config() - { - Load(); - } - - public static void Load() - { - OVRPlugin.HapticsDesc desc = OVRPlugin.GetControllerHapticsDesc((uint)OVRPlugin.Controller.RTouch); - - SampleRateHz = desc.SampleRateHz; - SampleSizeInBytes = desc.SampleSizeInBytes; - MinimumSafeSamplesQueued = desc.MinimumSafeSamplesQueued; - MinimumBufferSamplesCount = desc.MinimumBufferSamplesCount; - OptimalBufferSamplesCount = desc.OptimalBufferSamplesCount; - MaximumBufferSamplesCount = desc.MaximumBufferSamplesCount; - } - } - - /// - /// A track of haptics data that can be mixed or sequenced with another track. - /// - public class OVRHapticsChannel - { - private OVRHapticsOutput m_output; - - /// - /// Constructs a channel targeting the specified output. - /// - public OVRHapticsChannel(uint outputIndex) - { - m_output = m_outputs[outputIndex]; - } - - /// - /// Cancels any currently-playing clips and immediatly plays the specified clip instead. - /// - public void Preempt(OVRHapticsClip clip) - { - m_output.Preempt(clip); - } - - /// - /// Enqueues the specified clip to play after any currently-playing clips finish. - /// - public void Queue(OVRHapticsClip clip) - { - m_output.Queue(clip); - } - - /// - /// Adds the specified clip to play simultaneously to the currently-playing clip(s). - /// - public void Mix(OVRHapticsClip clip) - { - m_output.Mix(clip); - } - - /// - /// Cancels any currently-playing clips. - /// - public void Clear() - { - m_output.Clear(); - } - } - - private class OVRHapticsOutput - { - private class ClipPlaybackTracker - { - public int ReadCount { get; set; } - public OVRHapticsClip Clip { get; set; } - - public ClipPlaybackTracker(OVRHapticsClip clip) - { - Clip = clip; - } - } - - private bool m_lowLatencyMode = true; - private int m_prevSamplesQueued = 0; - private float m_prevSamplesQueuedTime = 0; - private int m_numPredictionHits = 0; - private int m_numPredictionMisses = 0; - private int m_numUnderruns = 0; - private List m_pendingClips = new List(); - private uint m_controller = 0; - private OVRNativeBuffer m_nativeBuffer = new OVRNativeBuffer(OVRHaptics.Config.MaximumBufferSamplesCount * OVRHaptics.Config.SampleSizeInBytes); - private OVRHapticsClip m_paddingClip = new OVRHapticsClip(); - - public OVRHapticsOutput(uint controller) - { - m_controller = controller; - } - - /// - /// The system calls this each frame to update haptics playback. - /// - public void Process() - { - var hapticsState = OVRPlugin.GetControllerHapticsState(m_controller); - - float elapsedTime = Time.realtimeSinceStartup - m_prevSamplesQueuedTime; - if (m_prevSamplesQueued > 0) - { - int expectedSamples = m_prevSamplesQueued - (int)(elapsedTime * OVRHaptics.Config.SampleRateHz + 0.5f); - if (expectedSamples < 0) - expectedSamples = 0; - - if ((hapticsState.SamplesQueued - expectedSamples) == 0) - m_numPredictionHits++; - else - m_numPredictionMisses++; - - //Debug.Log(hapticsState.SamplesAvailable + "a " + hapticsState.SamplesQueued + "q " + expectedSamples + "e " - //+ "Prediction Accuracy: " + m_numPredictionHits / (float)(m_numPredictionMisses + m_numPredictionHits)); - - if ((expectedSamples > 0) && (hapticsState.SamplesQueued == 0)) - { - m_numUnderruns++; - //Debug.LogError("Samples Underrun (" + m_controller + " #" + m_numUnderruns + ") -" - // + " Expected: " + expectedSamples - // + " Actual: " + hapticsState.SamplesQueued); - } - - m_prevSamplesQueued = hapticsState.SamplesQueued; - m_prevSamplesQueuedTime = Time.realtimeSinceStartup; - } - - int desiredSamplesCount = OVRHaptics.Config.OptimalBufferSamplesCount; - if (m_lowLatencyMode) - { - float sampleRateMs = 1000.0f / (float)OVRHaptics.Config.SampleRateHz; - float elapsedMs = elapsedTime * 1000.0f; - int samplesNeededPerFrame = (int)Mathf.Ceil(elapsedMs / sampleRateMs); - int lowLatencySamplesCount = OVRHaptics.Config.MinimumSafeSamplesQueued + samplesNeededPerFrame; - - if (lowLatencySamplesCount < desiredSamplesCount) - desiredSamplesCount = lowLatencySamplesCount; - } - - if (hapticsState.SamplesQueued > desiredSamplesCount) - return; - - if (desiredSamplesCount > OVRHaptics.Config.MaximumBufferSamplesCount) - desiredSamplesCount = OVRHaptics.Config.MaximumBufferSamplesCount; - if (desiredSamplesCount > hapticsState.SamplesAvailable) - desiredSamplesCount = hapticsState.SamplesAvailable; - - int acquiredSamplesCount = 0; - int clipIndex = 0; - while(acquiredSamplesCount < desiredSamplesCount && clipIndex < m_pendingClips.Count) - { - int numSamplesToCopy = desiredSamplesCount - acquiredSamplesCount; - int remainingSamplesInClip = m_pendingClips[clipIndex].Clip.Count - m_pendingClips[clipIndex].ReadCount; - if (numSamplesToCopy > remainingSamplesInClip) - numSamplesToCopy = remainingSamplesInClip; - - if (numSamplesToCopy > 0) - { - int numBytes = numSamplesToCopy * OVRHaptics.Config.SampleSizeInBytes; - int dstOffset = acquiredSamplesCount * OVRHaptics.Config.SampleSizeInBytes; - int srcOffset = m_pendingClips[clipIndex].ReadCount * OVRHaptics.Config.SampleSizeInBytes; - Marshal.Copy(m_pendingClips[clipIndex].Clip.Samples, srcOffset, m_nativeBuffer.GetPointer(dstOffset), numBytes); - - m_pendingClips[clipIndex].ReadCount += numSamplesToCopy; - acquiredSamplesCount += numSamplesToCopy; - } - - clipIndex++; - } - - for (int i = m_pendingClips.Count - 1; i >= 0 && m_pendingClips.Count > 0; i--) - { - if (m_pendingClips[i].ReadCount >= m_pendingClips[i].Clip.Count) - m_pendingClips.RemoveAt(i); - } - - int desiredPadding = desiredSamplesCount - (hapticsState.SamplesQueued + acquiredSamplesCount); - if (desiredPadding < (OVRHaptics.Config.MinimumBufferSamplesCount - acquiredSamplesCount)) - desiredPadding = (OVRHaptics.Config.MinimumBufferSamplesCount - acquiredSamplesCount); - if (desiredPadding > hapticsState.SamplesAvailable) - desiredPadding = hapticsState.SamplesAvailable; - - if (desiredPadding > 0) - { - int numBytes = desiredPadding * OVRHaptics.Config.SampleSizeInBytes; - int dstOffset = acquiredSamplesCount * OVRHaptics.Config.SampleSizeInBytes; - int srcOffset = 0; - Marshal.Copy(m_paddingClip.Samples, srcOffset, m_nativeBuffer.GetPointer(dstOffset), numBytes); - - acquiredSamplesCount += desiredPadding; - } - - if (acquiredSamplesCount > 0) - { - OVRPlugin.HapticsBuffer hapticsBuffer; - hapticsBuffer.Samples = m_nativeBuffer.GetPointer(); - hapticsBuffer.SamplesCount = acquiredSamplesCount; - - OVRPlugin.SetControllerHaptics(m_controller, hapticsBuffer); - - hapticsState = OVRPlugin.GetControllerHapticsState(m_controller); - m_prevSamplesQueued = hapticsState.SamplesQueued; - m_prevSamplesQueuedTime = Time.realtimeSinceStartup; - } - } - - /// - /// Immediately plays the specified clip without waiting for any currently-playing clip to finish. - /// - public void Preempt(OVRHapticsClip clip) - { - m_pendingClips.Clear(); - m_pendingClips.Add(new ClipPlaybackTracker(clip)); - } - - /// - /// Enqueues the specified clip to play after any currently-playing clip finishes. - /// - public void Queue(OVRHapticsClip clip) - { - m_pendingClips.Add(new ClipPlaybackTracker(clip)); - } - - /// - /// Adds the samples from the specified clip to the ones in the currently-playing clip(s). - /// - public void Mix(OVRHapticsClip clip) - { - int numClipsToMix = 0; - int numSamplesToMix = 0; - int numSamplesRemaining = clip.Count; - - while (numSamplesRemaining > 0 && numClipsToMix < m_pendingClips.Count) - { - int numSamplesRemainingInClip = m_pendingClips[numClipsToMix].Clip.Count - m_pendingClips[numClipsToMix].ReadCount; - numSamplesRemaining -= numSamplesRemainingInClip; - numSamplesToMix += numSamplesRemainingInClip; - numClipsToMix++; - } - - if (numSamplesRemaining > 0) - { - numSamplesToMix += numSamplesRemaining; - numSamplesRemaining = 0; - } - - if (numClipsToMix > 0) - { - OVRHapticsClip mixClip = new OVRHapticsClip(numSamplesToMix); - - OVRHapticsClip a = clip; - int aReadCount = 0; - - for (int i = 0; i < numClipsToMix; i++) - { - OVRHapticsClip b = m_pendingClips[i].Clip; - for(int bReadCount = m_pendingClips[i].ReadCount; bReadCount < b.Count; bReadCount++) - { - if (OVRHaptics.Config.SampleSizeInBytes == 1) - { - byte sample = 0; // TODO support multi-byte samples - if ((aReadCount < a.Count) && (bReadCount < b.Count)) - { - sample = (byte)(Mathf.Clamp(a.Samples[aReadCount] + b.Samples[bReadCount], 0, System.Byte.MaxValue)); // TODO support multi-byte samples - aReadCount++; - } - else if (bReadCount < b.Count) - { - sample = b.Samples[bReadCount]; // TODO support multi-byte samples - } - - mixClip.WriteSample(sample); // TODO support multi-byte samples - } - } - } - - while (aReadCount < a.Count) - { - if (OVRHaptics.Config.SampleSizeInBytes == 1) - { - mixClip.WriteSample(a.Samples[aReadCount]); // TODO support multi-byte samples - } - aReadCount++; - } - - m_pendingClips[0] = new ClipPlaybackTracker(mixClip); - for (int i = 1; i < numClipsToMix; i++) - { - m_pendingClips.RemoveAt(1); - } - } - else - { - m_pendingClips.Add(new ClipPlaybackTracker(clip)); - } - } - - public void Clear() - { - m_pendingClips.Clear(); - } - } - - /// - /// The system calls this each frame to update haptics playback. - /// - public static void Process() - { - Config.Load(); - - for (int i = 0; i < m_outputs.Length; i++) - { - m_outputs[i].Process(); - } - } + public readonly static OVRHapticsChannel[] Channels; + public readonly static OVRHapticsChannel LeftChannel; + public readonly static OVRHapticsChannel RightChannel; + + private readonly static OVRHapticsOutput[] m_outputs; + + static OVRHaptics() + { + Config.Load(); + + m_outputs = new OVRHapticsOutput[] + { + new OVRHapticsOutput((uint)OVRPlugin.Controller.LTouch), + new OVRHapticsOutput((uint)OVRPlugin.Controller.RTouch), + }; + + Channels = new OVRHapticsChannel[] + { + LeftChannel = new OVRHapticsChannel(0), + RightChannel = new OVRHapticsChannel(1), + }; + } + + /// + /// Determines the target format for haptics data on a specific device. + /// + public static class Config + { + public static int SampleRateHz { get; private set; } + public static int SampleSizeInBytes { get; private set; } + public static int MinimumSafeSamplesQueued { get; private set; } + public static int MinimumBufferSamplesCount { get; private set; } + public static int OptimalBufferSamplesCount { get; private set; } + public static int MaximumBufferSamplesCount { get; private set; } + + static Config() + { + Load(); + } + + public static void Load() + { + OVRPlugin.HapticsDesc desc = OVRPlugin.GetControllerHapticsDesc((uint)OVRPlugin.Controller.RTouch); + + SampleRateHz = desc.SampleRateHz; + SampleSizeInBytes = desc.SampleSizeInBytes; + MinimumSafeSamplesQueued = desc.MinimumSafeSamplesQueued; + MinimumBufferSamplesCount = desc.MinimumBufferSamplesCount; + OptimalBufferSamplesCount = desc.OptimalBufferSamplesCount; + MaximumBufferSamplesCount = desc.MaximumBufferSamplesCount; + } + } + + /// + /// A track of haptics data that can be mixed or sequenced with another track. + /// + public class OVRHapticsChannel + { + private OVRHapticsOutput m_output; + + /// + /// Constructs a channel targeting the specified output. + /// + public OVRHapticsChannel(uint outputIndex) + { + m_output = m_outputs[outputIndex]; + } + + /// + /// Cancels any currently-playing clips and immediatly plays the specified clip instead. + /// + public void Preempt(OVRHapticsClip clip) + { + m_output.Preempt(clip); + } + + /// + /// Enqueues the specified clip to play after any currently-playing clips finish. + /// + public void Queue(OVRHapticsClip clip) + { + m_output.Queue(clip); + } + + /// + /// Adds the specified clip to play simultaneously to the currently-playing clip(s). + /// + public void Mix(OVRHapticsClip clip) + { + m_output.Mix(clip); + } + + /// + /// Cancels any currently-playing clips. + /// + public void Clear() + { + m_output.Clear(); + } + } + + private class OVRHapticsOutput + { + private class ClipPlaybackTracker + { + public int ReadCount { get; set; } + public OVRHapticsClip Clip { get; set; } + + public ClipPlaybackTracker(OVRHapticsClip clip) + { + Clip = clip; + } + } + + private bool m_lowLatencyMode = true; + private int m_prevSamplesQueued = 0; + private float m_prevSamplesQueuedTime = 0; + private int m_numPredictionHits = 0; + private int m_numPredictionMisses = 0; + private int m_numUnderruns = 0; + private List m_pendingClips = new List(); + private uint m_controller = 0; + private OVRNativeBuffer m_nativeBuffer = new OVRNativeBuffer(OVRHaptics.Config.MaximumBufferSamplesCount * OVRHaptics.Config.SampleSizeInBytes); + private OVRHapticsClip m_paddingClip = new OVRHapticsClip(); + + public OVRHapticsOutput(uint controller) + { + m_controller = controller; + } + + /// + /// The system calls this each frame to update haptics playback. + /// + public void Process() + { + var hapticsState = OVRPlugin.GetControllerHapticsState(m_controller); + + float elapsedTime = Time.realtimeSinceStartup - m_prevSamplesQueuedTime; + if (m_prevSamplesQueued > 0) + { + int expectedSamples = m_prevSamplesQueued - (int)(elapsedTime * OVRHaptics.Config.SampleRateHz + 0.5f); + if (expectedSamples < 0) + expectedSamples = 0; + + if ((hapticsState.SamplesQueued - expectedSamples) == 0) + m_numPredictionHits++; + else + m_numPredictionMisses++; + + //Debug.Log(hapticsState.SamplesAvailable + "a " + hapticsState.SamplesQueued + "q " + expectedSamples + "e " + //+ "Prediction Accuracy: " + m_numPredictionHits / (float)(m_numPredictionMisses + m_numPredictionHits)); + + if ((expectedSamples > 0) && (hapticsState.SamplesQueued == 0)) + { + m_numUnderruns++; + //Debug.LogError("Samples Underrun (" + m_controller + " #" + m_numUnderruns + ") -" + // + " Expected: " + expectedSamples + // + " Actual: " + hapticsState.SamplesQueued); + } + + m_prevSamplesQueued = hapticsState.SamplesQueued; + m_prevSamplesQueuedTime = Time.realtimeSinceStartup; + } + + int desiredSamplesCount = OVRHaptics.Config.OptimalBufferSamplesCount; + if (m_lowLatencyMode) + { + float sampleRateMs = 1000.0f / (float)OVRHaptics.Config.SampleRateHz; + float elapsedMs = elapsedTime * 1000.0f; + int samplesNeededPerFrame = (int)Mathf.Ceil(elapsedMs / sampleRateMs); + int lowLatencySamplesCount = OVRHaptics.Config.MinimumSafeSamplesQueued + samplesNeededPerFrame; + + if (lowLatencySamplesCount < desiredSamplesCount) + desiredSamplesCount = lowLatencySamplesCount; + } + + if (hapticsState.SamplesQueued > desiredSamplesCount) + return; + + if (desiredSamplesCount > OVRHaptics.Config.MaximumBufferSamplesCount) + desiredSamplesCount = OVRHaptics.Config.MaximumBufferSamplesCount; + if (desiredSamplesCount > hapticsState.SamplesAvailable) + desiredSamplesCount = hapticsState.SamplesAvailable; + + int acquiredSamplesCount = 0; + int clipIndex = 0; + while (acquiredSamplesCount < desiredSamplesCount && clipIndex < m_pendingClips.Count) + { + int numSamplesToCopy = desiredSamplesCount - acquiredSamplesCount; + int remainingSamplesInClip = m_pendingClips[clipIndex].Clip.Count - m_pendingClips[clipIndex].ReadCount; + if (numSamplesToCopy > remainingSamplesInClip) + numSamplesToCopy = remainingSamplesInClip; + + if (numSamplesToCopy > 0) + { + int numBytes = numSamplesToCopy * OVRHaptics.Config.SampleSizeInBytes; + int dstOffset = acquiredSamplesCount * OVRHaptics.Config.SampleSizeInBytes; + int srcOffset = m_pendingClips[clipIndex].ReadCount * OVRHaptics.Config.SampleSizeInBytes; + Marshal.Copy(m_pendingClips[clipIndex].Clip.Samples, srcOffset, m_nativeBuffer.GetPointer(dstOffset), numBytes); + + m_pendingClips[clipIndex].ReadCount += numSamplesToCopy; + acquiredSamplesCount += numSamplesToCopy; + } + + clipIndex++; + } + + for (int i = m_pendingClips.Count - 1; i >= 0 && m_pendingClips.Count > 0; i--) + { + if (m_pendingClips[i].ReadCount >= m_pendingClips[i].Clip.Count) + m_pendingClips.RemoveAt(i); + } + + int desiredPadding = desiredSamplesCount - (hapticsState.SamplesQueued + acquiredSamplesCount); + if (desiredPadding < (OVRHaptics.Config.MinimumBufferSamplesCount - acquiredSamplesCount)) + desiredPadding = (OVRHaptics.Config.MinimumBufferSamplesCount - acquiredSamplesCount); + if (desiredPadding > hapticsState.SamplesAvailable) + desiredPadding = hapticsState.SamplesAvailable; + + if (desiredPadding > 0) + { + int numBytes = desiredPadding * OVRHaptics.Config.SampleSizeInBytes; + int dstOffset = acquiredSamplesCount * OVRHaptics.Config.SampleSizeInBytes; + int srcOffset = 0; + Marshal.Copy(m_paddingClip.Samples, srcOffset, m_nativeBuffer.GetPointer(dstOffset), numBytes); + + acquiredSamplesCount += desiredPadding; + } + + if (acquiredSamplesCount > 0) + { + OVRPlugin.HapticsBuffer hapticsBuffer; + hapticsBuffer.Samples = m_nativeBuffer.GetPointer(); + hapticsBuffer.SamplesCount = acquiredSamplesCount; + + OVRPlugin.SetControllerHaptics(m_controller, hapticsBuffer); + + hapticsState = OVRPlugin.GetControllerHapticsState(m_controller); + m_prevSamplesQueued = hapticsState.SamplesQueued; + m_prevSamplesQueuedTime = Time.realtimeSinceStartup; + } + } + + /// + /// Immediately plays the specified clip without waiting for any currently-playing clip to finish. + /// + public void Preempt(OVRHapticsClip clip) + { + m_pendingClips.Clear(); + m_pendingClips.Add(new ClipPlaybackTracker(clip)); + } + + /// + /// Enqueues the specified clip to play after any currently-playing clip finishes. + /// + public void Queue(OVRHapticsClip clip) + { + m_pendingClips.Add(new ClipPlaybackTracker(clip)); + } + + /// + /// Adds the samples from the specified clip to the ones in the currently-playing clip(s). + /// + public void Mix(OVRHapticsClip clip) + { + int numClipsToMix = 0; + int numSamplesToMix = 0; + int numSamplesRemaining = clip.Count; + + while (numSamplesRemaining > 0 && numClipsToMix < m_pendingClips.Count) + { + int numSamplesRemainingInClip = m_pendingClips[numClipsToMix].Clip.Count - m_pendingClips[numClipsToMix].ReadCount; + numSamplesRemaining -= numSamplesRemainingInClip; + numSamplesToMix += numSamplesRemainingInClip; + numClipsToMix++; + } + + if (numSamplesRemaining > 0) + { + numSamplesToMix += numSamplesRemaining; + numSamplesRemaining = 0; + } + + if (numClipsToMix > 0) + { + OVRHapticsClip mixClip = new OVRHapticsClip(numSamplesToMix); + + OVRHapticsClip a = clip; + int aReadCount = 0; + + for (int i = 0; i < numClipsToMix; i++) + { + OVRHapticsClip b = m_pendingClips[i].Clip; + for (int bReadCount = m_pendingClips[i].ReadCount; bReadCount < b.Count; bReadCount++) + { + if (OVRHaptics.Config.SampleSizeInBytes == 1) + { + byte sample = 0; // TODO support multi-byte samples + if ((aReadCount < a.Count) && (bReadCount < b.Count)) + { + sample = (byte)(Mathf.Clamp(a.Samples[aReadCount] + b.Samples[bReadCount], 0, System.Byte.MaxValue)); // TODO support multi-byte samples + aReadCount++; + } + else if (bReadCount < b.Count) + { + sample = b.Samples[bReadCount]; // TODO support multi-byte samples + } + + mixClip.WriteSample(sample); // TODO support multi-byte samples + } + } + } + + while (aReadCount < a.Count) + { + if (OVRHaptics.Config.SampleSizeInBytes == 1) + { + mixClip.WriteSample(a.Samples[aReadCount]); // TODO support multi-byte samples + } + aReadCount++; + } + + m_pendingClips[0] = new ClipPlaybackTracker(mixClip); + for (int i = 1; i < numClipsToMix; i++) + { + m_pendingClips.RemoveAt(1); + } + } + else + { + m_pendingClips.Add(new ClipPlaybackTracker(clip)); + } + } + + public void Clear() + { + m_pendingClips.Clear(); + } + } + + /// + /// The system calls this each frame to update haptics playback. + /// + public static void Process() + { + Config.Load(); + + for (int i = 0; i < m_outputs.Length; i++) + { + m_outputs[i].Process(); + } + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRHapticsClip.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRHapticsClip.cs index 2c6c18d2..c4d3a5e3 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRHapticsClip.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRHapticsClip.cs @@ -6,144 +6,144 @@ /// public class OVRHapticsClip { - /// - /// The current number of samples in the clip. - /// - public int Count { get; private set; } - - /// - /// The maximum number of samples the clip can store. - /// - public int Capacity { get; private set; } - - /// - /// The raw haptics data. - /// - public byte[] Samples { get; private set; } - - public OVRHapticsClip() - { - Capacity = OVRHaptics.Config.MaximumBufferSamplesCount; - Samples = new byte[Capacity * OVRHaptics.Config.SampleSizeInBytes]; - } - - /// - /// Creates a clip with the specified capacity. - /// - public OVRHapticsClip(int capacity) - { - Capacity = (capacity >= 0) ? capacity : 0; - Samples = new byte[Capacity * OVRHaptics.Config.SampleSizeInBytes]; - } - - /// - /// Creates a clip with the specified data. - /// - public OVRHapticsClip(byte[] samples, int samplesCount) - { - Samples = samples; - Capacity = Samples.Length / OVRHaptics.Config.SampleSizeInBytes; - Count = (samplesCount >= 0) ? samplesCount : 0; - } - - /// - /// Creates a clip by mixing the specified clips. - /// - public OVRHapticsClip(OVRHapticsClip a, OVRHapticsClip b) - { - int maxCount = a.Count; - if (b.Count > maxCount) - maxCount = b.Count; - - Capacity = maxCount; - Samples = new byte[Capacity * OVRHaptics.Config.SampleSizeInBytes]; - - for (int i = 0; i < a.Count || i < b.Count; i++) - { - if (OVRHaptics.Config.SampleSizeInBytes == 1) - { - byte sample = 0; // TODO support multi-byte samples - if ((i < a.Count) && (i < b.Count)) - sample = (byte)(Mathf.Clamp(a.Samples[i] + b.Samples[i], 0, System.Byte.MaxValue)); // TODO support multi-byte samples - else if (i < a.Count) - sample = a.Samples[i]; // TODO support multi-byte samples - else if (i < b.Count) - sample = b.Samples[i]; // TODO support multi-byte samples - - WriteSample(sample); // TODO support multi-byte samples - } - } - } - - /// - /// Creates a haptics clip from the specified audio clip. - /// - public OVRHapticsClip(AudioClip audioClip, int channel = 0) - { - float[] audioData = new float[audioClip.samples * audioClip.channels]; - audioClip.GetData(audioData, 0); - - InitializeFromAudioFloatTrack(audioData, audioClip.frequency, audioClip.channels, channel); - } - - /// - /// Adds the specified sample to the end of the clip. - /// - public void WriteSample(byte sample) // TODO support multi-byte samples - { - if (Count >= Capacity) - { - //Debug.LogError("Attempted to write OVRHapticsClip sample out of range - Count:" + Count + " Capacity:" + Capacity); - return; - } - - if (OVRHaptics.Config.SampleSizeInBytes == 1) - { - Samples[Count * OVRHaptics.Config.SampleSizeInBytes] = sample; // TODO support multi-byte samples - } - - Count++; - } - - /// - /// Clears the clip and resets its size to 0. - /// - public void Reset() - { - Count = 0; - } - - private void InitializeFromAudioFloatTrack(float[] sourceData, double sourceFrequency, int sourceChannelCount, int sourceChannel) - { - double stepSizePrecise = (sourceFrequency + 1e-6) / OVRHaptics.Config.SampleRateHz; - - if (stepSizePrecise < 1.0) - return; - - int stepSize = (int)stepSizePrecise; - double stepSizeError = stepSizePrecise - stepSize; - double accumulatedStepSizeError = 0.0f; - int length = sourceData.Length; - - Count = 0; - Capacity = length / sourceChannelCount / stepSize + 1; - Samples = new byte[Capacity * OVRHaptics.Config.SampleSizeInBytes]; - - int i = sourceChannel % sourceChannelCount; - while (i < length) - { - if (OVRHaptics.Config.SampleSizeInBytes == 1) - { - WriteSample((byte)(Mathf.Clamp01(Mathf.Abs(sourceData[i])) * System.Byte.MaxValue)); // TODO support multi-byte samples - } - i+= stepSize * sourceChannelCount; - accumulatedStepSizeError += stepSizeError; - if ((int)accumulatedStepSizeError > 0) - { - i+= (int)accumulatedStepSizeError * sourceChannelCount; - accumulatedStepSizeError = accumulatedStepSizeError - (int)accumulatedStepSizeError; - } - } - } + /// + /// The current number of samples in the clip. + /// + public int Count { get; private set; } + + /// + /// The maximum number of samples the clip can store. + /// + public int Capacity { get; private set; } + + /// + /// The raw haptics data. + /// + public byte[] Samples { get; private set; } + + public OVRHapticsClip() + { + Capacity = OVRHaptics.Config.MaximumBufferSamplesCount; + Samples = new byte[Capacity * OVRHaptics.Config.SampleSizeInBytes]; + } + + /// + /// Creates a clip with the specified capacity. + /// + public OVRHapticsClip(int capacity) + { + Capacity = (capacity >= 0) ? capacity : 0; + Samples = new byte[Capacity * OVRHaptics.Config.SampleSizeInBytes]; + } + + /// + /// Creates a clip with the specified data. + /// + public OVRHapticsClip(byte[] samples, int samplesCount) + { + Samples = samples; + Capacity = Samples.Length / OVRHaptics.Config.SampleSizeInBytes; + Count = (samplesCount >= 0) ? samplesCount : 0; + } + + /// + /// Creates a clip by mixing the specified clips. + /// + public OVRHapticsClip(OVRHapticsClip a, OVRHapticsClip b) + { + int maxCount = a.Count; + if (b.Count > maxCount) + maxCount = b.Count; + + Capacity = maxCount; + Samples = new byte[Capacity * OVRHaptics.Config.SampleSizeInBytes]; + + for (int i = 0; i < a.Count || i < b.Count; i++) + { + if (OVRHaptics.Config.SampleSizeInBytes == 1) + { + byte sample = 0; // TODO support multi-byte samples + if ((i < a.Count) && (i < b.Count)) + sample = (byte)(Mathf.Clamp(a.Samples[i] + b.Samples[i], 0, System.Byte.MaxValue)); // TODO support multi-byte samples + else if (i < a.Count) + sample = a.Samples[i]; // TODO support multi-byte samples + else if (i < b.Count) + sample = b.Samples[i]; // TODO support multi-byte samples + + WriteSample(sample); // TODO support multi-byte samples + } + } + } + + /// + /// Creates a haptics clip from the specified audio clip. + /// + public OVRHapticsClip(AudioClip audioClip, int channel = 0) + { + float[] audioData = new float[audioClip.samples * audioClip.channels]; + audioClip.GetData(audioData, 0); + + InitializeFromAudioFloatTrack(audioData, audioClip.frequency, audioClip.channels, channel); + } + + /// + /// Adds the specified sample to the end of the clip. + /// + public void WriteSample(byte sample) // TODO support multi-byte samples + { + if (Count >= Capacity) + { + //Debug.LogError("Attempted to write OVRHapticsClip sample out of range - Count:" + Count + " Capacity:" + Capacity); + return; + } + + if (OVRHaptics.Config.SampleSizeInBytes == 1) + { + Samples[Count * OVRHaptics.Config.SampleSizeInBytes] = sample; // TODO support multi-byte samples + } + + Count++; + } + + /// + /// Clears the clip and resets its size to 0. + /// + public void Reset() + { + Count = 0; + } + + private void InitializeFromAudioFloatTrack(float[] sourceData, double sourceFrequency, int sourceChannelCount, int sourceChannel) + { + double stepSizePrecise = (sourceFrequency + 1e-6) / OVRHaptics.Config.SampleRateHz; + + if (stepSizePrecise < 1.0) + return; + + int stepSize = (int)stepSizePrecise; + double stepSizeError = stepSizePrecise - stepSize; + double accumulatedStepSizeError = 0.0f; + int length = sourceData.Length; + + Count = 0; + Capacity = length / sourceChannelCount / stepSize + 1; + Samples = new byte[Capacity * OVRHaptics.Config.SampleSizeInBytes]; + + int i = sourceChannel % sourceChannelCount; + while (i < length) + { + if (OVRHaptics.Config.SampleSizeInBytes == 1) + { + WriteSample((byte)(Mathf.Clamp01(Mathf.Abs(sourceData[i])) * System.Byte.MaxValue)); // TODO support multi-byte samples + } + i += stepSize * sourceChannelCount; + accumulatedStepSizeError += stepSizeError; + if ((int)accumulatedStepSizeError > 0) + { + i += (int)accumulatedStepSizeError * sourceChannelCount; + accumulatedStepSizeError = accumulatedStepSizeError - (int)accumulatedStepSizeError; + } + } + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRInput.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRInput.cs index 5451deb9..25cecc95 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRInput.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRInput.cs @@ -30,249 +30,249 @@ limitations under the License. /// public static class OVRInput { - [Flags] - /// Virtual button mappings that allow the same input bindings to work across different controllers. - public enum Button - { - None = 0, ///< Maps to RawButton: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - One = 0x00000001, ///< Maps to RawButton: [Gamepad, Touch, RTouch: A], [LTouch: X], [LTrackedRemote: LTouchpad], [RTrackedRemote: RTouchpad], [Touchpad, Remote: Start] - Two = 0x00000002, ///< Maps to RawButton: [Gamepad, Touch, RTouch: B], [LTouch: Y], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: Back] - Three = 0x00000004, ///< Maps to RawButton: [Gamepad, Touch: X], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - Four = 0x00000008, ///< Maps to RawButton: [Gamepad, Touch: Y], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - Start = 0x00000100, ///< Maps to RawButton: [Gamepad: Start], [Touch, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: Start], [RTouch: None] - Back = 0x00000200, ///< Maps to RawButton: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: Back], [Touch, LTouch, RTouch: None] - PrimaryShoulder = 0x00001000, ///< Maps to RawButton: [Gamepad: LShoulder], [Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - PrimaryIndexTrigger = 0x00002000, ///< Maps to RawButton: [Gamepad, Touch, LTouch, LTrackedRemote: LIndexTrigger], [RTouch, RTrackedRemote: RIndexTrigger], [Touchpad, Remote: None] - PrimaryHandTrigger = 0x00004000, ///< Maps to RawButton: [Touch, LTouch: LHandTrigger], [RTouch: RHandTrigger], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - PrimaryThumbstick = 0x00008000, ///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstick], [RTouch: RThumbstick], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - PrimaryThumbstickUp = 0x00010000, ///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstickUp], [RTouch: RThumbstickUp], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - PrimaryThumbstickDown = 0x00020000, ///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstickDown], [RTouch: RThumbstickDown], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - PrimaryThumbstickLeft = 0x00040000, ///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstickLeft], [RTouch: RThumbstickLeft], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - PrimaryThumbstickRight = 0x00080000, ///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstickRight], [RTouch: RThumbstickRight], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - PrimaryTouchpad = 0x00000400, ///< Maps to RawButton: [LTrackedRemote, Touchpad: LTouchpad], [RTrackedRemote: RTouchpad], [Gamepad, Touch, LTouch, RTouch, Remote: None] - SecondaryShoulder = 0x00100000, ///< Maps to RawButton: [Gamepad: RShoulder], [Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - SecondaryIndexTrigger = 0x00200000, ///< Maps to RawButton: [Gamepad, Touch: RIndexTrigger], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - SecondaryHandTrigger = 0x00400000, ///< Maps to RawButton: [Touch: RHandTrigger], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - SecondaryThumbstick = 0x00800000, ///< Maps to RawButton: [Gamepad, Touch: RThumbstick], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - SecondaryThumbstickUp = 0x01000000, ///< Maps to RawButton: [Gamepad, Touch: RThumbstickUp], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - SecondaryThumbstickDown = 0x02000000, ///< Maps to RawButton: [Gamepad, Touch: RThumbstickDown], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - SecondaryThumbstickLeft = 0x04000000, ///< Maps to RawButton: [Gamepad, Touch: RThumbstickLeft], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - SecondaryThumbstickRight = 0x08000000, ///< Maps to RawButton: [Gamepad, Touch: RThumbstickRight], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - SecondaryTouchpad = 0x00000800, ///< Maps to RawButton: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - DpadUp = 0x00000010, ///< Maps to RawButton: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadUp], [Touch, LTouch, RTouch: None] - DpadDown = 0x00000020, ///< Maps to RawButton: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadDown], [Touch, LTouch, RTouch: None] - DpadLeft = 0x00000040, ///< Maps to RawButton: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadLeft], [Touch, LTouch, RTouch: None] - DpadRight = 0x00000080, ///< Maps to RawButton: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadRight], [Touch, LTouch, RTouch: None] - Up = 0x10000000, ///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstickUp], [RTouch: RThumbstickUp], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadUp] - Down = 0x20000000, ///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstickDown], [RTouch: RThumbstickDown], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadDown] - Left = 0x40000000, ///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstickLeft], [RTouch: RThumbstickLeft], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadLeft] - Right = unchecked((int)0x80000000),///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstickRight], [RTouch: RThumbstickRight], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadRight] - Any = ~None, ///< Maps to RawButton: [Gamepad, Touch, LTouch, RTouch: Any] - } - - [Flags] - /// Raw button mappings that can be used to directly query the state of a controller. - public enum RawButton - { - None = 0, ///< Maps to Physical Button: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - A = 0x00000001, ///< Maps to Physical Button: [Gamepad, Touch, RTouch: A], [LTrackedRemote: LIndexTrigger], [RTrackedRemote: RIndexTrigger], [LTouch, Touchpad, Remote: None] - B = 0x00000002, ///< Maps to Physical Button: [Gamepad, Touch, RTouch: B], [LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - X = 0x00000100, ///< Maps to Physical Button: [Gamepad, Touch, LTouch: X], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - Y = 0x00000200, ///< Maps to Physical Button: [Gamepad, Touch, LTouch: Y], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - Start = 0x00100000, ///< Maps to Physical Button: [Gamepad, Touch, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: Start], [RTouch: None] - Back = 0x00200000, ///< Maps to Physical Button: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: Back], [Touch, LTouch, RTouch: None] - LShoulder = 0x00000800, ///< Maps to Physical Button: [Gamepad: LShoulder], [Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - LIndexTrigger = 0x10000000, ///< Maps to Physical Button: [Gamepad, Touch, LTouch, LTrackedRemote: LIndexTrigger], [RTouch, RTrackedRemote, Touchpad, Remote: None] - LHandTrigger = 0x20000000, ///< Maps to Physical Button: [Touch, LTouch: LHandTrigger], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - LThumbstick = 0x00000400, ///< Maps to Physical Button: [Gamepad, Touch, LTouch: LThumbstick], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - LThumbstickUp = 0x00000010, ///< Maps to Physical Button: [Gamepad, Touch, LTouch: LThumbstickUp], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - LThumbstickDown = 0x00000020, ///< Maps to Physical Button: [Gamepad, Touch, LTouch: LThumbstickDown], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - LThumbstickLeft = 0x00000040, ///< Maps to Physical Button: [Gamepad, Touch, LTouch: LThumbstickLeft], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - LThumbstickRight = 0x00000080, ///< Maps to Physical Button: [Gamepad, Touch, LTouch: LThumbstickRight], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - LTouchpad = 0x40000000, ///< Maps to Physical Button: [LTrackedRemote: LTouchpad], [Gamepad, Touch, LTouch, RTouch, RTrackedRemote, Touchpad, Remote: None] - RShoulder = 0x00000008, ///< Maps to Physical Button: [Gamepad: RShoulder], [Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - RIndexTrigger = 0x04000000, ///< Maps to Physical Button: [Gamepad, Touch, RTouch, RTrackedRemote: RIndexTrigger], [LTouch, LTrackedRemote, Touchpad, Remote: None] - RHandTrigger = 0x08000000, ///< Maps to Physical Button: [Touch, RTouch: RHandTrigger], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - RThumbstick = 0x00000004, ///< Maps to Physical Button: [Gamepad, Touch, RTouch: RThumbstick], [LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - RThumbstickUp = 0x00001000, ///< Maps to Physical Button: [Gamepad, Touch, RTouch: RThumbstickUp], [LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - RThumbstickDown = 0x00002000, ///< Maps to Physical Button: [Gamepad, Touch, RTouch: RThumbstickDown], [LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - RThumbstickLeft = 0x00004000, ///< Maps to Physical Button: [Gamepad, Touch, RTouch: RThumbstickLeft], [LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - RThumbstickRight = 0x00008000, ///< Maps to Physical Button: [Gamepad, Touch, RTouch: RThumbstickRight], [LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - RTouchpad = unchecked((int)0x80000000),///< Maps to Physical Button: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - DpadUp = 0x00010000, ///< Maps to Physical Button: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadUp], [Touch, LTouch, RTouch: None] - DpadDown = 0x00020000, ///< Maps to Physical Button: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadDown], [Touch, LTouch, RTouch: None] - DpadLeft = 0x00040000, ///< Maps to Physical Button: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadLeft], [Touch, LTouch, RTouch: None] - DpadRight = 0x00080000, ///< Maps to Physical Button: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadRight], [Touch, LTouch, RTouch: None] - Any = ~None, ///< Maps to Physical Button: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: Any] + [Flags] + /// Virtual button mappings that allow the same input bindings to work across different controllers. + public enum Button + { + None = 0, ///< Maps to RawButton: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + One = 0x00000001, ///< Maps to RawButton: [Gamepad, Touch, RTouch: A], [LTouch: X], [LTrackedRemote: LTouchpad], [RTrackedRemote: RTouchpad], [Touchpad, Remote: Start] + Two = 0x00000002, ///< Maps to RawButton: [Gamepad, Touch, RTouch: B], [LTouch: Y], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: Back] + Three = 0x00000004, ///< Maps to RawButton: [Gamepad, Touch: X], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + Four = 0x00000008, ///< Maps to RawButton: [Gamepad, Touch: Y], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + Start = 0x00000100, ///< Maps to RawButton: [Gamepad: Start], [Touch, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: Start], [RTouch: None] + Back = 0x00000200, ///< Maps to RawButton: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: Back], [Touch, LTouch, RTouch: None] + PrimaryShoulder = 0x00001000, ///< Maps to RawButton: [Gamepad: LShoulder], [Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + PrimaryIndexTrigger = 0x00002000, ///< Maps to RawButton: [Gamepad, Touch, LTouch, LTrackedRemote: LIndexTrigger], [RTouch, RTrackedRemote: RIndexTrigger], [Touchpad, Remote: None] + PrimaryHandTrigger = 0x00004000, ///< Maps to RawButton: [Touch, LTouch: LHandTrigger], [RTouch: RHandTrigger], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + PrimaryThumbstick = 0x00008000, ///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstick], [RTouch: RThumbstick], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + PrimaryThumbstickUp = 0x00010000, ///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstickUp], [RTouch: RThumbstickUp], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + PrimaryThumbstickDown = 0x00020000, ///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstickDown], [RTouch: RThumbstickDown], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + PrimaryThumbstickLeft = 0x00040000, ///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstickLeft], [RTouch: RThumbstickLeft], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + PrimaryThumbstickRight = 0x00080000, ///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstickRight], [RTouch: RThumbstickRight], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + PrimaryTouchpad = 0x00000400, ///< Maps to RawButton: [LTrackedRemote, Touchpad: LTouchpad], [RTrackedRemote: RTouchpad], [Gamepad, Touch, LTouch, RTouch, Remote: None] + SecondaryShoulder = 0x00100000, ///< Maps to RawButton: [Gamepad: RShoulder], [Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + SecondaryIndexTrigger = 0x00200000, ///< Maps to RawButton: [Gamepad, Touch: RIndexTrigger], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + SecondaryHandTrigger = 0x00400000, ///< Maps to RawButton: [Touch: RHandTrigger], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + SecondaryThumbstick = 0x00800000, ///< Maps to RawButton: [Gamepad, Touch: RThumbstick], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + SecondaryThumbstickUp = 0x01000000, ///< Maps to RawButton: [Gamepad, Touch: RThumbstickUp], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + SecondaryThumbstickDown = 0x02000000, ///< Maps to RawButton: [Gamepad, Touch: RThumbstickDown], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + SecondaryThumbstickLeft = 0x04000000, ///< Maps to RawButton: [Gamepad, Touch: RThumbstickLeft], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + SecondaryThumbstickRight = 0x08000000, ///< Maps to RawButton: [Gamepad, Touch: RThumbstickRight], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + SecondaryTouchpad = 0x00000800, ///< Maps to RawButton: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + DpadUp = 0x00000010, ///< Maps to RawButton: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadUp], [Touch, LTouch, RTouch: None] + DpadDown = 0x00000020, ///< Maps to RawButton: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadDown], [Touch, LTouch, RTouch: None] + DpadLeft = 0x00000040, ///< Maps to RawButton: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadLeft], [Touch, LTouch, RTouch: None] + DpadRight = 0x00000080, ///< Maps to RawButton: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadRight], [Touch, LTouch, RTouch: None] + Up = 0x10000000, ///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstickUp], [RTouch: RThumbstickUp], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadUp] + Down = 0x20000000, ///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstickDown], [RTouch: RThumbstickDown], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadDown] + Left = 0x40000000, ///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstickLeft], [RTouch: RThumbstickLeft], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadLeft] + Right = unchecked((int)0x80000000),///< Maps to RawButton: [Gamepad, Touch, LTouch: LThumbstickRight], [RTouch: RThumbstickRight], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadRight] + Any = ~None, ///< Maps to RawButton: [Gamepad, Touch, LTouch, RTouch: Any] } [Flags] - /// Virtual capacitive touch mappings that allow the same input bindings to work across different controllers with capacitive touch support. - public enum Touch - { - None = 0, ///< Maps to RawTouch: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - One = Button.One, ///< Maps to RawTouch: [Touch, RTouch: A], [LTouch: X], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - Two = Button.Two, ///< Maps to RawTouch: [Touch, RTouch: B], [LTouch: Y], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - Three = Button.Three, ///< Maps to RawTouch: [Touch: X], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - Four = Button.Four, ///< Maps to RawTouch: [Touch: Y], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - PrimaryIndexTrigger = Button.PrimaryIndexTrigger, ///< Maps to RawTouch: [Touch, LTouch: LIndexTrigger], [RTouch: RIndexTrigger], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - PrimaryThumbstick = Button.PrimaryThumbstick, ///< Maps to RawTouch: [Touch, LTouch: LThumbstick], [RTouch: RThumbstick], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - PrimaryThumbRest = 0x00001000, ///< Maps to RawTouch: [Touch, LTouch: LThumbRest], [RTouch: RThumbRest], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - PrimaryTouchpad = Button.PrimaryTouchpad, ///< Maps to RawTouch: [LTrackedRemote, Touchpad: LTouchpad], [RTrackedRemote: RTouchpad], [Gamepad, Touch, LTouch, RTouch, Remote: None] - SecondaryIndexTrigger = Button.SecondaryIndexTrigger, ///< Maps to RawTouch: [Touch: RIndexTrigger], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - SecondaryThumbstick = Button.SecondaryThumbstick, ///< Maps to RawTouch: [Touch: RThumbstick], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - SecondaryThumbRest = 0x00100000, ///< Maps to RawTouch: [Touch: RThumbRest], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - SecondaryTouchpad = Button.SecondaryTouchpad, ///< Maps to RawTouch: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - Any = ~None, ///< Maps to RawTouch: [Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad: Any], [Gamepad, Remote: None] + /// Raw button mappings that can be used to directly query the state of a controller. + public enum RawButton + { + None = 0, ///< Maps to Physical Button: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + A = 0x00000001, ///< Maps to Physical Button: [Gamepad, Touch, RTouch: A], [LTrackedRemote: LIndexTrigger], [RTrackedRemote: RIndexTrigger], [LTouch, Touchpad, Remote: None] + B = 0x00000002, ///< Maps to Physical Button: [Gamepad, Touch, RTouch: B], [LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + X = 0x00000100, ///< Maps to Physical Button: [Gamepad, Touch, LTouch: X], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + Y = 0x00000200, ///< Maps to Physical Button: [Gamepad, Touch, LTouch: Y], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + Start = 0x00100000, ///< Maps to Physical Button: [Gamepad, Touch, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: Start], [RTouch: None] + Back = 0x00200000, ///< Maps to Physical Button: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: Back], [Touch, LTouch, RTouch: None] + LShoulder = 0x00000800, ///< Maps to Physical Button: [Gamepad: LShoulder], [Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + LIndexTrigger = 0x10000000, ///< Maps to Physical Button: [Gamepad, Touch, LTouch, LTrackedRemote: LIndexTrigger], [RTouch, RTrackedRemote, Touchpad, Remote: None] + LHandTrigger = 0x20000000, ///< Maps to Physical Button: [Touch, LTouch: LHandTrigger], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + LThumbstick = 0x00000400, ///< Maps to Physical Button: [Gamepad, Touch, LTouch: LThumbstick], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + LThumbstickUp = 0x00000010, ///< Maps to Physical Button: [Gamepad, Touch, LTouch: LThumbstickUp], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + LThumbstickDown = 0x00000020, ///< Maps to Physical Button: [Gamepad, Touch, LTouch: LThumbstickDown], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + LThumbstickLeft = 0x00000040, ///< Maps to Physical Button: [Gamepad, Touch, LTouch: LThumbstickLeft], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + LThumbstickRight = 0x00000080, ///< Maps to Physical Button: [Gamepad, Touch, LTouch: LThumbstickRight], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + LTouchpad = 0x40000000, ///< Maps to Physical Button: [LTrackedRemote: LTouchpad], [Gamepad, Touch, LTouch, RTouch, RTrackedRemote, Touchpad, Remote: None] + RShoulder = 0x00000008, ///< Maps to Physical Button: [Gamepad: RShoulder], [Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + RIndexTrigger = 0x04000000, ///< Maps to Physical Button: [Gamepad, Touch, RTouch, RTrackedRemote: RIndexTrigger], [LTouch, LTrackedRemote, Touchpad, Remote: None] + RHandTrigger = 0x08000000, ///< Maps to Physical Button: [Touch, RTouch: RHandTrigger], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + RThumbstick = 0x00000004, ///< Maps to Physical Button: [Gamepad, Touch, RTouch: RThumbstick], [LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + RThumbstickUp = 0x00001000, ///< Maps to Physical Button: [Gamepad, Touch, RTouch: RThumbstickUp], [LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + RThumbstickDown = 0x00002000, ///< Maps to Physical Button: [Gamepad, Touch, RTouch: RThumbstickDown], [LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + RThumbstickLeft = 0x00004000, ///< Maps to Physical Button: [Gamepad, Touch, RTouch: RThumbstickLeft], [LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + RThumbstickRight = 0x00008000, ///< Maps to Physical Button: [Gamepad, Touch, RTouch: RThumbstickRight], [LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + RTouchpad = unchecked((int)0x80000000),///< Maps to Physical Button: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + DpadUp = 0x00010000, ///< Maps to Physical Button: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadUp], [Touch, LTouch, RTouch: None] + DpadDown = 0x00020000, ///< Maps to Physical Button: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadDown], [Touch, LTouch, RTouch: None] + DpadLeft = 0x00040000, ///< Maps to Physical Button: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadLeft], [Touch, LTouch, RTouch: None] + DpadRight = 0x00080000, ///< Maps to Physical Button: [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: DpadRight], [Touch, LTouch, RTouch: None] + Any = ~None, ///< Maps to Physical Button: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: Any] } [Flags] - /// Raw capacitive touch mappings that can be used to directly query the state of a controller. - public enum RawTouch - { - None = 0, ///< Maps to Physical Touch: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - A = RawButton.A, ///< Maps to Physical Touch: [Touch, RTouch: A], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - B = RawButton.B, ///< Maps to Physical Touch: [Touch, RTouch: B], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - X = RawButton.X, ///< Maps to Physical Touch: [Touch, LTouch: X], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - Y = RawButton.Y, ///< Maps to Physical Touch: [Touch, LTouch: Y], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - LIndexTrigger = 0x00001000, ///< Maps to Physical Touch: [Touch, LTouch: LIndexTrigger], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - LThumbstick = RawButton.LThumbstick, ///< Maps to Physical Touch: [Touch, LTouch: LThumbstick], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - LThumbRest = 0x00000800, ///< Maps to Physical Touch: [Touch, LTouch: LThumbRest], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - LTouchpad = RawButton.LTouchpad, ///< Maps to Physical Touch: [LTrackedRemote, Touchpad: LTouchpad], [Gamepad, Touch, LTouch, RTouch, RTrackedRemote, Remote: None] - RIndexTrigger = 0x00000010, ///< Maps to Physical Touch: [Touch, RTouch: RIndexTrigger], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - RThumbstick = RawButton.RThumbstick, ///< Maps to Physical Touch: [Touch, RTouch: RThumbstick], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - RThumbRest = 0x00000008, ///< Maps to Physical Touch: [Touch, RTouch: RThumbRest], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - RTouchpad = RawButton.RTouchpad, ///< Maps to Physical Touch: [RTrackedRemote: RTouchpad], [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, Touchpad, Remote: None] - Any = ~None, ///< Maps to Physical Touch: [Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad: Any], [Gamepad, Remote: None] + /// Virtual capacitive touch mappings that allow the same input bindings to work across different controllers with capacitive touch support. + public enum Touch + { + None = 0, ///< Maps to RawTouch: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + One = Button.One, ///< Maps to RawTouch: [Touch, RTouch: A], [LTouch: X], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + Two = Button.Two, ///< Maps to RawTouch: [Touch, RTouch: B], [LTouch: Y], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + Three = Button.Three, ///< Maps to RawTouch: [Touch: X], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + Four = Button.Four, ///< Maps to RawTouch: [Touch: Y], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + PrimaryIndexTrigger = Button.PrimaryIndexTrigger, ///< Maps to RawTouch: [Touch, LTouch: LIndexTrigger], [RTouch: RIndexTrigger], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + PrimaryThumbstick = Button.PrimaryThumbstick, ///< Maps to RawTouch: [Touch, LTouch: LThumbstick], [RTouch: RThumbstick], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + PrimaryThumbRest = 0x00001000, ///< Maps to RawTouch: [Touch, LTouch: LThumbRest], [RTouch: RThumbRest], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + PrimaryTouchpad = Button.PrimaryTouchpad, ///< Maps to RawTouch: [LTrackedRemote, Touchpad: LTouchpad], [RTrackedRemote: RTouchpad], [Gamepad, Touch, LTouch, RTouch, Remote: None] + SecondaryIndexTrigger = Button.SecondaryIndexTrigger, ///< Maps to RawTouch: [Touch: RIndexTrigger], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + SecondaryThumbstick = Button.SecondaryThumbstick, ///< Maps to RawTouch: [Touch: RThumbstick], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + SecondaryThumbRest = 0x00100000, ///< Maps to RawTouch: [Touch: RThumbRest], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + SecondaryTouchpad = Button.SecondaryTouchpad, ///< Maps to RawTouch: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + Any = ~None, ///< Maps to RawTouch: [Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad: Any], [Gamepad, Remote: None] } [Flags] - /// Virtual near touch mappings that allow the same input bindings to work across different controllers with near touch support. - /// A near touch uses the capacitive touch sensors of a controller to detect approximate finger proximity prior to a full touch being reported. - public enum NearTouch - { - None = 0, ///< Maps to RawNearTouch: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - PrimaryIndexTrigger = 0x00000001, ///< Maps to RawNearTouch: [Touch, LTouch: LIndexTrigger], [RTouch: RIndexTrigger], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - PrimaryThumbButtons = 0x00000002, ///< Maps to RawNearTouch: [Touch, LTouch: LThumbButtons], [RTouch: RThumbButtons], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - SecondaryIndexTrigger = 0x00000004, ///< Maps to RawNearTouch: [Touch: RIndexTrigger], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - SecondaryThumbButtons = 0x00000008, ///< Maps to RawNearTouch: [Touch: RThumbButtons], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - Any = ~None, ///< Maps to RawNearTouch: [Touch, LTouch, RTouch: Any], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + /// Raw capacitive touch mappings that can be used to directly query the state of a controller. + public enum RawTouch + { + None = 0, ///< Maps to Physical Touch: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + A = RawButton.A, ///< Maps to Physical Touch: [Touch, RTouch: A], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + B = RawButton.B, ///< Maps to Physical Touch: [Touch, RTouch: B], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + X = RawButton.X, ///< Maps to Physical Touch: [Touch, LTouch: X], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + Y = RawButton.Y, ///< Maps to Physical Touch: [Touch, LTouch: Y], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + LIndexTrigger = 0x00001000, ///< Maps to Physical Touch: [Touch, LTouch: LIndexTrigger], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + LThumbstick = RawButton.LThumbstick, ///< Maps to Physical Touch: [Touch, LTouch: LThumbstick], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + LThumbRest = 0x00000800, ///< Maps to Physical Touch: [Touch, LTouch: LThumbRest], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + LTouchpad = RawButton.LTouchpad, ///< Maps to Physical Touch: [LTrackedRemote, Touchpad: LTouchpad], [Gamepad, Touch, LTouch, RTouch, RTrackedRemote, Remote: None] + RIndexTrigger = 0x00000010, ///< Maps to Physical Touch: [Touch, RTouch: RIndexTrigger], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + RThumbstick = RawButton.RThumbstick, ///< Maps to Physical Touch: [Touch, RTouch: RThumbstick], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + RThumbRest = 0x00000008, ///< Maps to Physical Touch: [Touch, RTouch: RThumbRest], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + RTouchpad = RawButton.RTouchpad, ///< Maps to Physical Touch: [RTrackedRemote: RTouchpad], [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, Touchpad, Remote: None] + Any = ~None, ///< Maps to Physical Touch: [Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad: Any], [Gamepad, Remote: None] } [Flags] - /// Raw near touch mappings that can be used to directly query the state of a controller. - public enum RawNearTouch - { - None = 0, ///< Maps to Physical NearTouch: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - LIndexTrigger = 0x00000001, ///< Maps to Physical NearTouch: [Touch, LTouch: Implies finger is in close proximity to LIndexTrigger.], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - LThumbButtons = 0x00000002, ///< Maps to Physical NearTouch: [Touch, LTouch: Implies thumb is in close proximity to LThumbstick OR X/Y buttons.], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - RIndexTrigger = 0x00000004, ///< Maps to Physical NearTouch: [Touch, RTouch: Implies finger is in close proximity to RIndexTrigger.], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - RThumbButtons = 0x00000008, ///< Maps to Physical NearTouch: [Touch, RTouch: Implies thumb is in close proximity to RThumbstick OR A/B buttons.], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - Any = ~None, ///< Maps to Physical NearTouch: [Touch, LTouch, RTouch: Any], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + /// Virtual near touch mappings that allow the same input bindings to work across different controllers with near touch support. + /// A near touch uses the capacitive touch sensors of a controller to detect approximate finger proximity prior to a full touch being reported. + public enum NearTouch + { + None = 0, ///< Maps to RawNearTouch: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + PrimaryIndexTrigger = 0x00000001, ///< Maps to RawNearTouch: [Touch, LTouch: LIndexTrigger], [RTouch: RIndexTrigger], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + PrimaryThumbButtons = 0x00000002, ///< Maps to RawNearTouch: [Touch, LTouch: LThumbButtons], [RTouch: RThumbButtons], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + SecondaryIndexTrigger = 0x00000004, ///< Maps to RawNearTouch: [Touch: RIndexTrigger], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + SecondaryThumbButtons = 0x00000008, ///< Maps to RawNearTouch: [Touch: RThumbButtons], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + Any = ~None, ///< Maps to RawNearTouch: [Touch, LTouch, RTouch: Any], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] } [Flags] - /// Virtual 1-dimensional axis (float) mappings that allow the same input bindings to work across different controllers. - public enum Axis1D - { - None = 0, ///< Maps to RawAxis1D: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - PrimaryIndexTrigger = 0x01, ///< Maps to RawAxis1D: [Gamepad, Touch, LTouch: LIndexTrigger], [RTouch: RIndexTrigger], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - PrimaryHandTrigger = 0x04, ///< Maps to RawAxis1D: [Touch, LTouch: LHandTrigger], [RTouch: RHandTrigger], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - SecondaryIndexTrigger = 0x02, ///< Maps to RawAxis1D: [Gamepad, Touch: RIndexTrigger], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - SecondaryHandTrigger = 0x08, ///< Maps to RawAxis1D: [Touch: RHandTrigger], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - Any = ~None, ///< Maps to RawAxis1D: [Gamepad, Touch, LTouch, RTouch: Any], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + /// Raw near touch mappings that can be used to directly query the state of a controller. + public enum RawNearTouch + { + None = 0, ///< Maps to Physical NearTouch: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + LIndexTrigger = 0x00000001, ///< Maps to Physical NearTouch: [Touch, LTouch: Implies finger is in close proximity to LIndexTrigger.], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + LThumbButtons = 0x00000002, ///< Maps to Physical NearTouch: [Touch, LTouch: Implies thumb is in close proximity to LThumbstick OR X/Y buttons.], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + RIndexTrigger = 0x00000004, ///< Maps to Physical NearTouch: [Touch, RTouch: Implies finger is in close proximity to RIndexTrigger.], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + RThumbButtons = 0x00000008, ///< Maps to Physical NearTouch: [Touch, RTouch: Implies thumb is in close proximity to RThumbstick OR A/B buttons.], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + Any = ~None, ///< Maps to Physical NearTouch: [Touch, LTouch, RTouch: Any], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] } [Flags] - /// Raw 1-dimensional axis (float) mappings that can be used to directly query the state of a controller. - public enum RawAxis1D - { - None = 0, ///< Maps to Physical Axis1D: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - LIndexTrigger = 0x01, ///< Maps to Physical Axis1D: [Gamepad, Touch, LTouch: LIndexTrigger], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - LHandTrigger = 0x04, ///< Maps to Physical Axis1D: [Touch, LTouch: LHandTrigger], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - RIndexTrigger = 0x02, ///< Maps to Physical Axis1D: [Gamepad, Touch, RTouch: RIndexTrigger], [LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - RHandTrigger = 0x08, ///< Maps to Physical Axis1D: [Touch, RTouch: RHandTrigger], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - Any = ~None, ///< Maps to Physical Axis1D: [Gamepad, Touch, LTouch, RTouch: Any], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + /// Virtual 1-dimensional axis (float) mappings that allow the same input bindings to work across different controllers. + public enum Axis1D + { + None = 0, ///< Maps to RawAxis1D: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + PrimaryIndexTrigger = 0x01, ///< Maps to RawAxis1D: [Gamepad, Touch, LTouch: LIndexTrigger], [RTouch: RIndexTrigger], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + PrimaryHandTrigger = 0x04, ///< Maps to RawAxis1D: [Touch, LTouch: LHandTrigger], [RTouch: RHandTrigger], [Gamepad, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + SecondaryIndexTrigger = 0x02, ///< Maps to RawAxis1D: [Gamepad, Touch: RIndexTrigger], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + SecondaryHandTrigger = 0x08, ///< Maps to RawAxis1D: [Touch: RHandTrigger], [Gamepad, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + Any = ~None, ///< Maps to RawAxis1D: [Gamepad, Touch, LTouch, RTouch: Any], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] } [Flags] - /// Virtual 2-dimensional axis (Vector2) mappings that allow the same input bindings to work across different controllers. - public enum Axis2D - { - None = 0, ///< Maps to RawAxis2D: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - PrimaryThumbstick = 0x01, ///< Maps to RawAxis2D: [Gamepad, Touch, LTouch: LThumbstick], [RTouch: RThumbstick], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - PrimaryTouchpad = 0x04, ///< Maps to RawAxis2D: [LTrackedRemote, Touchpad: LTouchpad], RTrackedRemote: RTouchpad], [Gamepad, Touch, LTouch, RTouch, Remote: None] - SecondaryThumbstick = 0x02, ///< Maps to RawAxis2D: [Gamepad, Touch: RThumbstick], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - SecondaryTouchpad = 0x08, ///< Maps to RawAxis2D: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - Any = ~None, ///< Maps to RawAxis2D: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad: Any], [Remote: None] + /// Raw 1-dimensional axis (float) mappings that can be used to directly query the state of a controller. + public enum RawAxis1D + { + None = 0, ///< Maps to Physical Axis1D: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + LIndexTrigger = 0x01, ///< Maps to Physical Axis1D: [Gamepad, Touch, LTouch: LIndexTrigger], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + LHandTrigger = 0x04, ///< Maps to Physical Axis1D: [Touch, LTouch: LHandTrigger], [Gamepad, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + RIndexTrigger = 0x02, ///< Maps to Physical Axis1D: [Gamepad, Touch, RTouch: RIndexTrigger], [LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + RHandTrigger = 0x08, ///< Maps to Physical Axis1D: [Touch, RTouch: RHandTrigger], [Gamepad, LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + Any = ~None, ///< Maps to Physical Axis1D: [Gamepad, Touch, LTouch, RTouch: Any], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] } [Flags] - /// Raw 2-dimensional axis (Vector2) mappings that can be used to directly query the state of a controller. - public enum RawAxis2D - { - None = 0, ///< Maps to Physical Axis2D: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - LThumbstick = 0x01, ///< Maps to Physical Axis2D: [Gamepad, Touch, LTouch: LThumbstick], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - LTouchpad = 0x04, ///< Maps to Physical Axis2D: [LTrackedRemote, Touchpad: LTouchpad], [Gamepad, Touch, LTouch, RTouch, RTrackedRemote, Remote: None] - RThumbstick = 0x02, ///< Maps to Physical Axis2D: [Gamepad, Touch, RTouch: RThumbstick], [LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] - RTouchpad = 0x08, ///< Maps to Physical Axis2D: [RTrackedRemote: RTouchpad], [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, Touchpad, Remote: None] - Any = ~None, ///< Maps to Physical Axis2D: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote: Any], [Touchpad, Remote: None] + /// Virtual 2-dimensional axis (Vector2) mappings that allow the same input bindings to work across different controllers. + public enum Axis2D + { + None = 0, ///< Maps to RawAxis2D: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + PrimaryThumbstick = 0x01, ///< Maps to RawAxis2D: [Gamepad, Touch, LTouch: LThumbstick], [RTouch: RThumbstick], [LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + PrimaryTouchpad = 0x04, ///< Maps to RawAxis2D: [LTrackedRemote, Touchpad: LTouchpad], RTrackedRemote: RTouchpad], [Gamepad, Touch, LTouch, RTouch, Remote: None] + SecondaryThumbstick = 0x02, ///< Maps to RawAxis2D: [Gamepad, Touch: RThumbstick], [LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + SecondaryTouchpad = 0x08, ///< Maps to RawAxis2D: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + Any = ~None, ///< Maps to RawAxis2D: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad: Any], [Remote: None] } - [Flags] - /// Identifies a controller which can be used to query the virtual or raw input state. - public enum Controller - { - None = OVRPlugin.Controller.None, ///< Null controller. - LTouch = OVRPlugin.Controller.LTouch, ///< Left Oculus Touch controller. Virtual input mapping differs from the combined L/R Touch mapping. - RTouch = OVRPlugin.Controller.RTouch, ///< Right Oculus Touch controller. Virtual input mapping differs from the combined L/R Touch mapping. - Touch = OVRPlugin.Controller.Touch, ///< Combined Left/Right pair of Oculus Touch controllers. - Remote = OVRPlugin.Controller.Remote, ///< Oculus Remote controller. - Gamepad = OVRPlugin.Controller.Gamepad, ///< Xbox 360 or Xbox One gamepad on PC. Generic gamepad on Android. - Touchpad = OVRPlugin.Controller.Touchpad, ///< GearVR touchpad on Android. - LTrackedRemote = OVRPlugin.Controller.LTrackedRemote, ///< Left GearVR tracked remote on Android. - RTrackedRemote = OVRPlugin.Controller.RTrackedRemote, ///< Right GearVR tracked remote on Android. - Active = OVRPlugin.Controller.Active, ///< Default controller. Represents the controller that most recently registered a button press from the user. - All = OVRPlugin.Controller.All, ///< Represents the logical OR of all controllers. + [Flags] + /// Raw 2-dimensional axis (Vector2) mappings that can be used to directly query the state of a controller. + public enum RawAxis2D + { + None = 0, ///< Maps to Physical Axis2D: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + LThumbstick = 0x01, ///< Maps to Physical Axis2D: [Gamepad, Touch, LTouch: LThumbstick], [RTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + LTouchpad = 0x04, ///< Maps to Physical Axis2D: [LTrackedRemote, Touchpad: LTouchpad], [Gamepad, Touch, LTouch, RTouch, RTrackedRemote, Remote: None] + RThumbstick = 0x02, ///< Maps to Physical Axis2D: [Gamepad, Touch, RTouch: RThumbstick], [LTouch, LTrackedRemote, RTrackedRemote, Touchpad, Remote: None] + RTouchpad = 0x08, ///< Maps to Physical Axis2D: [RTrackedRemote: RTouchpad], [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, Touchpad, Remote: None] + Any = ~None, ///< Maps to Physical Axis2D: [Gamepad, Touch, LTouch, RTouch, LTrackedRemote, RTrackedRemote: Any], [Touchpad, Remote: None] } - private static readonly float AXIS_AS_BUTTON_THRESHOLD = 0.5f; - private static readonly float AXIS_DEADZONE_THRESHOLD = 0.2f; - private static List controllers; - private static Controller activeControllerType = Controller.None; - private static Controller connectedControllerTypes = Controller.None; - private static OVRPlugin.Step stepType = OVRPlugin.Step.Render; + [Flags] + /// Identifies a controller which can be used to query the virtual or raw input state. + public enum Controller + { + None = OVRPlugin.Controller.None, ///< Null controller. + LTouch = OVRPlugin.Controller.LTouch, ///< Left Oculus Touch controller. Virtual input mapping differs from the combined L/R Touch mapping. + RTouch = OVRPlugin.Controller.RTouch, ///< Right Oculus Touch controller. Virtual input mapping differs from the combined L/R Touch mapping. + Touch = OVRPlugin.Controller.Touch, ///< Combined Left/Right pair of Oculus Touch controllers. + Remote = OVRPlugin.Controller.Remote, ///< Oculus Remote controller. + Gamepad = OVRPlugin.Controller.Gamepad, ///< Xbox 360 or Xbox One gamepad on PC. Generic gamepad on Android. + Touchpad = OVRPlugin.Controller.Touchpad, ///< GearVR touchpad on Android. + LTrackedRemote = OVRPlugin.Controller.LTrackedRemote, ///< Left GearVR tracked remote on Android. + RTrackedRemote = OVRPlugin.Controller.RTrackedRemote, ///< Right GearVR tracked remote on Android. + Active = OVRPlugin.Controller.Active, ///< Default controller. Represents the controller that most recently registered a button press from the user. + All = OVRPlugin.Controller.All, ///< Represents the logical OR of all controllers. + } + + private static readonly float AXIS_AS_BUTTON_THRESHOLD = 0.5f; + private static readonly float AXIS_DEADZONE_THRESHOLD = 0.2f; + private static List controllers; + private static Controller activeControllerType = Controller.None; + private static Controller connectedControllerTypes = Controller.None; + private static OVRPlugin.Step stepType = OVRPlugin.Step.Render; private static int fixedUpdateCount = 0; - private static bool _pluginSupportsActiveController = false; - private static bool _pluginSupportsActiveControllerCached = false; - private static System.Version _pluginSupportsActiveControllerMinVersion = new System.Version(1, 9, 0); - private static bool pluginSupportsActiveController - { - get - { - if (!_pluginSupportsActiveControllerCached) - { - bool isSupportedPlatform = true; + private static bool _pluginSupportsActiveController = false; + private static bool _pluginSupportsActiveControllerCached = false; + private static System.Version _pluginSupportsActiveControllerMinVersion = new System.Version(1, 9, 0); + private static bool pluginSupportsActiveController + { + get + { + if (!_pluginSupportsActiveControllerCached) + { + bool isSupportedPlatform = true; #if (UNITY_ANDROID && !UNITY_EDITOR) || UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX isSupportedPlatform = false; #endif - _pluginSupportsActiveController = isSupportedPlatform && (OVRPlugin.version >= _pluginSupportsActiveControllerMinVersion); - _pluginSupportsActiveControllerCached = true; - } + _pluginSupportsActiveController = isSupportedPlatform && (OVRPlugin.version >= _pluginSupportsActiveControllerMinVersion); + _pluginSupportsActiveControllerCached = true; + } - return _pluginSupportsActiveController; - } - } + return _pluginSupportsActiveController; + } + } - /// - /// Creates an instance of OVRInput. - /// - static OVRInput() - { - controllers = new List - { + /// + /// Creates an instance of OVRInput. + /// + static OVRInput() + { + controllers = new List + { #if UNITY_ANDROID && !UNITY_EDITOR new OVRControllerGamepadAndroid(), new OVRControllerTouchpad(), @@ -282,53 +282,53 @@ static OVRInput() new OVRControllerGamepadMac(), #else new OVRControllerGamepadPC(), - new OVRControllerTouch(), - new OVRControllerLTouch(), - new OVRControllerRTouch(), - new OVRControllerRemote(), + new OVRControllerTouch(), + new OVRControllerLTouch(), + new OVRControllerRTouch(), + new OVRControllerRemote(), #endif }; - } + } /// /// Updates the internal state of OVRInput. Must be called manually if used independently from OVRManager. /// public static void Update() - { - connectedControllerTypes = Controller.None; - stepType = OVRPlugin.Step.Render; - fixedUpdateCount = 0; - - for (int i = 0; i < controllers.Count; i++) - { - OVRControllerBase controller = controllers[i]; - - connectedControllerTypes |= controller.Update(); - - if ((connectedControllerTypes & controller.controllerType) != 0) - { - if (Get(RawButton.Any, controller.controllerType) - || Get(RawTouch.Any, controller.controllerType)) - { - activeControllerType = controller.controllerType; - } - } - } - - if ((activeControllerType == Controller.LTouch) || (activeControllerType == Controller.RTouch)) - { - // If either Touch controller is Active, set both to Active. - activeControllerType = Controller.Touch; - } - - if ((connectedControllerTypes & activeControllerType) == 0) - { - activeControllerType = Controller.None; - } + { + connectedControllerTypes = Controller.None; + stepType = OVRPlugin.Step.Render; + fixedUpdateCount = 0; + + for (int i = 0; i < controllers.Count; i++) + { + OVRControllerBase controller = controllers[i]; + + connectedControllerTypes |= controller.Update(); + + if ((connectedControllerTypes & controller.controllerType) != 0) + { + if (Get(RawButton.Any, controller.controllerType) + || Get(RawTouch.Any, controller.controllerType)) + { + activeControllerType = controller.controllerType; + } + } + } + + if ((activeControllerType == Controller.LTouch) || (activeControllerType == Controller.RTouch)) + { + // If either Touch controller is Active, set both to Active. + activeControllerType = Controller.Touch; + } + + if ((connectedControllerTypes & activeControllerType) == 0) + { + activeControllerType = Controller.None; + } // Promote TrackedRemote to Active if one is connected and no other controller is active - if (activeControllerType == Controller.None) - { + if (activeControllerType == Controller.None) + { if ((connectedControllerTypes & Controller.RTrackedRemote) != 0) { activeControllerType = Controller.RTrackedRemote; @@ -337,87 +337,87 @@ public static void Update() { activeControllerType = Controller.LTrackedRemote; } - } - - if (pluginSupportsActiveController) - { - // override locally derived active and connected controllers if plugin provides more accurate data - connectedControllerTypes = (OVRInput.Controller)OVRPlugin.GetConnectedControllers(); - activeControllerType = (OVRInput.Controller)OVRPlugin.GetActiveController(); - } - } + } - /// - /// Updates the internal physics state of OVRInput. Must be called manually if used independently from OVRManager. - /// - public static void FixedUpdate() - { - stepType = OVRPlugin.Step.Physics; - - double predictionSeconds = (double)fixedUpdateCount * Time.fixedDeltaTime / Mathf.Max(Time.timeScale, 1e-6f); - fixedUpdateCount++; - - OVRPlugin.UpdateNodePhysicsPoses(0, predictionSeconds); - } + if (pluginSupportsActiveController) + { + // override locally derived active and connected controllers if plugin provides more accurate data + connectedControllerTypes = (OVRInput.Controller)OVRPlugin.GetConnectedControllers(); + activeControllerType = (OVRInput.Controller)OVRPlugin.GetActiveController(); + } + } + + /// + /// Updates the internal physics state of OVRInput. Must be called manually if used independently from OVRManager. + /// + public static void FixedUpdate() + { + stepType = OVRPlugin.Step.Physics; - /// - /// Returns true if the given Controller's orientation is currently tracked. - /// Only supported for Oculus LTouch and RTouch controllers. Non-tracked controllers will return false. - /// - public static bool GetControllerOrientationTracked(OVRInput.Controller controllerType) - { - switch (controllerType) - { - case Controller.LTouch: + double predictionSeconds = (double)fixedUpdateCount * Time.fixedDeltaTime / Mathf.Max(Time.timeScale, 1e-6f); + fixedUpdateCount++; + + OVRPlugin.UpdateNodePhysicsPoses(0, predictionSeconds); + } + + /// + /// Returns true if the given Controller's orientation is currently tracked. + /// Only supported for Oculus LTouch and RTouch controllers. Non-tracked controllers will return false. + /// + public static bool GetControllerOrientationTracked(OVRInput.Controller controllerType) + { + switch (controllerType) + { + case Controller.LTouch: case Controller.LTrackedRemote: return OVRPlugin.GetNodeOrientationTracked(OVRPlugin.Node.HandLeft); case Controller.RTouch: case Controller.RTrackedRemote: return OVRPlugin.GetNodeOrientationTracked(OVRPlugin.Node.HandRight); default: - return false; - } - } + return false; + } + } - /// - /// Returns true if the given Controller's position is currently tracked. - /// Only supported for Oculus LTouch and RTouch controllers. Non-tracked controllers will return false. - /// - public static bool GetControllerPositionTracked(OVRInput.Controller controllerType) - { - switch (controllerType) - { - case Controller.LTouch: - case Controller.LTrackedRemote: + /// + /// Returns true if the given Controller's position is currently tracked. + /// Only supported for Oculus LTouch and RTouch controllers. Non-tracked controllers will return false. + /// + public static bool GetControllerPositionTracked(OVRInput.Controller controllerType) + { + switch (controllerType) + { + case Controller.LTouch: + case Controller.LTrackedRemote: return OVRPlugin.GetNodePositionTracked(OVRPlugin.Node.HandLeft); case Controller.RTouch: case Controller.RTrackedRemote: return OVRPlugin.GetNodePositionTracked(OVRPlugin.Node.HandRight); default: - return false; - } - } + return false; + } + } - /// - /// Gets the position of the given Controller local to its tracking space. - /// Only supported for Oculus LTouch and RTouch controllers. Non-tracked controllers will return Vector3.zero. - /// - public static Vector3 GetLocalControllerPosition(OVRInput.Controller controllerType) - { - switch (controllerType) - { - case Controller.LTouch: - case Controller.LTrackedRemote: + /// + /// Gets the position of the given Controller local to its tracking space. + /// Only supported for Oculus LTouch and RTouch controllers. Non-tracked controllers will return Vector3.zero. + /// + public static Vector3 GetLocalControllerPosition(OVRInput.Controller controllerType) + { + switch (controllerType) + { + case Controller.LTouch: + case Controller.LTrackedRemote: return OVRPlugin.GetNodePose(OVRPlugin.Node.HandLeft, stepType).ToOVRPose().position; case Controller.RTouch: - case Controller.RTrackedRemote: + case Controller.RTrackedRemote: return OVRPlugin.GetNodePose(OVRPlugin.Node.HandRight, stepType).ToOVRPose().position; default: - return Vector3.zero; - } - } + return Vector3.zero; + } + } - /// + /// /// Gets the linear velocity of the given Controller local to its tracking space. /// Only supported for Oculus LTouch and RTouch controllers. Non-tracked controllers will return Vector3.zero. /// @@ -426,11 +426,11 @@ public static Vector3 GetLocalControllerVelocity(OVRInput.Controller controllerT switch (controllerType) { case Controller.LTouch: - case Controller.LTrackedRemote: - return OVRPlugin.GetNodeVelocity(OVRPlugin.Node.HandLeft, stepType).FromFlippedZVector3f(); + case Controller.LTrackedRemote: + return OVRPlugin.GetNodeVelocity(OVRPlugin.Node.HandLeft, stepType).FromFlippedZVector3f(); case Controller.RTouch: - case Controller.RTrackedRemote: - return OVRPlugin.GetNodeVelocity(OVRPlugin.Node.HandRight, stepType).FromFlippedZVector3f(); + case Controller.RTrackedRemote: + return OVRPlugin.GetNodeVelocity(OVRPlugin.Node.HandRight, stepType).FromFlippedZVector3f(); default: return Vector3.zero; } @@ -445,2065 +445,2065 @@ public static Vector3 GetLocalControllerAcceleration(OVRInput.Controller control switch (controllerType) { case Controller.LTouch: - case Controller.LTrackedRemote: - return OVRPlugin.GetNodeAcceleration(OVRPlugin.Node.HandLeft, stepType).FromFlippedZVector3f(); + case Controller.LTrackedRemote: + return OVRPlugin.GetNodeAcceleration(OVRPlugin.Node.HandLeft, stepType).FromFlippedZVector3f(); case Controller.RTouch: - case Controller.RTrackedRemote: - return OVRPlugin.GetNodeAcceleration(OVRPlugin.Node.HandRight, stepType).FromFlippedZVector3f(); + case Controller.RTrackedRemote: + return OVRPlugin.GetNodeAcceleration(OVRPlugin.Node.HandRight, stepType).FromFlippedZVector3f(); default: return Vector3.zero; } } - /// - /// Gets the rotation of the given Controller local to its tracking space. - /// Only supported for Oculus LTouch and RTouch controllers. Non-tracked controllers will return Quaternion.identity. - /// - public static Quaternion GetLocalControllerRotation(OVRInput.Controller controllerType) - { - switch (controllerType) - { - case Controller.LTouch: - case Controller.LTrackedRemote: - return OVRPlugin.GetNodePose(OVRPlugin.Node.HandLeft, stepType).ToOVRPose().orientation; + /// + /// Gets the rotation of the given Controller local to its tracking space. + /// Only supported for Oculus LTouch and RTouch controllers. Non-tracked controllers will return Quaternion.identity. + /// + public static Quaternion GetLocalControllerRotation(OVRInput.Controller controllerType) + { + switch (controllerType) + { + case Controller.LTouch: + case Controller.LTrackedRemote: + return OVRPlugin.GetNodePose(OVRPlugin.Node.HandLeft, stepType).ToOVRPose().orientation; case Controller.RTouch: - case Controller.RTrackedRemote: - return OVRPlugin.GetNodePose(OVRPlugin.Node.HandRight, stepType).ToOVRPose().orientation; + case Controller.RTrackedRemote: + return OVRPlugin.GetNodePose(OVRPlugin.Node.HandRight, stepType).ToOVRPose().orientation; default: - return Quaternion.identity; - } - } + return Quaternion.identity; + } + } - /// - /// Gets the angular velocity of the given Controller local to its tracking space in radians per second around each axis. - /// Only supported for Oculus LTouch and RTouch controllers. Non-tracked controllers will return Quaternion.identity. - /// - public static Vector3 GetLocalControllerAngularVelocity(OVRInput.Controller controllerType) - { - switch (controllerType) - { - case Controller.LTouch: - case Controller.LTrackedRemote: - return OVRPlugin.GetNodeAngularVelocity(OVRPlugin.Node.HandLeft, stepType).FromFlippedZVector3f(); - case Controller.RTouch: - case Controller.RTrackedRemote: - return OVRPlugin.GetNodeAngularVelocity(OVRPlugin.Node.HandRight, stepType).FromFlippedZVector3f(); - default: - return Vector3.zero; - } - } + /// + /// Gets the angular velocity of the given Controller local to its tracking space in radians per second around each axis. + /// Only supported for Oculus LTouch and RTouch controllers. Non-tracked controllers will return Quaternion.identity. + /// + public static Vector3 GetLocalControllerAngularVelocity(OVRInput.Controller controllerType) + { + switch (controllerType) + { + case Controller.LTouch: + case Controller.LTrackedRemote: + return OVRPlugin.GetNodeAngularVelocity(OVRPlugin.Node.HandLeft, stepType).FromFlippedZVector3f(); + case Controller.RTouch: + case Controller.RTrackedRemote: + return OVRPlugin.GetNodeAngularVelocity(OVRPlugin.Node.HandRight, stepType).FromFlippedZVector3f(); + default: + return Vector3.zero; + } + } - /// - /// Gets the angular acceleration of the given Controller local to its tracking space in radians per second per second around each axis. - /// Only supported for Oculus LTouch and RTouch controllers. Non-tracked controllers will return Quaternion.identity. - /// - public static Vector3 GetLocalControllerAngularAcceleration(OVRInput.Controller controllerType) - { - switch (controllerType) - { - case Controller.LTouch: - case Controller.LTrackedRemote: - return OVRPlugin.GetNodeAngularAcceleration(OVRPlugin.Node.HandLeft, stepType).FromFlippedZVector3f(); - case Controller.RTouch: - case Controller.RTrackedRemote: - return OVRPlugin.GetNodeAngularAcceleration(OVRPlugin.Node.HandRight, stepType).FromFlippedZVector3f(); - default: - return Vector3.zero; - } - } + /// + /// Gets the angular acceleration of the given Controller local to its tracking space in radians per second per second around each axis. + /// Only supported for Oculus LTouch and RTouch controllers. Non-tracked controllers will return Quaternion.identity. + /// + public static Vector3 GetLocalControllerAngularAcceleration(OVRInput.Controller controllerType) + { + switch (controllerType) + { + case Controller.LTouch: + case Controller.LTrackedRemote: + return OVRPlugin.GetNodeAngularAcceleration(OVRPlugin.Node.HandLeft, stepType).FromFlippedZVector3f(); + case Controller.RTouch: + case Controller.RTrackedRemote: + return OVRPlugin.GetNodeAngularAcceleration(OVRPlugin.Node.HandRight, stepType).FromFlippedZVector3f(); + default: + return Vector3.zero; + } + } - /// - /// Gets the current state of the given virtual button mask with the given controller mask. - /// Returns true if any masked button is down on any masked controller. - /// - public static bool Get(Button virtualMask, Controller controllerMask = Controller.Active) - { - return GetResolvedButton(virtualMask, RawButton.None, controllerMask); - } + /// + /// Gets the current state of the given virtual button mask with the given controller mask. + /// Returns true if any masked button is down on any masked controller. + /// + public static bool Get(Button virtualMask, Controller controllerMask = Controller.Active) + { + return GetResolvedButton(virtualMask, RawButton.None, controllerMask); + } - /// - /// Gets the current state of the given raw button mask with the given controller mask. - /// Returns true if any masked button is down on any masked controllers. - /// - public static bool Get(RawButton rawMask, Controller controllerMask = Controller.Active) - { - return GetResolvedButton(Button.None, rawMask, controllerMask); - } + /// + /// Gets the current state of the given raw button mask with the given controller mask. + /// Returns true if any masked button is down on any masked controllers. + /// + public static bool Get(RawButton rawMask, Controller controllerMask = Controller.Active) + { + return GetResolvedButton(Button.None, rawMask, controllerMask); + } - private static bool GetResolvedButton(Button virtualMask, RawButton rawMask, Controller controllerMask) - { - if ((controllerMask & Controller.Active) != 0) - controllerMask |= activeControllerType; + private static bool GetResolvedButton(Button virtualMask, RawButton rawMask, Controller controllerMask) + { + if ((controllerMask & Controller.Active) != 0) + controllerMask |= activeControllerType; - for (int i = 0; i < controllers.Count; i++) - { - OVRControllerBase controller = controllers[i]; + for (int i = 0; i < controllers.Count; i++) + { + OVRControllerBase controller = controllers[i]; - if (ShouldResolveController(controller.controllerType, controllerMask)) - { - RawButton resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); + if (ShouldResolveController(controller.controllerType, controllerMask)) + { + RawButton resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); - if (((RawButton)controller.currentState.Buttons & resolvedMask) != 0) - { - return true; - } - } - } + if (((RawButton)controller.currentState.Buttons & resolvedMask) != 0) + { + return true; + } + } + } - return false; - } + return false; + } - /// - /// Gets the current down state of the given virtual button mask with the given controller mask. - /// Returns true if any masked button was pressed this frame on any masked controller and no masked button was previously down last frame. - /// - public static bool GetDown(Button virtualMask, Controller controllerMask = Controller.Active) - { - return GetResolvedButtonDown(virtualMask, RawButton.None, controllerMask); - } + /// + /// Gets the current down state of the given virtual button mask with the given controller mask. + /// Returns true if any masked button was pressed this frame on any masked controller and no masked button was previously down last frame. + /// + public static bool GetDown(Button virtualMask, Controller controllerMask = Controller.Active) + { + return GetResolvedButtonDown(virtualMask, RawButton.None, controllerMask); + } - /// - /// Gets the current down state of the given raw button mask with the given controller mask. - /// Returns true if any masked button was pressed this frame on any masked controller and no masked button was previously down last frame. - /// - public static bool GetDown(RawButton rawMask, Controller controllerMask = Controller.Active) - { - return GetResolvedButtonDown(Button.None, rawMask, controllerMask); - } + /// + /// Gets the current down state of the given raw button mask with the given controller mask. + /// Returns true if any masked button was pressed this frame on any masked controller and no masked button was previously down last frame. + /// + public static bool GetDown(RawButton rawMask, Controller controllerMask = Controller.Active) + { + return GetResolvedButtonDown(Button.None, rawMask, controllerMask); + } - private static bool GetResolvedButtonDown(Button virtualMask, RawButton rawMask, Controller controllerMask) - { - bool down = false; + private static bool GetResolvedButtonDown(Button virtualMask, RawButton rawMask, Controller controllerMask) + { + bool down = false; - if ((controllerMask & Controller.Active) != 0) - controllerMask |= activeControllerType; + if ((controllerMask & Controller.Active) != 0) + controllerMask |= activeControllerType; - for (int i = 0; i < controllers.Count; i++) - { - OVRControllerBase controller = controllers[i]; + for (int i = 0; i < controllers.Count; i++) + { + OVRControllerBase controller = controllers[i]; - if (ShouldResolveController(controller.controllerType, controllerMask)) - { - RawButton resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); + if (ShouldResolveController(controller.controllerType, controllerMask)) + { + RawButton resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); - if (((RawButton)controller.previousState.Buttons & resolvedMask) != 0) - { - return false; - } + if (((RawButton)controller.previousState.Buttons & resolvedMask) != 0) + { + return false; + } - if ((((RawButton)controller.currentState.Buttons & resolvedMask) != 0) - && (((RawButton)controller.previousState.Buttons & resolvedMask) == 0)) - { - down = true; - } - } - } + if ((((RawButton)controller.currentState.Buttons & resolvedMask) != 0) + && (((RawButton)controller.previousState.Buttons & resolvedMask) == 0)) + { + down = true; + } + } + } - return down; - } + return down; + } - /// - /// Gets the current up state of the given virtual button mask with the given controller mask. - /// Returns true if any masked button was released this frame on any masked controller and no other masked button is still down this frame. - /// - public static bool GetUp(Button virtualMask, Controller controllerMask = Controller.Active) - { - return GetResolvedButtonUp(virtualMask, RawButton.None, controllerMask); - } + /// + /// Gets the current up state of the given virtual button mask with the given controller mask. + /// Returns true if any masked button was released this frame on any masked controller and no other masked button is still down this frame. + /// + public static bool GetUp(Button virtualMask, Controller controllerMask = Controller.Active) + { + return GetResolvedButtonUp(virtualMask, RawButton.None, controllerMask); + } - /// - /// Gets the current up state of the given raw button mask with the given controller mask. - /// Returns true if any masked button was released this frame on any masked controller and no other masked button is still down this frame. - /// - public static bool GetUp(RawButton rawMask, Controller controllerMask = Controller.Active) - { - return GetResolvedButtonUp(Button.None, rawMask, controllerMask); - } + /// + /// Gets the current up state of the given raw button mask with the given controller mask. + /// Returns true if any masked button was released this frame on any masked controller and no other masked button is still down this frame. + /// + public static bool GetUp(RawButton rawMask, Controller controllerMask = Controller.Active) + { + return GetResolvedButtonUp(Button.None, rawMask, controllerMask); + } - private static bool GetResolvedButtonUp(Button virtualMask, RawButton rawMask, Controller controllerMask) - { - bool up = false; + private static bool GetResolvedButtonUp(Button virtualMask, RawButton rawMask, Controller controllerMask) + { + bool up = false; - if ((controllerMask & Controller.Active) != 0) - controllerMask |= activeControllerType; + if ((controllerMask & Controller.Active) != 0) + controllerMask |= activeControllerType; - for (int i = 0; i < controllers.Count; i++) - { - OVRControllerBase controller = controllers[i]; + for (int i = 0; i < controllers.Count; i++) + { + OVRControllerBase controller = controllers[i]; - if (ShouldResolveController(controller.controllerType, controllerMask)) - { - RawButton resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); + if (ShouldResolveController(controller.controllerType, controllerMask)) + { + RawButton resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); - if (((RawButton)controller.currentState.Buttons & resolvedMask) != 0) - { - return false; - } + if (((RawButton)controller.currentState.Buttons & resolvedMask) != 0) + { + return false; + } - if ((((RawButton)controller.currentState.Buttons & resolvedMask) == 0) - && (((RawButton)controller.previousState.Buttons & resolvedMask) != 0)) - { - up = true; - } - } - } + if ((((RawButton)controller.currentState.Buttons & resolvedMask) == 0) + && (((RawButton)controller.previousState.Buttons & resolvedMask) != 0)) + { + up = true; + } + } + } - return up; - } + return up; + } - /// - /// Gets the current state of the given virtual touch mask with the given controller mask. - /// Returns true if any masked touch is down on any masked controller. - /// - public static bool Get(Touch virtualMask, Controller controllerMask = Controller.Active) - { - return GetResolvedTouch(virtualMask, RawTouch.None, controllerMask); - } + /// + /// Gets the current state of the given virtual touch mask with the given controller mask. + /// Returns true if any masked touch is down on any masked controller. + /// + public static bool Get(Touch virtualMask, Controller controllerMask = Controller.Active) + { + return GetResolvedTouch(virtualMask, RawTouch.None, controllerMask); + } - /// - /// Gets the current state of the given raw touch mask with the given controller mask. - /// Returns true if any masked touch is down on any masked controllers. - /// - public static bool Get(RawTouch rawMask, Controller controllerMask = Controller.Active) - { - return GetResolvedTouch(Touch.None, rawMask, controllerMask); - } + /// + /// Gets the current state of the given raw touch mask with the given controller mask. + /// Returns true if any masked touch is down on any masked controllers. + /// + public static bool Get(RawTouch rawMask, Controller controllerMask = Controller.Active) + { + return GetResolvedTouch(Touch.None, rawMask, controllerMask); + } - private static bool GetResolvedTouch(Touch virtualMask, RawTouch rawMask, Controller controllerMask) - { - if ((controllerMask & Controller.Active) != 0) - controllerMask |= activeControllerType; + private static bool GetResolvedTouch(Touch virtualMask, RawTouch rawMask, Controller controllerMask) + { + if ((controllerMask & Controller.Active) != 0) + controllerMask |= activeControllerType; - for (int i = 0; i < controllers.Count; i++) - { - OVRControllerBase controller = controllers[i]; + for (int i = 0; i < controllers.Count; i++) + { + OVRControllerBase controller = controllers[i]; - if (ShouldResolveController(controller.controllerType, controllerMask)) - { - RawTouch resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); + if (ShouldResolveController(controller.controllerType, controllerMask)) + { + RawTouch resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); - if (((RawTouch)controller.currentState.Touches & resolvedMask) != 0) - { - return true; - } - } - } + if (((RawTouch)controller.currentState.Touches & resolvedMask) != 0) + { + return true; + } + } + } - return false; - } + return false; + } - /// - /// Gets the current down state of the given virtual touch mask with the given controller mask. - /// Returns true if any masked touch was pressed this frame on any masked controller and no masked touch was previously down last frame. - /// - public static bool GetDown(Touch virtualMask, Controller controllerMask = Controller.Active) - { - return GetResolvedTouchDown(virtualMask, RawTouch.None, controllerMask); - } + /// + /// Gets the current down state of the given virtual touch mask with the given controller mask. + /// Returns true if any masked touch was pressed this frame on any masked controller and no masked touch was previously down last frame. + /// + public static bool GetDown(Touch virtualMask, Controller controllerMask = Controller.Active) + { + return GetResolvedTouchDown(virtualMask, RawTouch.None, controllerMask); + } - /// - /// Gets the current down state of the given raw touch mask with the given controller mask. - /// Returns true if any masked touch was pressed this frame on any masked controller and no masked touch was previously down last frame. - /// - public static bool GetDown(RawTouch rawMask, Controller controllerMask = Controller.Active) - { - return GetResolvedTouchDown(Touch.None, rawMask, controllerMask); - } + /// + /// Gets the current down state of the given raw touch mask with the given controller mask. + /// Returns true if any masked touch was pressed this frame on any masked controller and no masked touch was previously down last frame. + /// + public static bool GetDown(RawTouch rawMask, Controller controllerMask = Controller.Active) + { + return GetResolvedTouchDown(Touch.None, rawMask, controllerMask); + } - private static bool GetResolvedTouchDown(Touch virtualMask, RawTouch rawMask, Controller controllerMask) - { - bool down = false; + private static bool GetResolvedTouchDown(Touch virtualMask, RawTouch rawMask, Controller controllerMask) + { + bool down = false; - if ((controllerMask & Controller.Active) != 0) - controllerMask |= activeControllerType; + if ((controllerMask & Controller.Active) != 0) + controllerMask |= activeControllerType; - for (int i = 0; i < controllers.Count; i++) - { - OVRControllerBase controller = controllers[i]; + for (int i = 0; i < controllers.Count; i++) + { + OVRControllerBase controller = controllers[i]; - if (ShouldResolveController(controller.controllerType, controllerMask)) - { - RawTouch resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); + if (ShouldResolveController(controller.controllerType, controllerMask)) + { + RawTouch resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); - if (((RawTouch)controller.previousState.Touches & resolvedMask) != 0) - { - return false; - } + if (((RawTouch)controller.previousState.Touches & resolvedMask) != 0) + { + return false; + } - if ((((RawTouch)controller.currentState.Touches & resolvedMask) != 0) - && (((RawTouch)controller.previousState.Touches & resolvedMask) == 0)) - { - down = true; - } - } - } + if ((((RawTouch)controller.currentState.Touches & resolvedMask) != 0) + && (((RawTouch)controller.previousState.Touches & resolvedMask) == 0)) + { + down = true; + } + } + } - return down; - } + return down; + } - /// - /// Gets the current up state of the given virtual touch mask with the given controller mask. - /// Returns true if any masked touch was released this frame on any masked controller and no other masked touch is still down this frame. - /// - public static bool GetUp(Touch virtualMask, Controller controllerMask = Controller.Active) - { - return GetResolvedTouchUp(virtualMask, RawTouch.None, controllerMask); - } + /// + /// Gets the current up state of the given virtual touch mask with the given controller mask. + /// Returns true if any masked touch was released this frame on any masked controller and no other masked touch is still down this frame. + /// + public static bool GetUp(Touch virtualMask, Controller controllerMask = Controller.Active) + { + return GetResolvedTouchUp(virtualMask, RawTouch.None, controllerMask); + } - /// - /// Gets the current up state of the given raw touch mask with the given controller mask. - /// Returns true if any masked touch was released this frame on any masked controller and no other masked touch is still down this frame. - /// - public static bool GetUp(RawTouch rawMask, Controller controllerMask = Controller.Active) - { - return GetResolvedTouchUp(Touch.None, rawMask, controllerMask); - } + /// + /// Gets the current up state of the given raw touch mask with the given controller mask. + /// Returns true if any masked touch was released this frame on any masked controller and no other masked touch is still down this frame. + /// + public static bool GetUp(RawTouch rawMask, Controller controllerMask = Controller.Active) + { + return GetResolvedTouchUp(Touch.None, rawMask, controllerMask); + } - private static bool GetResolvedTouchUp(Touch virtualMask, RawTouch rawMask, Controller controllerMask) - { - bool up = false; + private static bool GetResolvedTouchUp(Touch virtualMask, RawTouch rawMask, Controller controllerMask) + { + bool up = false; - if ((controllerMask & Controller.Active) != 0) - controllerMask |= activeControllerType; + if ((controllerMask & Controller.Active) != 0) + controllerMask |= activeControllerType; - for (int i = 0; i < controllers.Count; i++) - { - OVRControllerBase controller = controllers[i]; + for (int i = 0; i < controllers.Count; i++) + { + OVRControllerBase controller = controllers[i]; - if (ShouldResolveController(controller.controllerType, controllerMask)) - { - RawTouch resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); + if (ShouldResolveController(controller.controllerType, controllerMask)) + { + RawTouch resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); - if (((RawTouch)controller.currentState.Touches & resolvedMask) != 0) - { - return false; - } + if (((RawTouch)controller.currentState.Touches & resolvedMask) != 0) + { + return false; + } - if ((((RawTouch)controller.currentState.Touches & resolvedMask) == 0) - && (((RawTouch)controller.previousState.Touches & resolvedMask) != 0)) - { - up = true; - } - } - } + if ((((RawTouch)controller.currentState.Touches & resolvedMask) == 0) + && (((RawTouch)controller.previousState.Touches & resolvedMask) != 0)) + { + up = true; + } + } + } - return up; - } + return up; + } - /// - /// Gets the current state of the given virtual near touch mask with the given controller mask. - /// Returns true if any masked near touch is down on any masked controller. - /// - public static bool Get(NearTouch virtualMask, Controller controllerMask = Controller.Active) - { - return GetResolvedNearTouch(virtualMask, RawNearTouch.None, controllerMask); - } + /// + /// Gets the current state of the given virtual near touch mask with the given controller mask. + /// Returns true if any masked near touch is down on any masked controller. + /// + public static bool Get(NearTouch virtualMask, Controller controllerMask = Controller.Active) + { + return GetResolvedNearTouch(virtualMask, RawNearTouch.None, controllerMask); + } - /// - /// Gets the current state of the given raw near touch mask with the given controller mask. - /// Returns true if any masked near touch is down on any masked controllers. - /// - public static bool Get(RawNearTouch rawMask, Controller controllerMask = Controller.Active) - { - return GetResolvedNearTouch(NearTouch.None, rawMask, controllerMask); - } + /// + /// Gets the current state of the given raw near touch mask with the given controller mask. + /// Returns true if any masked near touch is down on any masked controllers. + /// + public static bool Get(RawNearTouch rawMask, Controller controllerMask = Controller.Active) + { + return GetResolvedNearTouch(NearTouch.None, rawMask, controllerMask); + } - private static bool GetResolvedNearTouch(NearTouch virtualMask, RawNearTouch rawMask, Controller controllerMask) - { - if ((controllerMask & Controller.Active) != 0) - controllerMask |= activeControllerType; + private static bool GetResolvedNearTouch(NearTouch virtualMask, RawNearTouch rawMask, Controller controllerMask) + { + if ((controllerMask & Controller.Active) != 0) + controllerMask |= activeControllerType; - for (int i = 0; i < controllers.Count; i++) - { - OVRControllerBase controller = controllers[i]; + for (int i = 0; i < controllers.Count; i++) + { + OVRControllerBase controller = controllers[i]; - if (ShouldResolveController(controller.controllerType, controllerMask)) - { - RawNearTouch resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); + if (ShouldResolveController(controller.controllerType, controllerMask)) + { + RawNearTouch resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); - if (((RawNearTouch)controller.currentState.NearTouches & resolvedMask) != 0) - { - return true; - } - } - } + if (((RawNearTouch)controller.currentState.NearTouches & resolvedMask) != 0) + { + return true; + } + } + } - return false; - } + return false; + } - /// - /// Gets the current down state of the given virtual near touch mask with the given controller mask. - /// Returns true if any masked near touch was pressed this frame on any masked controller and no masked near touch was previously down last frame. - /// - public static bool GetDown(NearTouch virtualMask, Controller controllerMask = Controller.Active) - { - return GetResolvedNearTouchDown(virtualMask, RawNearTouch.None, controllerMask); - } + /// + /// Gets the current down state of the given virtual near touch mask with the given controller mask. + /// Returns true if any masked near touch was pressed this frame on any masked controller and no masked near touch was previously down last frame. + /// + public static bool GetDown(NearTouch virtualMask, Controller controllerMask = Controller.Active) + { + return GetResolvedNearTouchDown(virtualMask, RawNearTouch.None, controllerMask); + } - /// - /// Gets the current down state of the given raw near touch mask with the given controller mask. - /// Returns true if any masked near touch was pressed this frame on any masked controller and no masked near touch was previously down last frame. - /// - public static bool GetDown(RawNearTouch rawMask, Controller controllerMask = Controller.Active) - { - return GetResolvedNearTouchDown(NearTouch.None, rawMask, controllerMask); - } + /// + /// Gets the current down state of the given raw near touch mask with the given controller mask. + /// Returns true if any masked near touch was pressed this frame on any masked controller and no masked near touch was previously down last frame. + /// + public static bool GetDown(RawNearTouch rawMask, Controller controllerMask = Controller.Active) + { + return GetResolvedNearTouchDown(NearTouch.None, rawMask, controllerMask); + } + + private static bool GetResolvedNearTouchDown(NearTouch virtualMask, RawNearTouch rawMask, Controller controllerMask) + { + bool down = false; + + if ((controllerMask & Controller.Active) != 0) + controllerMask |= activeControllerType; + + for (int i = 0; i < controllers.Count; i++) + { + OVRControllerBase controller = controllers[i]; + + if (ShouldResolveController(controller.controllerType, controllerMask)) + { + RawNearTouch resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); + + if (((RawNearTouch)controller.previousState.NearTouches & resolvedMask) != 0) + { + return false; + } + + if ((((RawNearTouch)controller.currentState.NearTouches & resolvedMask) != 0) + && (((RawNearTouch)controller.previousState.NearTouches & resolvedMask) == 0)) + { + down = true; + } + } + } + + return down; + } + + /// + /// Gets the current up state of the given virtual near touch mask with the given controller mask. + /// Returns true if any masked near touch was released this frame on any masked controller and no other masked near touch is still down this frame. + /// + public static bool GetUp(NearTouch virtualMask, Controller controllerMask = Controller.Active) + { + return GetResolvedNearTouchUp(virtualMask, RawNearTouch.None, controllerMask); + } + + /// + /// Gets the current up state of the given raw near touch mask with the given controller mask. + /// Returns true if any masked near touch was released this frame on any masked controller and no other masked near touch is still down this frame. + /// + public static bool GetUp(RawNearTouch rawMask, Controller controllerMask = Controller.Active) + { + return GetResolvedNearTouchUp(NearTouch.None, rawMask, controllerMask); + } + + private static bool GetResolvedNearTouchUp(NearTouch virtualMask, RawNearTouch rawMask, Controller controllerMask) + { + bool up = false; + + if ((controllerMask & Controller.Active) != 0) + controllerMask |= activeControllerType; + + for (int i = 0; i < controllers.Count; i++) + { + OVRControllerBase controller = controllers[i]; + + if (ShouldResolveController(controller.controllerType, controllerMask)) + { + RawNearTouch resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); + + if (((RawNearTouch)controller.currentState.NearTouches & resolvedMask) != 0) + { + return false; + } + + if ((((RawNearTouch)controller.currentState.NearTouches & resolvedMask) == 0) + && (((RawNearTouch)controller.previousState.NearTouches & resolvedMask) != 0)) + { + up = true; + } + } + } + + return up; + } + + /// + /// Gets the current state of the given virtual 1-dimensional axis mask on the given controller mask. + /// Returns the value of the largest masked axis across all masked controllers. Values range from 0 to 1. + /// + public static float Get(Axis1D virtualMask, Controller controllerMask = Controller.Active) + { + return GetResolvedAxis1D(virtualMask, RawAxis1D.None, controllerMask); + } + + /// + /// Gets the current state of the given raw 1-dimensional axis mask on the given controller mask. + /// Returns the value of the largest masked axis across all masked controllers. Values range from 0 to 1. + /// + public static float Get(RawAxis1D rawMask, Controller controllerMask = Controller.Active) + { + return GetResolvedAxis1D(Axis1D.None, rawMask, controllerMask); + } + + private static float GetResolvedAxis1D(Axis1D virtualMask, RawAxis1D rawMask, Controller controllerMask) + { + float maxAxis = 0.0f; + + if ((controllerMask & Controller.Active) != 0) + controllerMask |= activeControllerType; + + for (int i = 0; i < controllers.Count; i++) + { + OVRControllerBase controller = controllers[i]; + + if (ShouldResolveController(controller.controllerType, controllerMask)) + { + RawAxis1D resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); + + if ((RawAxis1D.LIndexTrigger & resolvedMask) != 0) + { + float axis = controller.currentState.LIndexTrigger; + + if (controller.shouldApplyDeadzone) + axis = CalculateDeadzone(axis, AXIS_DEADZONE_THRESHOLD); + + maxAxis = CalculateAbsMax(maxAxis, axis); + } + if ((RawAxis1D.RIndexTrigger & resolvedMask) != 0) + { + float axis = controller.currentState.RIndexTrigger; + + if (controller.shouldApplyDeadzone) + axis = CalculateDeadzone(axis, AXIS_DEADZONE_THRESHOLD); + + maxAxis = CalculateAbsMax(maxAxis, axis); + } + if ((RawAxis1D.LHandTrigger & resolvedMask) != 0) + { + float axis = controller.currentState.LHandTrigger; + + if (controller.shouldApplyDeadzone) + axis = CalculateDeadzone(axis, AXIS_DEADZONE_THRESHOLD); + + maxAxis = CalculateAbsMax(maxAxis, axis); + } + if ((RawAxis1D.RHandTrigger & resolvedMask) != 0) + { + float axis = controller.currentState.RHandTrigger; + + if (controller.shouldApplyDeadzone) + axis = CalculateDeadzone(axis, AXIS_DEADZONE_THRESHOLD); + + maxAxis = CalculateAbsMax(maxAxis, axis); + } + } + } + + return maxAxis; + } + + /// + /// Gets the current state of the given virtual 2-dimensional axis mask on the given controller mask. + /// Returns the vector of the largest masked axis across all masked controllers. Values range from -1 to 1. + /// + public static Vector2 Get(Axis2D virtualMask, Controller controllerMask = Controller.Active) + { + return GetResolvedAxis2D(virtualMask, RawAxis2D.None, controllerMask); + } + + /// + /// Gets the current state of the given raw 2-dimensional axis mask on the given controller mask. + /// Returns the vector of the largest masked axis across all masked controllers. Values range from -1 to 1. + /// + public static Vector2 Get(RawAxis2D rawMask, Controller controllerMask = Controller.Active) + { + return GetResolvedAxis2D(Axis2D.None, rawMask, controllerMask); + } + + private static Vector2 GetResolvedAxis2D(Axis2D virtualMask, RawAxis2D rawMask, Controller controllerMask) + { + Vector2 maxAxis = Vector2.zero; + + if ((controllerMask & Controller.Active) != 0) + controllerMask |= activeControllerType; + + for (int i = 0; i < controllers.Count; i++) + { + OVRControllerBase controller = controllers[i]; + + if (ShouldResolveController(controller.controllerType, controllerMask)) + { + RawAxis2D resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); + + if ((RawAxis2D.LThumbstick & resolvedMask) != 0) + { + Vector2 axis = new Vector2( + controller.currentState.LThumbstick.x, + controller.currentState.LThumbstick.y); + + if (controller.shouldApplyDeadzone) + axis = CalculateDeadzone(axis, AXIS_DEADZONE_THRESHOLD); + + maxAxis = CalculateAbsMax(maxAxis, axis); + } + if ((RawAxis2D.LTouchpad & resolvedMask) != 0) + { + Vector2 axis = new Vector2( + controller.currentState.LTouchpad.x, + controller.currentState.LTouchpad.y); + + //if (controller.shouldApplyDeadzone) + // axis = CalculateDeadzone(axis, AXIS_DEADZONE_THRESHOLD); + + maxAxis = CalculateAbsMax(maxAxis, axis); + } + if ((RawAxis2D.RThumbstick & resolvedMask) != 0) + { + Vector2 axis = new Vector2( + controller.currentState.RThumbstick.x, + controller.currentState.RThumbstick.y); + + if (controller.shouldApplyDeadzone) + axis = CalculateDeadzone(axis, AXIS_DEADZONE_THRESHOLD); + + maxAxis = CalculateAbsMax(maxAxis, axis); + } + if ((RawAxis2D.RTouchpad & resolvedMask) != 0) + { + Vector2 axis = new Vector2( + controller.currentState.RTouchpad.x, + controller.currentState.RTouchpad.y); + + //if (controller.shouldApplyDeadzone) + // axis = CalculateDeadzone(axis, AXIS_DEADZONE_THRESHOLD); + + maxAxis = CalculateAbsMax(maxAxis, axis); + } + } + } + + return maxAxis; + } + + /// + /// Returns a mask of all currently connected controller types. + /// + public static Controller GetConnectedControllers() + { + return connectedControllerTypes; + } + + /// + /// Returns true if the specified controller type is currently connected. + /// + public static bool IsControllerConnected(Controller controller) + { + return (connectedControllerTypes & controller) == controller; + } + + /// + /// Returns the current active controller type. + /// + public static Controller GetActiveController() + { + return activeControllerType; + } + + /// + /// Activates vibration with the given frequency and amplitude with the given controller mask. + /// Ignored on controllers that do not support vibration. Expected values range from 0 to 1. + /// + public static void SetControllerVibration(float frequency, float amplitude, Controller controllerMask = Controller.Active) + { + if ((controllerMask & Controller.Active) != 0) + controllerMask |= activeControllerType; + + for (int i = 0; i < controllers.Count; i++) + { + OVRControllerBase controller = controllers[i]; + + if (ShouldResolveController(controller.controllerType, controllerMask)) + { + controller.SetControllerVibration(frequency, amplitude); + } + } + } + + public static void RecenterController(Controller controllerMask = Controller.Active) + { + if ((controllerMask & Controller.Active) != 0) + controllerMask |= activeControllerType; + + for (int i = 0; i < controllers.Count; i++) + { + OVRControllerBase controller = controllers[i]; + + if (ShouldResolveController(controller.controllerType, controllerMask)) + { + controller.RecenterController(); + } + } + } + + private static Vector2 CalculateAbsMax(Vector2 a, Vector2 b) + { + float absA = a.sqrMagnitude; + float absB = b.sqrMagnitude; + + if (absA >= absB) + return a; + return b; + } + + private static float CalculateAbsMax(float a, float b) + { + float absA = (a >= 0) ? a : -a; + float absB = (b >= 0) ? b : -b; + + if (absA >= absB) + return a; + return b; + } + + private static Vector2 CalculateDeadzone(Vector2 a, float deadzone) + { + if (a.sqrMagnitude <= (deadzone * deadzone)) + return Vector2.zero; + + a *= ((a.magnitude - deadzone) / (1.0f - deadzone)); + + if (a.sqrMagnitude > 1.0f) + return a.normalized; + return a; + } + + private static float CalculateDeadzone(float a, float deadzone) + { + float mag = (a >= 0) ? a : -a; + + if (mag <= deadzone) + return 0.0f; + + a *= (mag - deadzone) / (1.0f - deadzone); + + if ((a * a) > 1.0f) + return (a >= 0) ? 1.0f : -1.0f; + return a; + } + + private static bool ShouldResolveController(Controller controllerType, Controller controllerMask) + { + bool isValid = false; + + if ((controllerType & controllerMask) == controllerType) + { + isValid = true; + } + + // If the mask requests both Touch controllers, reject the individual touch controllers. + if (((controllerMask & Controller.Touch) == Controller.Touch) + && ((controllerType & Controller.Touch) != 0) + && ((controllerType & Controller.Touch) != Controller.Touch)) + { + isValid = false; + } + + return isValid; + } + + private abstract class OVRControllerBase + { + public class VirtualButtonMap + { + public RawButton None = RawButton.None; + public RawButton One = RawButton.None; + public RawButton Two = RawButton.None; + public RawButton Three = RawButton.None; + public RawButton Four = RawButton.None; + public RawButton Start = RawButton.None; + public RawButton Back = RawButton.None; + public RawButton PrimaryShoulder = RawButton.None; + public RawButton PrimaryIndexTrigger = RawButton.None; + public RawButton PrimaryHandTrigger = RawButton.None; + public RawButton PrimaryThumbstick = RawButton.None; + public RawButton PrimaryThumbstickUp = RawButton.None; + public RawButton PrimaryThumbstickDown = RawButton.None; + public RawButton PrimaryThumbstickLeft = RawButton.None; + public RawButton PrimaryThumbstickRight = RawButton.None; + public RawButton PrimaryTouchpad = RawButton.None; + public RawButton SecondaryShoulder = RawButton.None; + public RawButton SecondaryIndexTrigger = RawButton.None; + public RawButton SecondaryHandTrigger = RawButton.None; + public RawButton SecondaryThumbstick = RawButton.None; + public RawButton SecondaryThumbstickUp = RawButton.None; + public RawButton SecondaryThumbstickDown = RawButton.None; + public RawButton SecondaryThumbstickLeft = RawButton.None; + public RawButton SecondaryThumbstickRight = RawButton.None; + public RawButton SecondaryTouchpad = RawButton.None; + public RawButton DpadUp = RawButton.None; + public RawButton DpadDown = RawButton.None; + public RawButton DpadLeft = RawButton.None; + public RawButton DpadRight = RawButton.None; + public RawButton Up = RawButton.None; + public RawButton Down = RawButton.None; + public RawButton Left = RawButton.None; + public RawButton Right = RawButton.None; + + public RawButton ToRawMask(Button virtualMask) + { + RawButton rawMask = 0; + + if (virtualMask == Button.None) + return RawButton.None; + + if ((virtualMask & Button.One) != 0) + rawMask |= One; + if ((virtualMask & Button.Two) != 0) + rawMask |= Two; + if ((virtualMask & Button.Three) != 0) + rawMask |= Three; + if ((virtualMask & Button.Four) != 0) + rawMask |= Four; + if ((virtualMask & Button.Start) != 0) + rawMask |= Start; + if ((virtualMask & Button.Back) != 0) + rawMask |= Back; + if ((virtualMask & Button.PrimaryShoulder) != 0) + rawMask |= PrimaryShoulder; + if ((virtualMask & Button.PrimaryIndexTrigger) != 0) + rawMask |= PrimaryIndexTrigger; + if ((virtualMask & Button.PrimaryHandTrigger) != 0) + rawMask |= PrimaryHandTrigger; + if ((virtualMask & Button.PrimaryThumbstick) != 0) + rawMask |= PrimaryThumbstick; + if ((virtualMask & Button.PrimaryThumbstickUp) != 0) + rawMask |= PrimaryThumbstickUp; + if ((virtualMask & Button.PrimaryThumbstickDown) != 0) + rawMask |= PrimaryThumbstickDown; + if ((virtualMask & Button.PrimaryThumbstickLeft) != 0) + rawMask |= PrimaryThumbstickLeft; + if ((virtualMask & Button.PrimaryThumbstickRight) != 0) + rawMask |= PrimaryThumbstickRight; + if ((virtualMask & Button.PrimaryTouchpad) != 0) + rawMask |= PrimaryTouchpad; + if ((virtualMask & Button.SecondaryShoulder) != 0) + rawMask |= SecondaryShoulder; + if ((virtualMask & Button.SecondaryIndexTrigger) != 0) + rawMask |= SecondaryIndexTrigger; + if ((virtualMask & Button.SecondaryHandTrigger) != 0) + rawMask |= SecondaryHandTrigger; + if ((virtualMask & Button.SecondaryThumbstick) != 0) + rawMask |= SecondaryThumbstick; + if ((virtualMask & Button.SecondaryThumbstickUp) != 0) + rawMask |= SecondaryThumbstickUp; + if ((virtualMask & Button.SecondaryThumbstickDown) != 0) + rawMask |= SecondaryThumbstickDown; + if ((virtualMask & Button.SecondaryThumbstickLeft) != 0) + rawMask |= SecondaryThumbstickLeft; + if ((virtualMask & Button.SecondaryThumbstickRight) != 0) + rawMask |= SecondaryThumbstickRight; + if ((virtualMask & Button.SecondaryTouchpad) != 0) + rawMask |= SecondaryTouchpad; + if ((virtualMask & Button.DpadUp) != 0) + rawMask |= DpadUp; + if ((virtualMask & Button.DpadDown) != 0) + rawMask |= DpadDown; + if ((virtualMask & Button.DpadLeft) != 0) + rawMask |= DpadLeft; + if ((virtualMask & Button.DpadRight) != 0) + rawMask |= DpadRight; + if ((virtualMask & Button.Up) != 0) + rawMask |= Up; + if ((virtualMask & Button.Down) != 0) + rawMask |= Down; + if ((virtualMask & Button.Left) != 0) + rawMask |= Left; + if ((virtualMask & Button.Right) != 0) + rawMask |= Right; + + return rawMask; + } + } + + public class VirtualTouchMap + { + public RawTouch None = RawTouch.None; + public RawTouch One = RawTouch.None; + public RawTouch Two = RawTouch.None; + public RawTouch Three = RawTouch.None; + public RawTouch Four = RawTouch.None; + public RawTouch PrimaryIndexTrigger = RawTouch.None; + public RawTouch PrimaryThumbstick = RawTouch.None; + public RawTouch PrimaryThumbRest = RawTouch.None; + public RawTouch PrimaryTouchpad = RawTouch.None; + public RawTouch SecondaryIndexTrigger = RawTouch.None; + public RawTouch SecondaryThumbstick = RawTouch.None; + public RawTouch SecondaryThumbRest = RawTouch.None; + public RawTouch SecondaryTouchpad = RawTouch.None; + + public RawTouch ToRawMask(Touch virtualMask) + { + RawTouch rawMask = 0; + + if (virtualMask == Touch.None) + return RawTouch.None; + + if ((virtualMask & Touch.One) != 0) + rawMask |= One; + if ((virtualMask & Touch.Two) != 0) + rawMask |= Two; + if ((virtualMask & Touch.Three) != 0) + rawMask |= Three; + if ((virtualMask & Touch.Four) != 0) + rawMask |= Four; + if ((virtualMask & Touch.PrimaryIndexTrigger) != 0) + rawMask |= PrimaryIndexTrigger; + if ((virtualMask & Touch.PrimaryThumbstick) != 0) + rawMask |= PrimaryThumbstick; + if ((virtualMask & Touch.PrimaryThumbRest) != 0) + rawMask |= PrimaryThumbRest; + if ((virtualMask & Touch.PrimaryTouchpad) != 0) + rawMask |= PrimaryTouchpad; + if ((virtualMask & Touch.SecondaryIndexTrigger) != 0) + rawMask |= SecondaryIndexTrigger; + if ((virtualMask & Touch.SecondaryThumbstick) != 0) + rawMask |= SecondaryThumbstick; + if ((virtualMask & Touch.SecondaryThumbRest) != 0) + rawMask |= SecondaryThumbRest; + if ((virtualMask & Touch.SecondaryTouchpad) != 0) + rawMask |= SecondaryTouchpad; + + return rawMask; + } + } + + public class VirtualNearTouchMap + { + public RawNearTouch None = RawNearTouch.None; + public RawNearTouch PrimaryIndexTrigger = RawNearTouch.None; + public RawNearTouch PrimaryThumbButtons = RawNearTouch.None; + public RawNearTouch SecondaryIndexTrigger = RawNearTouch.None; + public RawNearTouch SecondaryThumbButtons = RawNearTouch.None; + + public RawNearTouch ToRawMask(NearTouch virtualMask) + { + RawNearTouch rawMask = 0; + + if (virtualMask == NearTouch.None) + return RawNearTouch.None; + + if ((virtualMask & NearTouch.PrimaryIndexTrigger) != 0) + rawMask |= PrimaryIndexTrigger; + if ((virtualMask & NearTouch.PrimaryThumbButtons) != 0) + rawMask |= PrimaryThumbButtons; + if ((virtualMask & NearTouch.SecondaryIndexTrigger) != 0) + rawMask |= SecondaryIndexTrigger; + if ((virtualMask & NearTouch.SecondaryThumbButtons) != 0) + rawMask |= SecondaryThumbButtons; + + return rawMask; + } + } + + public class VirtualAxis1DMap + { + public RawAxis1D None = RawAxis1D.None; + public RawAxis1D PrimaryIndexTrigger = RawAxis1D.None; + public RawAxis1D PrimaryHandTrigger = RawAxis1D.None; + public RawAxis1D SecondaryIndexTrigger = RawAxis1D.None; + public RawAxis1D SecondaryHandTrigger = RawAxis1D.None; + + public RawAxis1D ToRawMask(Axis1D virtualMask) + { + RawAxis1D rawMask = 0; + + if (virtualMask == Axis1D.None) + return RawAxis1D.None; + + if ((virtualMask & Axis1D.PrimaryIndexTrigger) != 0) + rawMask |= PrimaryIndexTrigger; + if ((virtualMask & Axis1D.PrimaryHandTrigger) != 0) + rawMask |= PrimaryHandTrigger; + if ((virtualMask & Axis1D.SecondaryIndexTrigger) != 0) + rawMask |= SecondaryIndexTrigger; + if ((virtualMask & Axis1D.SecondaryHandTrigger) != 0) + rawMask |= SecondaryHandTrigger; + + return rawMask; + } + } + + public class VirtualAxis2DMap + { + public RawAxis2D None = RawAxis2D.None; + public RawAxis2D PrimaryThumbstick = RawAxis2D.None; + public RawAxis2D PrimaryTouchpad = RawAxis2D.None; + public RawAxis2D SecondaryThumbstick = RawAxis2D.None; + public RawAxis2D SecondaryTouchpad = RawAxis2D.None; + + public RawAxis2D ToRawMask(Axis2D virtualMask) + { + RawAxis2D rawMask = 0; + + if (virtualMask == Axis2D.None) + return RawAxis2D.None; + + if ((virtualMask & Axis2D.PrimaryThumbstick) != 0) + rawMask |= PrimaryThumbstick; + if ((virtualMask & Axis2D.PrimaryTouchpad) != 0) + rawMask |= PrimaryTouchpad; + if ((virtualMask & Axis2D.SecondaryThumbstick) != 0) + rawMask |= SecondaryThumbstick; + if ((virtualMask & Axis2D.SecondaryTouchpad) != 0) + rawMask |= SecondaryTouchpad; + + return rawMask; + } + } + + public Controller controllerType = Controller.None; + public VirtualButtonMap buttonMap = new VirtualButtonMap(); + public VirtualTouchMap touchMap = new VirtualTouchMap(); + public VirtualNearTouchMap nearTouchMap = new VirtualNearTouchMap(); + public VirtualAxis1DMap axis1DMap = new VirtualAxis1DMap(); + public VirtualAxis2DMap axis2DMap = new VirtualAxis2DMap(); + public OVRPlugin.ControllerState2 previousState = new OVRPlugin.ControllerState2(); + public OVRPlugin.ControllerState2 currentState = new OVRPlugin.ControllerState2(); + public bool shouldApplyDeadzone = true; + + public OVRControllerBase() + { + ConfigureButtonMap(); + ConfigureTouchMap(); + ConfigureNearTouchMap(); + ConfigureAxis1DMap(); + ConfigureAxis2DMap(); + } + + public virtual Controller Update() + { + OVRPlugin.ControllerState2 state = OVRPlugin.GetControllerState2((uint)controllerType); + //Debug.Log("MalibuManaged - " + (state.Touches & (uint)RawTouch.LTouchpad) + " " + state.LTouchpad.x + " " + state.LTouchpad.y + " " + state.RTouchpad.x + " " + state.RTouchpad.y); + + if (state.LIndexTrigger >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LIndexTrigger; + if (state.LHandTrigger >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LHandTrigger; + if (state.LThumbstick.y >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LThumbstickUp; + if (state.LThumbstick.y <= -AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LThumbstickDown; + if (state.LThumbstick.x <= -AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LThumbstickLeft; + if (state.LThumbstick.x >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LThumbstickRight; + + if (state.RIndexTrigger >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RIndexTrigger; + if (state.RHandTrigger >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RHandTrigger; + if (state.RThumbstick.y >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RThumbstickUp; + if (state.RThumbstick.y <= -AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RThumbstickDown; + if (state.RThumbstick.x <= -AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RThumbstickLeft; + if (state.RThumbstick.x >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RThumbstickRight; + + previousState = currentState; + currentState = state; + + return ((Controller)currentState.ConnectedControllers & controllerType); + } + + public virtual void SetControllerVibration(float frequency, float amplitude) + { + OVRPlugin.SetControllerVibration((uint)controllerType, frequency, amplitude); + } - private static bool GetResolvedNearTouchDown(NearTouch virtualMask, RawNearTouch rawMask, Controller controllerMask) - { - bool down = false; + public virtual void RecenterController() + { + OVRPlugin.RecenterTrackingOrigin(OVRPlugin.RecenterFlags.Controllers); + } - if ((controllerMask & Controller.Active) != 0) - controllerMask |= activeControllerType; + public abstract void ConfigureButtonMap(); + public abstract void ConfigureTouchMap(); + public abstract void ConfigureNearTouchMap(); + public abstract void ConfigureAxis1DMap(); + public abstract void ConfigureAxis2DMap(); - for (int i = 0; i < controllers.Count; i++) - { - OVRControllerBase controller = controllers[i]; + public RawButton ResolveToRawMask(Button virtualMask) + { + return buttonMap.ToRawMask(virtualMask); + } - if (ShouldResolveController(controller.controllerType, controllerMask)) - { - RawNearTouch resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); + public RawTouch ResolveToRawMask(Touch virtualMask) + { + return touchMap.ToRawMask(virtualMask); + } - if (((RawNearTouch)controller.previousState.NearTouches & resolvedMask) != 0) - { - return false; - } + public RawNearTouch ResolveToRawMask(NearTouch virtualMask) + { + return nearTouchMap.ToRawMask(virtualMask); + } - if ((((RawNearTouch)controller.currentState.NearTouches & resolvedMask) != 0) - && (((RawNearTouch)controller.previousState.NearTouches & resolvedMask) == 0)) - { - down = true; - } - } - } + public RawAxis1D ResolveToRawMask(Axis1D virtualMask) + { + return axis1DMap.ToRawMask(virtualMask); + } - return down; - } + public RawAxis2D ResolveToRawMask(Axis2D virtualMask) + { + return axis2DMap.ToRawMask(virtualMask); + } + } - /// - /// Gets the current up state of the given virtual near touch mask with the given controller mask. - /// Returns true if any masked near touch was released this frame on any masked controller and no other masked near touch is still down this frame. - /// - public static bool GetUp(NearTouch virtualMask, Controller controllerMask = Controller.Active) - { - return GetResolvedNearTouchUp(virtualMask, RawNearTouch.None, controllerMask); - } + private class OVRControllerTouch : OVRControllerBase + { + public OVRControllerTouch() + { + controllerType = Controller.Touch; + } - /// - /// Gets the current up state of the given raw near touch mask with the given controller mask. - /// Returns true if any masked near touch was released this frame on any masked controller and no other masked near touch is still down this frame. - /// - public static bool GetUp(RawNearTouch rawMask, Controller controllerMask = Controller.Active) - { - return GetResolvedNearTouchUp(NearTouch.None, rawMask, controllerMask); - } + public override void ConfigureButtonMap() + { + buttonMap.None = RawButton.None; + buttonMap.One = RawButton.A; + buttonMap.Two = RawButton.B; + buttonMap.Three = RawButton.X; + buttonMap.Four = RawButton.Y; + buttonMap.Start = RawButton.Start; + buttonMap.Back = RawButton.None; + buttonMap.PrimaryShoulder = RawButton.None; + buttonMap.PrimaryIndexTrigger = RawButton.LIndexTrigger; + buttonMap.PrimaryHandTrigger = RawButton.LHandTrigger; + buttonMap.PrimaryThumbstick = RawButton.LThumbstick; + buttonMap.PrimaryThumbstickUp = RawButton.LThumbstickUp; + buttonMap.PrimaryThumbstickDown = RawButton.LThumbstickDown; + buttonMap.PrimaryThumbstickLeft = RawButton.LThumbstickLeft; + buttonMap.PrimaryThumbstickRight = RawButton.LThumbstickRight; + buttonMap.PrimaryTouchpad = RawButton.None; + buttonMap.SecondaryShoulder = RawButton.None; + buttonMap.SecondaryIndexTrigger = RawButton.RIndexTrigger; + buttonMap.SecondaryHandTrigger = RawButton.RHandTrigger; + buttonMap.SecondaryThumbstick = RawButton.RThumbstick; + buttonMap.SecondaryThumbstickUp = RawButton.RThumbstickUp; + buttonMap.SecondaryThumbstickDown = RawButton.RThumbstickDown; + buttonMap.SecondaryThumbstickLeft = RawButton.RThumbstickLeft; + buttonMap.SecondaryThumbstickRight = RawButton.RThumbstickRight; + buttonMap.SecondaryTouchpad = RawButton.None; + buttonMap.DpadUp = RawButton.None; + buttonMap.DpadDown = RawButton.None; + buttonMap.DpadLeft = RawButton.None; + buttonMap.DpadRight = RawButton.None; + buttonMap.Up = RawButton.LThumbstickUp; + buttonMap.Down = RawButton.LThumbstickDown; + buttonMap.Left = RawButton.LThumbstickLeft; + buttonMap.Right = RawButton.LThumbstickRight; + } - private static bool GetResolvedNearTouchUp(NearTouch virtualMask, RawNearTouch rawMask, Controller controllerMask) - { - bool up = false; + public override void ConfigureTouchMap() + { + touchMap.None = RawTouch.None; + touchMap.One = RawTouch.A; + touchMap.Two = RawTouch.B; + touchMap.Three = RawTouch.X; + touchMap.Four = RawTouch.Y; + touchMap.PrimaryIndexTrigger = RawTouch.LIndexTrigger; + touchMap.PrimaryThumbstick = RawTouch.LThumbstick; + touchMap.PrimaryThumbRest = RawTouch.LThumbRest; + touchMap.PrimaryTouchpad = RawTouch.None; + touchMap.SecondaryIndexTrigger = RawTouch.RIndexTrigger; + touchMap.SecondaryThumbstick = RawTouch.RThumbstick; + touchMap.SecondaryThumbRest = RawTouch.RThumbRest; + touchMap.SecondaryTouchpad = RawTouch.None; + } - if ((controllerMask & Controller.Active) != 0) - controllerMask |= activeControllerType; + public override void ConfigureNearTouchMap() + { + nearTouchMap.None = RawNearTouch.None; + nearTouchMap.PrimaryIndexTrigger = RawNearTouch.LIndexTrigger; + nearTouchMap.PrimaryThumbButtons = RawNearTouch.LThumbButtons; + nearTouchMap.SecondaryIndexTrigger = RawNearTouch.RIndexTrigger; + nearTouchMap.SecondaryThumbButtons = RawNearTouch.RThumbButtons; + } - for (int i = 0; i < controllers.Count; i++) - { - OVRControllerBase controller = controllers[i]; + public override void ConfigureAxis1DMap() + { + axis1DMap.None = RawAxis1D.None; + axis1DMap.PrimaryIndexTrigger = RawAxis1D.LIndexTrigger; + axis1DMap.PrimaryHandTrigger = RawAxis1D.LHandTrigger; + axis1DMap.SecondaryIndexTrigger = RawAxis1D.RIndexTrigger; + axis1DMap.SecondaryHandTrigger = RawAxis1D.RHandTrigger; + } - if (ShouldResolveController(controller.controllerType, controllerMask)) - { - RawNearTouch resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); + public override void ConfigureAxis2DMap() + { + axis2DMap.None = RawAxis2D.None; + axis2DMap.PrimaryThumbstick = RawAxis2D.LThumbstick; + axis2DMap.PrimaryTouchpad = RawAxis2D.None; + axis2DMap.SecondaryThumbstick = RawAxis2D.RThumbstick; + axis2DMap.SecondaryTouchpad = RawAxis2D.None; + } + } - if (((RawNearTouch)controller.currentState.NearTouches & resolvedMask) != 0) - { - return false; - } + private class OVRControllerLTouch : OVRControllerBase + { + public OVRControllerLTouch() + { + controllerType = Controller.LTouch; + } - if ((((RawNearTouch)controller.currentState.NearTouches & resolvedMask) == 0) - && (((RawNearTouch)controller.previousState.NearTouches & resolvedMask) != 0)) - { - up = true; - } - } - } + public override void ConfigureButtonMap() + { + buttonMap.None = RawButton.None; + buttonMap.One = RawButton.X; + buttonMap.Two = RawButton.Y; + buttonMap.Three = RawButton.None; + buttonMap.Four = RawButton.None; + buttonMap.Start = RawButton.Start; + buttonMap.Back = RawButton.None; + buttonMap.PrimaryShoulder = RawButton.None; + buttonMap.PrimaryIndexTrigger = RawButton.LIndexTrigger; + buttonMap.PrimaryHandTrigger = RawButton.LHandTrigger; + buttonMap.PrimaryThumbstick = RawButton.LThumbstick; + buttonMap.PrimaryThumbstickUp = RawButton.LThumbstickUp; + buttonMap.PrimaryThumbstickDown = RawButton.LThumbstickDown; + buttonMap.PrimaryThumbstickLeft = RawButton.LThumbstickLeft; + buttonMap.PrimaryThumbstickRight = RawButton.LThumbstickRight; + buttonMap.PrimaryTouchpad = RawButton.None; + buttonMap.SecondaryShoulder = RawButton.None; + buttonMap.SecondaryIndexTrigger = RawButton.None; + buttonMap.SecondaryHandTrigger = RawButton.None; + buttonMap.SecondaryThumbstick = RawButton.None; + buttonMap.SecondaryThumbstickUp = RawButton.None; + buttonMap.SecondaryThumbstickDown = RawButton.None; + buttonMap.SecondaryThumbstickLeft = RawButton.None; + buttonMap.SecondaryThumbstickRight = RawButton.None; + buttonMap.SecondaryTouchpad = RawButton.None; + buttonMap.DpadUp = RawButton.None; + buttonMap.DpadDown = RawButton.None; + buttonMap.DpadLeft = RawButton.None; + buttonMap.DpadRight = RawButton.None; + buttonMap.Up = RawButton.LThumbstickUp; + buttonMap.Down = RawButton.LThumbstickDown; + buttonMap.Left = RawButton.LThumbstickLeft; + buttonMap.Right = RawButton.LThumbstickRight; + } - return up; - } + public override void ConfigureTouchMap() + { + touchMap.None = RawTouch.None; + touchMap.One = RawTouch.X; + touchMap.Two = RawTouch.Y; + touchMap.Three = RawTouch.None; + touchMap.Four = RawTouch.None; + touchMap.PrimaryIndexTrigger = RawTouch.LIndexTrigger; + touchMap.PrimaryThumbstick = RawTouch.LThumbstick; + touchMap.PrimaryThumbRest = RawTouch.LThumbRest; + touchMap.PrimaryTouchpad = RawTouch.None; + touchMap.SecondaryIndexTrigger = RawTouch.None; + touchMap.SecondaryThumbstick = RawTouch.None; + touchMap.SecondaryThumbRest = RawTouch.None; + touchMap.SecondaryTouchpad = RawTouch.None; + } - /// - /// Gets the current state of the given virtual 1-dimensional axis mask on the given controller mask. - /// Returns the value of the largest masked axis across all masked controllers. Values range from 0 to 1. - /// - public static float Get(Axis1D virtualMask, Controller controllerMask = Controller.Active) - { - return GetResolvedAxis1D(virtualMask, RawAxis1D.None, controllerMask); - } + public override void ConfigureNearTouchMap() + { + nearTouchMap.None = RawNearTouch.None; + nearTouchMap.PrimaryIndexTrigger = RawNearTouch.LIndexTrigger; + nearTouchMap.PrimaryThumbButtons = RawNearTouch.LThumbButtons; + nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; + nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; + } - /// - /// Gets the current state of the given raw 1-dimensional axis mask on the given controller mask. - /// Returns the value of the largest masked axis across all masked controllers. Values range from 0 to 1. - /// - public static float Get(RawAxis1D rawMask, Controller controllerMask = Controller.Active) - { - return GetResolvedAxis1D(Axis1D.None, rawMask, controllerMask); - } + public override void ConfigureAxis1DMap() + { + axis1DMap.None = RawAxis1D.None; + axis1DMap.PrimaryIndexTrigger = RawAxis1D.LIndexTrigger; + axis1DMap.PrimaryHandTrigger = RawAxis1D.LHandTrigger; + axis1DMap.SecondaryIndexTrigger = RawAxis1D.None; + axis1DMap.SecondaryHandTrigger = RawAxis1D.None; + } - private static float GetResolvedAxis1D(Axis1D virtualMask, RawAxis1D rawMask, Controller controllerMask) - { - float maxAxis = 0.0f; + public override void ConfigureAxis2DMap() + { + axis2DMap.None = RawAxis2D.None; + axis2DMap.PrimaryThumbstick = RawAxis2D.LThumbstick; + axis2DMap.PrimaryTouchpad = RawAxis2D.None; + axis2DMap.SecondaryThumbstick = RawAxis2D.None; + axis2DMap.SecondaryTouchpad = RawAxis2D.None; + } + } - if ((controllerMask & Controller.Active) != 0) - controllerMask |= activeControllerType; + private class OVRControllerRTouch : OVRControllerBase + { + public OVRControllerRTouch() + { + controllerType = Controller.RTouch; + } - for (int i = 0; i < controllers.Count; i++) - { - OVRControllerBase controller = controllers[i]; + public override void ConfigureButtonMap() + { + buttonMap.None = RawButton.None; + buttonMap.One = RawButton.A; + buttonMap.Two = RawButton.B; + buttonMap.Three = RawButton.None; + buttonMap.Four = RawButton.None; + buttonMap.Start = RawButton.None; + buttonMap.Back = RawButton.None; + buttonMap.PrimaryShoulder = RawButton.None; + buttonMap.PrimaryIndexTrigger = RawButton.RIndexTrigger; + buttonMap.PrimaryHandTrigger = RawButton.RHandTrigger; + buttonMap.PrimaryThumbstick = RawButton.RThumbstick; + buttonMap.PrimaryThumbstickUp = RawButton.RThumbstickUp; + buttonMap.PrimaryThumbstickDown = RawButton.RThumbstickDown; + buttonMap.PrimaryThumbstickLeft = RawButton.RThumbstickLeft; + buttonMap.PrimaryThumbstickRight = RawButton.RThumbstickRight; + buttonMap.PrimaryTouchpad = RawButton.None; + buttonMap.SecondaryShoulder = RawButton.None; + buttonMap.SecondaryIndexTrigger = RawButton.None; + buttonMap.SecondaryHandTrigger = RawButton.None; + buttonMap.SecondaryThumbstick = RawButton.None; + buttonMap.SecondaryThumbstickUp = RawButton.None; + buttonMap.SecondaryThumbstickDown = RawButton.None; + buttonMap.SecondaryThumbstickLeft = RawButton.None; + buttonMap.SecondaryThumbstickRight = RawButton.None; + buttonMap.SecondaryTouchpad = RawButton.None; + buttonMap.DpadUp = RawButton.None; + buttonMap.DpadDown = RawButton.None; + buttonMap.DpadLeft = RawButton.None; + buttonMap.DpadRight = RawButton.None; + buttonMap.Up = RawButton.RThumbstickUp; + buttonMap.Down = RawButton.RThumbstickDown; + buttonMap.Left = RawButton.RThumbstickLeft; + buttonMap.Right = RawButton.RThumbstickRight; + } - if (ShouldResolveController(controller.controllerType, controllerMask)) - { - RawAxis1D resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); + public override void ConfigureTouchMap() + { + touchMap.None = RawTouch.None; + touchMap.One = RawTouch.A; + touchMap.Two = RawTouch.B; + touchMap.Three = RawTouch.None; + touchMap.Four = RawTouch.None; + touchMap.PrimaryIndexTrigger = RawTouch.RIndexTrigger; + touchMap.PrimaryThumbstick = RawTouch.RThumbstick; + touchMap.PrimaryThumbRest = RawTouch.RThumbRest; + touchMap.PrimaryTouchpad = RawTouch.None; + touchMap.SecondaryIndexTrigger = RawTouch.None; + touchMap.SecondaryThumbstick = RawTouch.None; + touchMap.SecondaryThumbRest = RawTouch.None; + touchMap.SecondaryTouchpad = RawTouch.None; + } - if ((RawAxis1D.LIndexTrigger & resolvedMask) != 0) - { - float axis = controller.currentState.LIndexTrigger; + public override void ConfigureNearTouchMap() + { + nearTouchMap.None = RawNearTouch.None; + nearTouchMap.PrimaryIndexTrigger = RawNearTouch.RIndexTrigger; + nearTouchMap.PrimaryThumbButtons = RawNearTouch.RThumbButtons; + nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; + nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; + } - if (controller.shouldApplyDeadzone) - axis = CalculateDeadzone(axis, AXIS_DEADZONE_THRESHOLD); + public override void ConfigureAxis1DMap() + { + axis1DMap.None = RawAxis1D.None; + axis1DMap.PrimaryIndexTrigger = RawAxis1D.RIndexTrigger; + axis1DMap.PrimaryHandTrigger = RawAxis1D.RHandTrigger; + axis1DMap.SecondaryIndexTrigger = RawAxis1D.None; + axis1DMap.SecondaryHandTrigger = RawAxis1D.None; + } - maxAxis = CalculateAbsMax(maxAxis, axis); - } - if ((RawAxis1D.RIndexTrigger & resolvedMask) != 0) - { - float axis = controller.currentState.RIndexTrigger; + public override void ConfigureAxis2DMap() + { + axis2DMap.None = RawAxis2D.None; + axis2DMap.PrimaryThumbstick = RawAxis2D.RThumbstick; + axis2DMap.PrimaryTouchpad = RawAxis2D.None; + axis2DMap.SecondaryThumbstick = RawAxis2D.None; + axis2DMap.SecondaryTouchpad = RawAxis2D.None; + } + } - if (controller.shouldApplyDeadzone) - axis = CalculateDeadzone(axis, AXIS_DEADZONE_THRESHOLD); + private class OVRControllerRemote : OVRControllerBase + { + public OVRControllerRemote() + { + controllerType = Controller.Remote; + } - maxAxis = CalculateAbsMax(maxAxis, axis); - } - if ((RawAxis1D.LHandTrigger & resolvedMask) != 0) - { - float axis = controller.currentState.LHandTrigger; + public override void ConfigureButtonMap() + { + buttonMap.None = RawButton.None; + buttonMap.One = RawButton.Start; + buttonMap.Two = RawButton.Back; + buttonMap.Three = RawButton.None; + buttonMap.Four = RawButton.None; + buttonMap.Start = RawButton.Start; + buttonMap.Back = RawButton.Back; + buttonMap.PrimaryShoulder = RawButton.None; + buttonMap.PrimaryIndexTrigger = RawButton.None; + buttonMap.PrimaryHandTrigger = RawButton.None; + buttonMap.PrimaryThumbstick = RawButton.None; + buttonMap.PrimaryThumbstickUp = RawButton.None; + buttonMap.PrimaryThumbstickDown = RawButton.None; + buttonMap.PrimaryThumbstickLeft = RawButton.None; + buttonMap.PrimaryThumbstickRight = RawButton.None; + buttonMap.PrimaryTouchpad = RawButton.None; + buttonMap.SecondaryShoulder = RawButton.None; + buttonMap.SecondaryIndexTrigger = RawButton.None; + buttonMap.SecondaryHandTrigger = RawButton.None; + buttonMap.SecondaryThumbstick = RawButton.None; + buttonMap.SecondaryThumbstickUp = RawButton.None; + buttonMap.SecondaryThumbstickDown = RawButton.None; + buttonMap.SecondaryThumbstickLeft = RawButton.None; + buttonMap.SecondaryThumbstickRight = RawButton.None; + buttonMap.SecondaryTouchpad = RawButton.None; + buttonMap.DpadUp = RawButton.DpadUp; + buttonMap.DpadDown = RawButton.DpadDown; + buttonMap.DpadLeft = RawButton.DpadLeft; + buttonMap.DpadRight = RawButton.DpadRight; + buttonMap.Up = RawButton.DpadUp; + buttonMap.Down = RawButton.DpadDown; + buttonMap.Left = RawButton.DpadLeft; + buttonMap.Right = RawButton.DpadRight; + } - if (controller.shouldApplyDeadzone) - axis = CalculateDeadzone(axis, AXIS_DEADZONE_THRESHOLD); + public override void ConfigureTouchMap() + { + touchMap.None = RawTouch.None; + touchMap.One = RawTouch.None; + touchMap.Two = RawTouch.None; + touchMap.Three = RawTouch.None; + touchMap.Four = RawTouch.None; + touchMap.PrimaryIndexTrigger = RawTouch.None; + touchMap.PrimaryThumbstick = RawTouch.None; + touchMap.PrimaryThumbRest = RawTouch.None; + touchMap.PrimaryTouchpad = RawTouch.None; + touchMap.SecondaryIndexTrigger = RawTouch.None; + touchMap.SecondaryThumbstick = RawTouch.None; + touchMap.SecondaryThumbRest = RawTouch.None; + touchMap.SecondaryTouchpad = RawTouch.None; + } - maxAxis = CalculateAbsMax(maxAxis, axis); - } - if ((RawAxis1D.RHandTrigger & resolvedMask) != 0) - { - float axis = controller.currentState.RHandTrigger; + public override void ConfigureNearTouchMap() + { + nearTouchMap.None = RawNearTouch.None; + nearTouchMap.PrimaryIndexTrigger = RawNearTouch.None; + nearTouchMap.PrimaryThumbButtons = RawNearTouch.None; + nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; + nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; + } - if (controller.shouldApplyDeadzone) - axis = CalculateDeadzone(axis, AXIS_DEADZONE_THRESHOLD); + public override void ConfigureAxis1DMap() + { + axis1DMap.None = RawAxis1D.None; + axis1DMap.PrimaryIndexTrigger = RawAxis1D.None; + axis1DMap.PrimaryHandTrigger = RawAxis1D.None; + axis1DMap.SecondaryIndexTrigger = RawAxis1D.None; + axis1DMap.SecondaryHandTrigger = RawAxis1D.None; + } - maxAxis = CalculateAbsMax(maxAxis, axis); - } - } - } + public override void ConfigureAxis2DMap() + { + axis2DMap.None = RawAxis2D.None; + axis2DMap.PrimaryThumbstick = RawAxis2D.None; + axis2DMap.PrimaryTouchpad = RawAxis2D.None; + axis2DMap.SecondaryThumbstick = RawAxis2D.None; + axis2DMap.SecondaryTouchpad = RawAxis2D.None; + } + } - return maxAxis; - } + private class OVRControllerGamepadPC : OVRControllerBase + { + public OVRControllerGamepadPC() + { + controllerType = Controller.Gamepad; + } - /// - /// Gets the current state of the given virtual 2-dimensional axis mask on the given controller mask. - /// Returns the vector of the largest masked axis across all masked controllers. Values range from -1 to 1. - /// - public static Vector2 Get(Axis2D virtualMask, Controller controllerMask = Controller.Active) - { - return GetResolvedAxis2D(virtualMask, RawAxis2D.None, controllerMask); - } + public override void ConfigureButtonMap() + { + buttonMap.None = RawButton.None; + buttonMap.One = RawButton.A; + buttonMap.Two = RawButton.B; + buttonMap.Three = RawButton.X; + buttonMap.Four = RawButton.Y; + buttonMap.Start = RawButton.Start; + buttonMap.Back = RawButton.Back; + buttonMap.PrimaryShoulder = RawButton.LShoulder; + buttonMap.PrimaryIndexTrigger = RawButton.LIndexTrigger; + buttonMap.PrimaryHandTrigger = RawButton.None; + buttonMap.PrimaryThumbstick = RawButton.LThumbstick; + buttonMap.PrimaryThumbstickUp = RawButton.LThumbstickUp; + buttonMap.PrimaryThumbstickDown = RawButton.LThumbstickDown; + buttonMap.PrimaryThumbstickLeft = RawButton.LThumbstickLeft; + buttonMap.PrimaryThumbstickRight = RawButton.LThumbstickRight; + buttonMap.PrimaryTouchpad = RawButton.None; + buttonMap.SecondaryShoulder = RawButton.RShoulder; + buttonMap.SecondaryIndexTrigger = RawButton.RIndexTrigger; + buttonMap.SecondaryHandTrigger = RawButton.None; + buttonMap.SecondaryThumbstick = RawButton.RThumbstick; + buttonMap.SecondaryThumbstickUp = RawButton.RThumbstickUp; + buttonMap.SecondaryThumbstickDown = RawButton.RThumbstickDown; + buttonMap.SecondaryThumbstickLeft = RawButton.RThumbstickLeft; + buttonMap.SecondaryThumbstickRight = RawButton.RThumbstickRight; + buttonMap.SecondaryTouchpad = RawButton.None; + buttonMap.DpadUp = RawButton.DpadUp; + buttonMap.DpadDown = RawButton.DpadDown; + buttonMap.DpadLeft = RawButton.DpadLeft; + buttonMap.DpadRight = RawButton.DpadRight; + buttonMap.Up = RawButton.LThumbstickUp; + buttonMap.Down = RawButton.LThumbstickDown; + buttonMap.Left = RawButton.LThumbstickLeft; + buttonMap.Right = RawButton.LThumbstickRight; + } - /// - /// Gets the current state of the given raw 2-dimensional axis mask on the given controller mask. - /// Returns the vector of the largest masked axis across all masked controllers. Values range from -1 to 1. - /// - public static Vector2 Get(RawAxis2D rawMask, Controller controllerMask = Controller.Active) - { - return GetResolvedAxis2D(Axis2D.None, rawMask, controllerMask); - } + public override void ConfigureTouchMap() + { + touchMap.None = RawTouch.None; + touchMap.One = RawTouch.None; + touchMap.Two = RawTouch.None; + touchMap.Three = RawTouch.None; + touchMap.Four = RawTouch.None; + touchMap.PrimaryIndexTrigger = RawTouch.None; + touchMap.PrimaryThumbstick = RawTouch.None; + touchMap.PrimaryThumbRest = RawTouch.None; + touchMap.PrimaryTouchpad = RawTouch.None; + touchMap.SecondaryIndexTrigger = RawTouch.None; + touchMap.SecondaryThumbstick = RawTouch.None; + touchMap.SecondaryThumbRest = RawTouch.None; + touchMap.SecondaryTouchpad = RawTouch.None; + } - private static Vector2 GetResolvedAxis2D(Axis2D virtualMask, RawAxis2D rawMask, Controller controllerMask) - { - Vector2 maxAxis = Vector2.zero; - - if ((controllerMask & Controller.Active) != 0) - controllerMask |= activeControllerType; - - for (int i = 0; i < controllers.Count; i++) - { - OVRControllerBase controller = controllers[i]; - - if (ShouldResolveController(controller.controllerType, controllerMask)) - { - RawAxis2D resolvedMask = rawMask | controller.ResolveToRawMask(virtualMask); - - if ((RawAxis2D.LThumbstick & resolvedMask) != 0) - { - Vector2 axis = new Vector2( - controller.currentState.LThumbstick.x, - controller.currentState.LThumbstick.y); - - if (controller.shouldApplyDeadzone) - axis = CalculateDeadzone(axis, AXIS_DEADZONE_THRESHOLD); - - maxAxis = CalculateAbsMax(maxAxis, axis); - } - if ((RawAxis2D.LTouchpad & resolvedMask) != 0) - { - Vector2 axis = new Vector2( - controller.currentState.LTouchpad.x, - controller.currentState.LTouchpad.y); - - //if (controller.shouldApplyDeadzone) - // axis = CalculateDeadzone(axis, AXIS_DEADZONE_THRESHOLD); - - maxAxis = CalculateAbsMax(maxAxis, axis); - } - if ((RawAxis2D.RThumbstick & resolvedMask) != 0) - { - Vector2 axis = new Vector2( - controller.currentState.RThumbstick.x, - controller.currentState.RThumbstick.y); - - if (controller.shouldApplyDeadzone) - axis = CalculateDeadzone(axis, AXIS_DEADZONE_THRESHOLD); - - maxAxis = CalculateAbsMax(maxAxis, axis); - } - if ((RawAxis2D.RTouchpad & resolvedMask) != 0) - { - Vector2 axis = new Vector2( - controller.currentState.RTouchpad.x, - controller.currentState.RTouchpad.y); - - //if (controller.shouldApplyDeadzone) - // axis = CalculateDeadzone(axis, AXIS_DEADZONE_THRESHOLD); - - maxAxis = CalculateAbsMax(maxAxis, axis); - } - } - } - - return maxAxis; - } + public override void ConfigureNearTouchMap() + { + nearTouchMap.None = RawNearTouch.None; + nearTouchMap.PrimaryIndexTrigger = RawNearTouch.None; + nearTouchMap.PrimaryThumbButtons = RawNearTouch.None; + nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; + nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; + } - /// - /// Returns a mask of all currently connected controller types. - /// - public static Controller GetConnectedControllers() - { - return connectedControllerTypes; - } - - /// - /// Returns true if the specified controller type is currently connected. - /// - public static bool IsControllerConnected(Controller controller) - { - return (connectedControllerTypes & controller) == controller; - } + public override void ConfigureAxis1DMap() + { + axis1DMap.None = RawAxis1D.None; + axis1DMap.PrimaryIndexTrigger = RawAxis1D.LIndexTrigger; + axis1DMap.PrimaryHandTrigger = RawAxis1D.None; + axis1DMap.SecondaryIndexTrigger = RawAxis1D.RIndexTrigger; + axis1DMap.SecondaryHandTrigger = RawAxis1D.None; + } - /// - /// Returns the current active controller type. - /// - public static Controller GetActiveController() - { - return activeControllerType; - } + public override void ConfigureAxis2DMap() + { + axis2DMap.None = RawAxis2D.None; + axis2DMap.PrimaryThumbstick = RawAxis2D.LThumbstick; + axis2DMap.PrimaryTouchpad = RawAxis2D.None; + axis2DMap.SecondaryThumbstick = RawAxis2D.RThumbstick; + axis2DMap.SecondaryTouchpad = RawAxis2D.None; + } + } - /// - /// Activates vibration with the given frequency and amplitude with the given controller mask. - /// Ignored on controllers that do not support vibration. Expected values range from 0 to 1. - /// - public static void SetControllerVibration(float frequency, float amplitude, Controller controllerMask = Controller.Active) - { - if ((controllerMask & Controller.Active) != 0) - controllerMask |= activeControllerType; - - for (int i = 0; i < controllers.Count; i++) - { - OVRControllerBase controller = controllers[i]; - - if (ShouldResolveController(controller.controllerType, controllerMask)) - { - controller.SetControllerVibration(frequency, amplitude); - } - } - } + private class OVRControllerGamepadMac : OVRControllerBase + { + /// An axis on the gamepad. + private enum AxisGPC + { + None = -1, + LeftXAxis = 0, + LeftYAxis, + RightXAxis, + RightYAxis, + LeftTrigger, + RightTrigger, + DPad_X_Axis, + DPad_Y_Axis, + Max, + }; + + /// A button on the gamepad. + public enum ButtonGPC + { + None = -1, + A = 0, + B, + X, + Y, + Up, + Down, + Left, + Right, + Start, + Back, + LStick, + RStick, + LeftShoulder, + RightShoulder, + Max + }; + + private bool initialized = false; + + public OVRControllerGamepadMac() + { + controllerType = Controller.Gamepad; - public static void RecenterController(Controller controllerMask = Controller.Active) - { - if ((controllerMask & Controller.Active) != 0) - controllerMask |= activeControllerType; + initialized = OVR_GamepadController_Initialize(); + } - for (int i = 0; i < controllers.Count; i++) - { - OVRControllerBase controller = controllers[i]; + ~OVRControllerGamepadMac() + { + if (!initialized) + return; - if (ShouldResolveController(controller.controllerType, controllerMask)) - { - controller.RecenterController(); - } - } - } + OVR_GamepadController_Destroy(); + } - private static Vector2 CalculateAbsMax(Vector2 a, Vector2 b) - { - float absA = a.sqrMagnitude; - float absB = b.sqrMagnitude; + public override Controller Update() + { + if (!initialized) + { + return Controller.None; + } - if (absA >= absB) - return a; - return b; - } + OVRPlugin.ControllerState2 state = new OVRPlugin.ControllerState2(); + + bool result = OVR_GamepadController_Update(); + + if (result) + state.ConnectedControllers = (uint)Controller.Gamepad; + + if (OVR_GamepadController_GetButton((int)ButtonGPC.A)) + state.Buttons |= (uint)RawButton.A; + if (OVR_GamepadController_GetButton((int)ButtonGPC.B)) + state.Buttons |= (uint)RawButton.B; + if (OVR_GamepadController_GetButton((int)ButtonGPC.X)) + state.Buttons |= (uint)RawButton.X; + if (OVR_GamepadController_GetButton((int)ButtonGPC.Y)) + state.Buttons |= (uint)RawButton.Y; + if (OVR_GamepadController_GetButton((int)ButtonGPC.Up)) + state.Buttons |= (uint)RawButton.DpadUp; + if (OVR_GamepadController_GetButton((int)ButtonGPC.Down)) + state.Buttons |= (uint)RawButton.DpadDown; + if (OVR_GamepadController_GetButton((int)ButtonGPC.Left)) + state.Buttons |= (uint)RawButton.DpadLeft; + if (OVR_GamepadController_GetButton((int)ButtonGPC.Right)) + state.Buttons |= (uint)RawButton.DpadRight; + if (OVR_GamepadController_GetButton((int)ButtonGPC.Start)) + state.Buttons |= (uint)RawButton.Start; + if (OVR_GamepadController_GetButton((int)ButtonGPC.Back)) + state.Buttons |= (uint)RawButton.Back; + if (OVR_GamepadController_GetButton((int)ButtonGPC.LStick)) + state.Buttons |= (uint)RawButton.LThumbstick; + if (OVR_GamepadController_GetButton((int)ButtonGPC.RStick)) + state.Buttons |= (uint)RawButton.RThumbstick; + if (OVR_GamepadController_GetButton((int)ButtonGPC.LeftShoulder)) + state.Buttons |= (uint)RawButton.LShoulder; + if (OVR_GamepadController_GetButton((int)ButtonGPC.RightShoulder)) + state.Buttons |= (uint)RawButton.RShoulder; + + state.LThumbstick.x = OVR_GamepadController_GetAxis((int)AxisGPC.LeftXAxis); + state.LThumbstick.y = OVR_GamepadController_GetAxis((int)AxisGPC.LeftYAxis); + state.RThumbstick.x = OVR_GamepadController_GetAxis((int)AxisGPC.RightXAxis); + state.RThumbstick.y = OVR_GamepadController_GetAxis((int)AxisGPC.RightYAxis); + state.LIndexTrigger = OVR_GamepadController_GetAxis((int)AxisGPC.LeftTrigger); + state.RIndexTrigger = OVR_GamepadController_GetAxis((int)AxisGPC.RightTrigger); + + if (state.LIndexTrigger >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LIndexTrigger; + if (state.LHandTrigger >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LHandTrigger; + if (state.LThumbstick.y >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LThumbstickUp; + if (state.LThumbstick.y <= -AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LThumbstickDown; + if (state.LThumbstick.x <= -AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LThumbstickLeft; + if (state.LThumbstick.x >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LThumbstickRight; + + if (state.RIndexTrigger >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RIndexTrigger; + if (state.RHandTrigger >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RHandTrigger; + if (state.RThumbstick.y >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RThumbstickUp; + if (state.RThumbstick.y <= -AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RThumbstickDown; + if (state.RThumbstick.x <= -AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RThumbstickLeft; + if (state.RThumbstick.x >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RThumbstickRight; + + previousState = currentState; + currentState = state; + + return ((Controller)currentState.ConnectedControllers & controllerType); + } - private static float CalculateAbsMax(float a, float b) - { - float absA = (a >= 0) ? a : -a; - float absB = (b >= 0) ? b : -b; + public override void ConfigureButtonMap() + { + buttonMap.None = RawButton.None; + buttonMap.One = RawButton.A; + buttonMap.Two = RawButton.B; + buttonMap.Three = RawButton.X; + buttonMap.Four = RawButton.Y; + buttonMap.Start = RawButton.Start; + buttonMap.Back = RawButton.Back; + buttonMap.PrimaryShoulder = RawButton.LShoulder; + buttonMap.PrimaryIndexTrigger = RawButton.LIndexTrigger; + buttonMap.PrimaryHandTrigger = RawButton.None; + buttonMap.PrimaryThumbstick = RawButton.LThumbstick; + buttonMap.PrimaryThumbstickUp = RawButton.LThumbstickUp; + buttonMap.PrimaryThumbstickDown = RawButton.LThumbstickDown; + buttonMap.PrimaryThumbstickLeft = RawButton.LThumbstickLeft; + buttonMap.PrimaryThumbstickRight = RawButton.LThumbstickRight; + buttonMap.PrimaryTouchpad = RawButton.None; + buttonMap.SecondaryShoulder = RawButton.RShoulder; + buttonMap.SecondaryIndexTrigger = RawButton.RIndexTrigger; + buttonMap.SecondaryHandTrigger = RawButton.None; + buttonMap.SecondaryThumbstick = RawButton.RThumbstick; + buttonMap.SecondaryThumbstickUp = RawButton.RThumbstickUp; + buttonMap.SecondaryThumbstickDown = RawButton.RThumbstickDown; + buttonMap.SecondaryThumbstickLeft = RawButton.RThumbstickLeft; + buttonMap.SecondaryThumbstickRight = RawButton.RThumbstickRight; + buttonMap.SecondaryTouchpad = RawButton.None; + buttonMap.DpadUp = RawButton.DpadUp; + buttonMap.DpadDown = RawButton.DpadDown; + buttonMap.DpadLeft = RawButton.DpadLeft; + buttonMap.DpadRight = RawButton.DpadRight; + buttonMap.Up = RawButton.LThumbstickUp; + buttonMap.Down = RawButton.LThumbstickDown; + buttonMap.Left = RawButton.LThumbstickLeft; + buttonMap.Right = RawButton.LThumbstickRight; + } - if (absA >= absB) - return a; - return b; - } + public override void ConfigureTouchMap() + { + touchMap.None = RawTouch.None; + touchMap.One = RawTouch.None; + touchMap.Two = RawTouch.None; + touchMap.Three = RawTouch.None; + touchMap.Four = RawTouch.None; + touchMap.PrimaryIndexTrigger = RawTouch.None; + touchMap.PrimaryThumbstick = RawTouch.None; + touchMap.PrimaryThumbRest = RawTouch.None; + touchMap.PrimaryTouchpad = RawTouch.None; + touchMap.SecondaryIndexTrigger = RawTouch.None; + touchMap.SecondaryThumbstick = RawTouch.None; + touchMap.SecondaryThumbRest = RawTouch.None; + touchMap.SecondaryTouchpad = RawTouch.None; + } - private static Vector2 CalculateDeadzone(Vector2 a, float deadzone) - { - if (a.sqrMagnitude <= (deadzone * deadzone)) - return Vector2.zero; + public override void ConfigureNearTouchMap() + { + nearTouchMap.None = RawNearTouch.None; + nearTouchMap.PrimaryIndexTrigger = RawNearTouch.None; + nearTouchMap.PrimaryThumbButtons = RawNearTouch.None; + nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; + nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; + } - a *= ((a.magnitude - deadzone) / (1.0f - deadzone)); + public override void ConfigureAxis1DMap() + { + axis1DMap.None = RawAxis1D.None; + axis1DMap.PrimaryIndexTrigger = RawAxis1D.LIndexTrigger; + axis1DMap.PrimaryHandTrigger = RawAxis1D.None; + axis1DMap.SecondaryIndexTrigger = RawAxis1D.RIndexTrigger; + axis1DMap.SecondaryHandTrigger = RawAxis1D.None; + } - if (a.sqrMagnitude > 1.0f) - return a.normalized; - return a; - } + public override void ConfigureAxis2DMap() + { + axis2DMap.None = RawAxis2D.None; + axis2DMap.PrimaryThumbstick = RawAxis2D.LThumbstick; + axis2DMap.PrimaryTouchpad = RawAxis2D.None; + axis2DMap.SecondaryThumbstick = RawAxis2D.RThumbstick; + axis2DMap.SecondaryTouchpad = RawAxis2D.None; + } - private static float CalculateDeadzone(float a, float deadzone) - { - float mag = (a >= 0) ? a : -a; + public override void SetControllerVibration(float frequency, float amplitude) + { + int gpcNode = 0; + float gpcFrequency = frequency * 200.0f; //Map frequency from 0-1 CAPI range to 0-200 GPC range + float gpcStrength = amplitude; - if (mag <= deadzone) - return 0.0f; + OVR_GamepadController_SetVibration(gpcNode, gpcStrength, gpcFrequency); + } - a *= (mag - deadzone) / (1.0f - deadzone); + private const string DllName = "OVRGamepad"; + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + private static extern bool OVR_GamepadController_Initialize(); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + private static extern bool OVR_GamepadController_Destroy(); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + private static extern bool OVR_GamepadController_Update(); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + private static extern float OVR_GamepadController_GetAxis(int axis); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + private static extern bool OVR_GamepadController_GetButton(int button); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + private static extern bool OVR_GamepadController_SetVibration(int node, float strength, float frequency); + } - if ((a * a) > 1.0f) - return (a >= 0) ? 1.0f : -1.0f; - return a; - } + private class OVRControllerGamepadAndroid : OVRControllerBase + { + private static class AndroidButtonNames + { + public static readonly KeyCode A = KeyCode.JoystickButton0; + public static readonly KeyCode B = KeyCode.JoystickButton1; + public static readonly KeyCode X = KeyCode.JoystickButton2; + public static readonly KeyCode Y = KeyCode.JoystickButton3; + public static readonly KeyCode Start = KeyCode.JoystickButton10; + public static readonly KeyCode Back = KeyCode.JoystickButton11; + public static readonly KeyCode LThumbstick = KeyCode.JoystickButton8; + public static readonly KeyCode RThumbstick = KeyCode.JoystickButton9; + public static readonly KeyCode LShoulder = KeyCode.JoystickButton4; + public static readonly KeyCode RShoulder = KeyCode.JoystickButton5; + } - private static bool ShouldResolveController(Controller controllerType, Controller controllerMask) - { - bool isValid = false; + private static class AndroidAxisNames + { + public static readonly string LThumbstickX = "Oculus_GearVR_LThumbstickX"; + public static readonly string LThumbstickY = "Oculus_GearVR_LThumbstickY"; + public static readonly string RThumbstickX = "Oculus_GearVR_RThumbstickX"; + public static readonly string RThumbstickY = "Oculus_GearVR_RThumbstickY"; + public static readonly string LIndexTrigger = "Oculus_GearVR_LIndexTrigger"; + public static readonly string RIndexTrigger = "Oculus_GearVR_RIndexTrigger"; + public static readonly string DpadX = "Oculus_GearVR_DpadX"; + public static readonly string DpadY = "Oculus_GearVR_DpadY"; + } - if ((controllerType & controllerMask) == controllerType) - { - isValid = true; - } + private bool joystickDetected = false; + private float joystickCheckInterval = 1.0f; + private float joystickCheckTime = 0.0f; - // If the mask requests both Touch controllers, reject the individual touch controllers. - if (((controllerMask & Controller.Touch) == Controller.Touch) - && ((controllerType & Controller.Touch) != 0) - && ((controllerType & Controller.Touch) != Controller.Touch)) - { - isValid = false; - } + public OVRControllerGamepadAndroid() + { + controllerType = Controller.Gamepad; + } - return isValid; - } + private bool ShouldUpdate() + { + // Use Unity's joystick detection as a quick way to determine joystick availability. + if ((Time.realtimeSinceStartup - joystickCheckTime) > joystickCheckInterval) + { + joystickCheckTime = Time.realtimeSinceStartup; + joystickDetected = false; + var joystickNames = UnityEngine.Input.GetJoystickNames(); - private abstract class OVRControllerBase - { - public class VirtualButtonMap - { - public RawButton None = RawButton.None; - public RawButton One = RawButton.None; - public RawButton Two = RawButton.None; - public RawButton Three = RawButton.None; - public RawButton Four = RawButton.None; - public RawButton Start = RawButton.None; - public RawButton Back = RawButton.None; - public RawButton PrimaryShoulder = RawButton.None; - public RawButton PrimaryIndexTrigger = RawButton.None; - public RawButton PrimaryHandTrigger = RawButton.None; - public RawButton PrimaryThumbstick = RawButton.None; - public RawButton PrimaryThumbstickUp = RawButton.None; - public RawButton PrimaryThumbstickDown = RawButton.None; - public RawButton PrimaryThumbstickLeft = RawButton.None; - public RawButton PrimaryThumbstickRight = RawButton.None; - public RawButton PrimaryTouchpad = RawButton.None; - public RawButton SecondaryShoulder = RawButton.None; - public RawButton SecondaryIndexTrigger = RawButton.None; - public RawButton SecondaryHandTrigger = RawButton.None; - public RawButton SecondaryThumbstick = RawButton.None; - public RawButton SecondaryThumbstickUp = RawButton.None; - public RawButton SecondaryThumbstickDown = RawButton.None; - public RawButton SecondaryThumbstickLeft = RawButton.None; - public RawButton SecondaryThumbstickRight = RawButton.None; - public RawButton SecondaryTouchpad = RawButton.None; - public RawButton DpadUp = RawButton.None; - public RawButton DpadDown = RawButton.None; - public RawButton DpadLeft = RawButton.None; - public RawButton DpadRight = RawButton.None; - public RawButton Up = RawButton.None; - public RawButton Down = RawButton.None; - public RawButton Left = RawButton.None; - public RawButton Right = RawButton.None; - - public RawButton ToRawMask(Button virtualMask) - { - RawButton rawMask = 0; - - if (virtualMask == Button.None) - return RawButton.None; - - if ((virtualMask & Button.One) != 0) - rawMask |= One; - if ((virtualMask & Button.Two) != 0) - rawMask |= Two; - if ((virtualMask & Button.Three) != 0) - rawMask |= Three; - if ((virtualMask & Button.Four) != 0) - rawMask |= Four; - if ((virtualMask & Button.Start) != 0) - rawMask |= Start; - if ((virtualMask & Button.Back) != 0) - rawMask |= Back; - if ((virtualMask & Button.PrimaryShoulder) != 0) - rawMask |= PrimaryShoulder; - if ((virtualMask & Button.PrimaryIndexTrigger) != 0) - rawMask |= PrimaryIndexTrigger; - if ((virtualMask & Button.PrimaryHandTrigger) != 0) - rawMask |= PrimaryHandTrigger; - if ((virtualMask & Button.PrimaryThumbstick) != 0) - rawMask |= PrimaryThumbstick; - if ((virtualMask & Button.PrimaryThumbstickUp) != 0) - rawMask |= PrimaryThumbstickUp; - if ((virtualMask & Button.PrimaryThumbstickDown) != 0) - rawMask |= PrimaryThumbstickDown; - if ((virtualMask & Button.PrimaryThumbstickLeft) != 0) - rawMask |= PrimaryThumbstickLeft; - if ((virtualMask & Button.PrimaryThumbstickRight) != 0) - rawMask |= PrimaryThumbstickRight; - if ((virtualMask & Button.PrimaryTouchpad) != 0) - rawMask |= PrimaryTouchpad; - if ((virtualMask & Button.SecondaryShoulder) != 0) - rawMask |= SecondaryShoulder; - if ((virtualMask & Button.SecondaryIndexTrigger) != 0) - rawMask |= SecondaryIndexTrigger; - if ((virtualMask & Button.SecondaryHandTrigger) != 0) - rawMask |= SecondaryHandTrigger; - if ((virtualMask & Button.SecondaryThumbstick) != 0) - rawMask |= SecondaryThumbstick; - if ((virtualMask & Button.SecondaryThumbstickUp) != 0) - rawMask |= SecondaryThumbstickUp; - if ((virtualMask & Button.SecondaryThumbstickDown) != 0) - rawMask |= SecondaryThumbstickDown; - if ((virtualMask & Button.SecondaryThumbstickLeft) != 0) - rawMask |= SecondaryThumbstickLeft; - if ((virtualMask & Button.SecondaryThumbstickRight) != 0) - rawMask |= SecondaryThumbstickRight; - if ((virtualMask & Button.SecondaryTouchpad) != 0) - rawMask |= SecondaryTouchpad; - if ((virtualMask & Button.DpadUp) != 0) - rawMask |= DpadUp; - if ((virtualMask & Button.DpadDown) != 0) - rawMask |= DpadDown; - if ((virtualMask & Button.DpadLeft) != 0) - rawMask |= DpadLeft; - if ((virtualMask & Button.DpadRight) != 0) - rawMask |= DpadRight; - if ((virtualMask & Button.Up) != 0) - rawMask |= Up; - if ((virtualMask & Button.Down) != 0) - rawMask |= Down; - if ((virtualMask & Button.Left) != 0) - rawMask |= Left; - if ((virtualMask & Button.Right) != 0) - rawMask |= Right; - - return rawMask; - } - } - - public class VirtualTouchMap - { - public RawTouch None = RawTouch.None; - public RawTouch One = RawTouch.None; - public RawTouch Two = RawTouch.None; - public RawTouch Three = RawTouch.None; - public RawTouch Four = RawTouch.None; - public RawTouch PrimaryIndexTrigger = RawTouch.None; - public RawTouch PrimaryThumbstick = RawTouch.None; - public RawTouch PrimaryThumbRest = RawTouch.None; - public RawTouch PrimaryTouchpad = RawTouch.None; - public RawTouch SecondaryIndexTrigger = RawTouch.None; - public RawTouch SecondaryThumbstick = RawTouch.None; - public RawTouch SecondaryThumbRest = RawTouch.None; - public RawTouch SecondaryTouchpad = RawTouch.None; - - public RawTouch ToRawMask(Touch virtualMask) - { - RawTouch rawMask = 0; - - if (virtualMask == Touch.None) - return RawTouch.None; - - if ((virtualMask & Touch.One) != 0) - rawMask |= One; - if ((virtualMask & Touch.Two) != 0) - rawMask |= Two; - if ((virtualMask & Touch.Three) != 0) - rawMask |= Three; - if ((virtualMask & Touch.Four) != 0) - rawMask |= Four; - if ((virtualMask & Touch.PrimaryIndexTrigger) != 0) - rawMask |= PrimaryIndexTrigger; - if ((virtualMask & Touch.PrimaryThumbstick) != 0) - rawMask |= PrimaryThumbstick; - if ((virtualMask & Touch.PrimaryThumbRest) != 0) - rawMask |= PrimaryThumbRest; - if ((virtualMask & Touch.PrimaryTouchpad) != 0) - rawMask |= PrimaryTouchpad; - if ((virtualMask & Touch.SecondaryIndexTrigger) != 0) - rawMask |= SecondaryIndexTrigger; - if ((virtualMask & Touch.SecondaryThumbstick) != 0) - rawMask |= SecondaryThumbstick; - if ((virtualMask & Touch.SecondaryThumbRest) != 0) - rawMask |= SecondaryThumbRest; - if ((virtualMask & Touch.SecondaryTouchpad) != 0) - rawMask |= SecondaryTouchpad; - - return rawMask; - } - } - - public class VirtualNearTouchMap - { - public RawNearTouch None = RawNearTouch.None; - public RawNearTouch PrimaryIndexTrigger = RawNearTouch.None; - public RawNearTouch PrimaryThumbButtons = RawNearTouch.None; - public RawNearTouch SecondaryIndexTrigger = RawNearTouch.None; - public RawNearTouch SecondaryThumbButtons = RawNearTouch.None; - - public RawNearTouch ToRawMask(NearTouch virtualMask) - { - RawNearTouch rawMask = 0; - - if (virtualMask == NearTouch.None) - return RawNearTouch.None; - - if ((virtualMask & NearTouch.PrimaryIndexTrigger) != 0) - rawMask |= PrimaryIndexTrigger; - if ((virtualMask & NearTouch.PrimaryThumbButtons) != 0) - rawMask |= PrimaryThumbButtons; - if ((virtualMask & NearTouch.SecondaryIndexTrigger) != 0) - rawMask |= SecondaryIndexTrigger; - if ((virtualMask & NearTouch.SecondaryThumbButtons) != 0) - rawMask |= SecondaryThumbButtons; - - return rawMask; - } - } - - public class VirtualAxis1DMap - { - public RawAxis1D None = RawAxis1D.None; - public RawAxis1D PrimaryIndexTrigger = RawAxis1D.None; - public RawAxis1D PrimaryHandTrigger = RawAxis1D.None; - public RawAxis1D SecondaryIndexTrigger = RawAxis1D.None; - public RawAxis1D SecondaryHandTrigger = RawAxis1D.None; - - public RawAxis1D ToRawMask(Axis1D virtualMask) - { - RawAxis1D rawMask = 0; - - if (virtualMask == Axis1D.None) - return RawAxis1D.None; - - if ((virtualMask & Axis1D.PrimaryIndexTrigger) != 0) - rawMask |= PrimaryIndexTrigger; - if ((virtualMask & Axis1D.PrimaryHandTrigger) != 0) - rawMask |= PrimaryHandTrigger; - if ((virtualMask & Axis1D.SecondaryIndexTrigger) != 0) - rawMask |= SecondaryIndexTrigger; - if ((virtualMask & Axis1D.SecondaryHandTrigger) != 0) - rawMask |= SecondaryHandTrigger; - - return rawMask; - } - } - - public class VirtualAxis2DMap - { - public RawAxis2D None = RawAxis2D.None; - public RawAxis2D PrimaryThumbstick = RawAxis2D.None; - public RawAxis2D PrimaryTouchpad = RawAxis2D.None; - public RawAxis2D SecondaryThumbstick = RawAxis2D.None; - public RawAxis2D SecondaryTouchpad = RawAxis2D.None; - - public RawAxis2D ToRawMask(Axis2D virtualMask) - { - RawAxis2D rawMask = 0; - - if (virtualMask == Axis2D.None) - return RawAxis2D.None; - - if ((virtualMask & Axis2D.PrimaryThumbstick) != 0) - rawMask |= PrimaryThumbstick; - if ((virtualMask & Axis2D.PrimaryTouchpad) != 0) - rawMask |= PrimaryTouchpad; - if ((virtualMask & Axis2D.SecondaryThumbstick) != 0) - rawMask |= SecondaryThumbstick; - if ((virtualMask & Axis2D.SecondaryTouchpad) != 0) - rawMask |= SecondaryTouchpad; - - return rawMask; - } - } - - public Controller controllerType = Controller.None; - public VirtualButtonMap buttonMap = new VirtualButtonMap(); - public VirtualTouchMap touchMap = new VirtualTouchMap(); - public VirtualNearTouchMap nearTouchMap = new VirtualNearTouchMap(); - public VirtualAxis1DMap axis1DMap = new VirtualAxis1DMap(); - public VirtualAxis2DMap axis2DMap = new VirtualAxis2DMap(); - public OVRPlugin.ControllerState2 previousState = new OVRPlugin.ControllerState2(); - public OVRPlugin.ControllerState2 currentState = new OVRPlugin.ControllerState2(); - public bool shouldApplyDeadzone = true; - - public OVRControllerBase() - { - ConfigureButtonMap(); - ConfigureTouchMap(); - ConfigureNearTouchMap(); - ConfigureAxis1DMap(); - ConfigureAxis2DMap(); - } - - public virtual Controller Update() - { - OVRPlugin.ControllerState2 state = OVRPlugin.GetControllerState2((uint)controllerType); - //Debug.Log("MalibuManaged - " + (state.Touches & (uint)RawTouch.LTouchpad) + " " + state.LTouchpad.x + " " + state.LTouchpad.y + " " + state.RTouchpad.x + " " + state.RTouchpad.y); + for (int i = 0; i < joystickNames.Length; i++) + { + if (joystickNames[i] != String.Empty) + { + joystickDetected = true; + break; + } + } + } - if (state.LIndexTrigger >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LIndexTrigger; - if (state.LHandTrigger >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LHandTrigger; - if (state.LThumbstick.y >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LThumbstickUp; - if (state.LThumbstick.y <= -AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LThumbstickDown; - if (state.LThumbstick.x <= -AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LThumbstickLeft; - if (state.LThumbstick.x >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LThumbstickRight; - - if (state.RIndexTrigger >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RIndexTrigger; - if (state.RHandTrigger >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RHandTrigger; - if (state.RThumbstick.y >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RThumbstickUp; - if (state.RThumbstick.y <= -AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RThumbstickDown; - if (state.RThumbstick.x <= -AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RThumbstickLeft; - if (state.RThumbstick.x >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RThumbstickRight; - - previousState = currentState; - currentState = state; - - return ((Controller)currentState.ConnectedControllers & controllerType); - } - - public virtual void SetControllerVibration(float frequency, float amplitude) - { - OVRPlugin.SetControllerVibration((uint)controllerType, frequency, amplitude); - } - - public virtual void RecenterController() - { - OVRPlugin.RecenterTrackingOrigin(OVRPlugin.RecenterFlags.Controllers); - } - - public abstract void ConfigureButtonMap(); - public abstract void ConfigureTouchMap(); - public abstract void ConfigureNearTouchMap(); - public abstract void ConfigureAxis1DMap(); - public abstract void ConfigureAxis2DMap(); - - public RawButton ResolveToRawMask(Button virtualMask) - { - return buttonMap.ToRawMask(virtualMask); - } - - public RawTouch ResolveToRawMask(Touch virtualMask) - { - return touchMap.ToRawMask(virtualMask); - } - - public RawNearTouch ResolveToRawMask(NearTouch virtualMask) - { - return nearTouchMap.ToRawMask(virtualMask); - } - - public RawAxis1D ResolveToRawMask(Axis1D virtualMask) - { - return axis1DMap.ToRawMask(virtualMask); - } - - public RawAxis2D ResolveToRawMask(Axis2D virtualMask) - { - return axis2DMap.ToRawMask(virtualMask); - } - } + return joystickDetected; + } - private class OVRControllerTouch : OVRControllerBase - { - public OVRControllerTouch() - { - controllerType = Controller.Touch; - } - - public override void ConfigureButtonMap() - { - buttonMap.None = RawButton.None; - buttonMap.One = RawButton.A; - buttonMap.Two = RawButton.B; - buttonMap.Three = RawButton.X; - buttonMap.Four = RawButton.Y; - buttonMap.Start = RawButton.Start; - buttonMap.Back = RawButton.None; - buttonMap.PrimaryShoulder = RawButton.None; - buttonMap.PrimaryIndexTrigger = RawButton.LIndexTrigger; - buttonMap.PrimaryHandTrigger = RawButton.LHandTrigger; - buttonMap.PrimaryThumbstick = RawButton.LThumbstick; - buttonMap.PrimaryThumbstickUp = RawButton.LThumbstickUp; - buttonMap.PrimaryThumbstickDown = RawButton.LThumbstickDown; - buttonMap.PrimaryThumbstickLeft = RawButton.LThumbstickLeft; - buttonMap.PrimaryThumbstickRight = RawButton.LThumbstickRight; - buttonMap.PrimaryTouchpad = RawButton.None; - buttonMap.SecondaryShoulder = RawButton.None; - buttonMap.SecondaryIndexTrigger = RawButton.RIndexTrigger; - buttonMap.SecondaryHandTrigger = RawButton.RHandTrigger; - buttonMap.SecondaryThumbstick = RawButton.RThumbstick; - buttonMap.SecondaryThumbstickUp = RawButton.RThumbstickUp; - buttonMap.SecondaryThumbstickDown = RawButton.RThumbstickDown; - buttonMap.SecondaryThumbstickLeft = RawButton.RThumbstickLeft; - buttonMap.SecondaryThumbstickRight = RawButton.RThumbstickRight; - buttonMap.SecondaryTouchpad = RawButton.None; - buttonMap.DpadUp = RawButton.None; - buttonMap.DpadDown = RawButton.None; - buttonMap.DpadLeft = RawButton.None; - buttonMap.DpadRight = RawButton.None; - buttonMap.Up = RawButton.LThumbstickUp; - buttonMap.Down = RawButton.LThumbstickDown; - buttonMap.Left = RawButton.LThumbstickLeft; - buttonMap.Right = RawButton.LThumbstickRight; - } - - public override void ConfigureTouchMap() - { - touchMap.None = RawTouch.None; - touchMap.One = RawTouch.A; - touchMap.Two = RawTouch.B; - touchMap.Three = RawTouch.X; - touchMap.Four = RawTouch.Y; - touchMap.PrimaryIndexTrigger = RawTouch.LIndexTrigger; - touchMap.PrimaryThumbstick = RawTouch.LThumbstick; - touchMap.PrimaryThumbRest = RawTouch.LThumbRest; - touchMap.PrimaryTouchpad = RawTouch.None; - touchMap.SecondaryIndexTrigger = RawTouch.RIndexTrigger; - touchMap.SecondaryThumbstick = RawTouch.RThumbstick; - touchMap.SecondaryThumbRest = RawTouch.RThumbRest; - touchMap.SecondaryTouchpad = RawTouch.None; - } - - public override void ConfigureNearTouchMap() - { - nearTouchMap.None = RawNearTouch.None; - nearTouchMap.PrimaryIndexTrigger = RawNearTouch.LIndexTrigger; - nearTouchMap.PrimaryThumbButtons = RawNearTouch.LThumbButtons; - nearTouchMap.SecondaryIndexTrigger = RawNearTouch.RIndexTrigger; - nearTouchMap.SecondaryThumbButtons = RawNearTouch.RThumbButtons; - } - - public override void ConfigureAxis1DMap() - { - axis1DMap.None = RawAxis1D.None; - axis1DMap.PrimaryIndexTrigger = RawAxis1D.LIndexTrigger; - axis1DMap.PrimaryHandTrigger = RawAxis1D.LHandTrigger; - axis1DMap.SecondaryIndexTrigger = RawAxis1D.RIndexTrigger; - axis1DMap.SecondaryHandTrigger = RawAxis1D.RHandTrigger; - } - - public override void ConfigureAxis2DMap() - { - axis2DMap.None = RawAxis2D.None; - axis2DMap.PrimaryThumbstick = RawAxis2D.LThumbstick; - axis2DMap.PrimaryTouchpad = RawAxis2D.None; - axis2DMap.SecondaryThumbstick = RawAxis2D.RThumbstick; - axis2DMap.SecondaryTouchpad = RawAxis2D.None; - } - } + public override Controller Update() + { + if (!ShouldUpdate()) + { + return Controller.None; + } - private class OVRControllerLTouch : OVRControllerBase - { - public OVRControllerLTouch() - { - controllerType = Controller.LTouch; - } - - public override void ConfigureButtonMap() - { - buttonMap.None = RawButton.None; - buttonMap.One = RawButton.X; - buttonMap.Two = RawButton.Y; - buttonMap.Three = RawButton.None; - buttonMap.Four = RawButton.None; - buttonMap.Start = RawButton.Start; - buttonMap.Back = RawButton.None; - buttonMap.PrimaryShoulder = RawButton.None; - buttonMap.PrimaryIndexTrigger = RawButton.LIndexTrigger; - buttonMap.PrimaryHandTrigger = RawButton.LHandTrigger; - buttonMap.PrimaryThumbstick = RawButton.LThumbstick; - buttonMap.PrimaryThumbstickUp = RawButton.LThumbstickUp; - buttonMap.PrimaryThumbstickDown = RawButton.LThumbstickDown; - buttonMap.PrimaryThumbstickLeft = RawButton.LThumbstickLeft; - buttonMap.PrimaryThumbstickRight = RawButton.LThumbstickRight; - buttonMap.PrimaryTouchpad = RawButton.None; - buttonMap.SecondaryShoulder = RawButton.None; - buttonMap.SecondaryIndexTrigger = RawButton.None; - buttonMap.SecondaryHandTrigger = RawButton.None; - buttonMap.SecondaryThumbstick = RawButton.None; - buttonMap.SecondaryThumbstickUp = RawButton.None; - buttonMap.SecondaryThumbstickDown = RawButton.None; - buttonMap.SecondaryThumbstickLeft = RawButton.None; - buttonMap.SecondaryThumbstickRight = RawButton.None; - buttonMap.SecondaryTouchpad = RawButton.None; - buttonMap.DpadUp = RawButton.None; - buttonMap.DpadDown = RawButton.None; - buttonMap.DpadLeft = RawButton.None; - buttonMap.DpadRight = RawButton.None; - buttonMap.Up = RawButton.LThumbstickUp; - buttonMap.Down = RawButton.LThumbstickDown; - buttonMap.Left = RawButton.LThumbstickLeft; - buttonMap.Right = RawButton.LThumbstickRight; - } - - public override void ConfigureTouchMap() - { - touchMap.None = RawTouch.None; - touchMap.One = RawTouch.X; - touchMap.Two = RawTouch.Y; - touchMap.Three = RawTouch.None; - touchMap.Four = RawTouch.None; - touchMap.PrimaryIndexTrigger = RawTouch.LIndexTrigger; - touchMap.PrimaryThumbstick = RawTouch.LThumbstick; - touchMap.PrimaryThumbRest = RawTouch.LThumbRest; - touchMap.PrimaryTouchpad = RawTouch.None; - touchMap.SecondaryIndexTrigger = RawTouch.None; - touchMap.SecondaryThumbstick = RawTouch.None; - touchMap.SecondaryThumbRest = RawTouch.None; - touchMap.SecondaryTouchpad = RawTouch.None; - } - - public override void ConfigureNearTouchMap() - { - nearTouchMap.None = RawNearTouch.None; - nearTouchMap.PrimaryIndexTrigger = RawNearTouch.LIndexTrigger; - nearTouchMap.PrimaryThumbButtons = RawNearTouch.LThumbButtons; - nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; - nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; - } - - public override void ConfigureAxis1DMap() - { - axis1DMap.None = RawAxis1D.None; - axis1DMap.PrimaryIndexTrigger = RawAxis1D.LIndexTrigger; - axis1DMap.PrimaryHandTrigger = RawAxis1D.LHandTrigger; - axis1DMap.SecondaryIndexTrigger = RawAxis1D.None; - axis1DMap.SecondaryHandTrigger = RawAxis1D.None; - } - - public override void ConfigureAxis2DMap() - { - axis2DMap.None = RawAxis2D.None; - axis2DMap.PrimaryThumbstick = RawAxis2D.LThumbstick; - axis2DMap.PrimaryTouchpad = RawAxis2D.None; - axis2DMap.SecondaryThumbstick = RawAxis2D.None; - axis2DMap.SecondaryTouchpad = RawAxis2D.None; - } - } + OVRPlugin.ControllerState2 state = new OVRPlugin.ControllerState2(); + + state.ConnectedControllers = (uint)Controller.Gamepad; + + if (Input.GetKey(AndroidButtonNames.A)) + state.Buttons |= (uint)RawButton.A; + if (Input.GetKey(AndroidButtonNames.B)) + state.Buttons |= (uint)RawButton.B; + if (Input.GetKey(AndroidButtonNames.X)) + state.Buttons |= (uint)RawButton.X; + if (Input.GetKey(AndroidButtonNames.Y)) + state.Buttons |= (uint)RawButton.Y; + if (Input.GetKey(AndroidButtonNames.Start)) + state.Buttons |= (uint)RawButton.Start; + if (Input.GetKey(AndroidButtonNames.Back) || Input.GetKey(KeyCode.Escape)) + state.Buttons |= (uint)RawButton.Back; + if (Input.GetKey(AndroidButtonNames.LThumbstick)) + state.Buttons |= (uint)RawButton.LThumbstick; + if (Input.GetKey(AndroidButtonNames.RThumbstick)) + state.Buttons |= (uint)RawButton.RThumbstick; + if (Input.GetKey(AndroidButtonNames.LShoulder)) + state.Buttons |= (uint)RawButton.LShoulder; + if (Input.GetKey(AndroidButtonNames.RShoulder)) + state.Buttons |= (uint)RawButton.RShoulder; + + state.LThumbstick.x = Input.GetAxisRaw(AndroidAxisNames.LThumbstickX); + state.LThumbstick.y = Input.GetAxisRaw(AndroidAxisNames.LThumbstickY); + state.RThumbstick.x = Input.GetAxisRaw(AndroidAxisNames.RThumbstickX); + state.RThumbstick.y = Input.GetAxisRaw(AndroidAxisNames.RThumbstickY); + state.LIndexTrigger = Input.GetAxisRaw(AndroidAxisNames.LIndexTrigger); + state.RIndexTrigger = Input.GetAxisRaw(AndroidAxisNames.RIndexTrigger); + + if (state.LIndexTrigger >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LIndexTrigger; + if (state.LHandTrigger >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LHandTrigger; + if (state.LThumbstick.y >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LThumbstickUp; + if (state.LThumbstick.y <= -AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LThumbstickDown; + if (state.LThumbstick.x <= -AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LThumbstickLeft; + if (state.LThumbstick.x >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.LThumbstickRight; + + if (state.RIndexTrigger >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RIndexTrigger; + if (state.RHandTrigger >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RHandTrigger; + if (state.RThumbstick.y >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RThumbstickUp; + if (state.RThumbstick.y <= -AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RThumbstickDown; + if (state.RThumbstick.x <= -AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RThumbstickLeft; + if (state.RThumbstick.x >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.RThumbstickRight; + + float dpadX = Input.GetAxisRaw(AndroidAxisNames.DpadX); + float dpadY = Input.GetAxisRaw(AndroidAxisNames.DpadY); + + if (dpadX <= -AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.DpadLeft; + if (dpadX >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.DpadRight; + if (dpadY <= -AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.DpadDown; + if (dpadY >= AXIS_AS_BUTTON_THRESHOLD) + state.Buttons |= (uint)RawButton.DpadUp; + + previousState = currentState; + currentState = state; + + return ((Controller)currentState.ConnectedControllers & controllerType); + } - private class OVRControllerRTouch : OVRControllerBase - { - public OVRControllerRTouch() - { - controllerType = Controller.RTouch; - } - - public override void ConfigureButtonMap() - { - buttonMap.None = RawButton.None; - buttonMap.One = RawButton.A; - buttonMap.Two = RawButton.B; - buttonMap.Three = RawButton.None; - buttonMap.Four = RawButton.None; - buttonMap.Start = RawButton.None; - buttonMap.Back = RawButton.None; - buttonMap.PrimaryShoulder = RawButton.None; - buttonMap.PrimaryIndexTrigger = RawButton.RIndexTrigger; - buttonMap.PrimaryHandTrigger = RawButton.RHandTrigger; - buttonMap.PrimaryThumbstick = RawButton.RThumbstick; - buttonMap.PrimaryThumbstickUp = RawButton.RThumbstickUp; - buttonMap.PrimaryThumbstickDown = RawButton.RThumbstickDown; - buttonMap.PrimaryThumbstickLeft = RawButton.RThumbstickLeft; - buttonMap.PrimaryThumbstickRight = RawButton.RThumbstickRight; - buttonMap.PrimaryTouchpad = RawButton.None; - buttonMap.SecondaryShoulder = RawButton.None; - buttonMap.SecondaryIndexTrigger = RawButton.None; - buttonMap.SecondaryHandTrigger = RawButton.None; - buttonMap.SecondaryThumbstick = RawButton.None; - buttonMap.SecondaryThumbstickUp = RawButton.None; - buttonMap.SecondaryThumbstickDown = RawButton.None; - buttonMap.SecondaryThumbstickLeft = RawButton.None; - buttonMap.SecondaryThumbstickRight = RawButton.None; - buttonMap.SecondaryTouchpad = RawButton.None; - buttonMap.DpadUp = RawButton.None; - buttonMap.DpadDown = RawButton.None; - buttonMap.DpadLeft = RawButton.None; - buttonMap.DpadRight = RawButton.None; - buttonMap.Up = RawButton.RThumbstickUp; - buttonMap.Down = RawButton.RThumbstickDown; - buttonMap.Left = RawButton.RThumbstickLeft; - buttonMap.Right = RawButton.RThumbstickRight; - } - - public override void ConfigureTouchMap() - { - touchMap.None = RawTouch.None; - touchMap.One = RawTouch.A; - touchMap.Two = RawTouch.B; - touchMap.Three = RawTouch.None; - touchMap.Four = RawTouch.None; - touchMap.PrimaryIndexTrigger = RawTouch.RIndexTrigger; - touchMap.PrimaryThumbstick = RawTouch.RThumbstick; - touchMap.PrimaryThumbRest = RawTouch.RThumbRest; - touchMap.PrimaryTouchpad = RawTouch.None; - touchMap.SecondaryIndexTrigger = RawTouch.None; - touchMap.SecondaryThumbstick = RawTouch.None; - touchMap.SecondaryThumbRest = RawTouch.None; - touchMap.SecondaryTouchpad = RawTouch.None; - } - - public override void ConfigureNearTouchMap() - { - nearTouchMap.None = RawNearTouch.None; - nearTouchMap.PrimaryIndexTrigger = RawNearTouch.RIndexTrigger; - nearTouchMap.PrimaryThumbButtons = RawNearTouch.RThumbButtons; - nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; - nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; - } - - public override void ConfigureAxis1DMap() - { - axis1DMap.None = RawAxis1D.None; - axis1DMap.PrimaryIndexTrigger = RawAxis1D.RIndexTrigger; - axis1DMap.PrimaryHandTrigger = RawAxis1D.RHandTrigger; - axis1DMap.SecondaryIndexTrigger = RawAxis1D.None; - axis1DMap.SecondaryHandTrigger = RawAxis1D.None; - } - - public override void ConfigureAxis2DMap() - { - axis2DMap.None = RawAxis2D.None; - axis2DMap.PrimaryThumbstick = RawAxis2D.RThumbstick; - axis2DMap.PrimaryTouchpad = RawAxis2D.None; - axis2DMap.SecondaryThumbstick = RawAxis2D.None; - axis2DMap.SecondaryTouchpad = RawAxis2D.None; - } - } + public override void ConfigureButtonMap() + { + buttonMap.None = RawButton.None; + buttonMap.One = RawButton.A; + buttonMap.Two = RawButton.B; + buttonMap.Three = RawButton.X; + buttonMap.Four = RawButton.Y; + buttonMap.Start = RawButton.Start; + buttonMap.Back = RawButton.Back; + buttonMap.PrimaryShoulder = RawButton.LShoulder; + buttonMap.PrimaryIndexTrigger = RawButton.LIndexTrigger; + buttonMap.PrimaryHandTrigger = RawButton.None; + buttonMap.PrimaryThumbstick = RawButton.LThumbstick; + buttonMap.PrimaryThumbstickUp = RawButton.LThumbstickUp; + buttonMap.PrimaryThumbstickDown = RawButton.LThumbstickDown; + buttonMap.PrimaryThumbstickLeft = RawButton.LThumbstickLeft; + buttonMap.PrimaryThumbstickRight = RawButton.LThumbstickRight; + buttonMap.PrimaryTouchpad = RawButton.None; + buttonMap.SecondaryShoulder = RawButton.RShoulder; + buttonMap.SecondaryIndexTrigger = RawButton.RIndexTrigger; + buttonMap.SecondaryHandTrigger = RawButton.None; + buttonMap.SecondaryThumbstick = RawButton.RThumbstick; + buttonMap.SecondaryThumbstickUp = RawButton.RThumbstickUp; + buttonMap.SecondaryThumbstickDown = RawButton.RThumbstickDown; + buttonMap.SecondaryThumbstickLeft = RawButton.RThumbstickLeft; + buttonMap.SecondaryThumbstickRight = RawButton.RThumbstickRight; + buttonMap.SecondaryTouchpad = RawButton.None; + buttonMap.DpadUp = RawButton.DpadUp; + buttonMap.DpadDown = RawButton.DpadDown; + buttonMap.DpadLeft = RawButton.DpadLeft; + buttonMap.DpadRight = RawButton.DpadRight; + buttonMap.Up = RawButton.LThumbstickUp; + buttonMap.Down = RawButton.LThumbstickDown; + buttonMap.Left = RawButton.LThumbstickLeft; + buttonMap.Right = RawButton.LThumbstickRight; + } - private class OVRControllerRemote : OVRControllerBase - { - public OVRControllerRemote() - { - controllerType = Controller.Remote; - } - - public override void ConfigureButtonMap() - { - buttonMap.None = RawButton.None; - buttonMap.One = RawButton.Start; - buttonMap.Two = RawButton.Back; - buttonMap.Three = RawButton.None; - buttonMap.Four = RawButton.None; - buttonMap.Start = RawButton.Start; - buttonMap.Back = RawButton.Back; - buttonMap.PrimaryShoulder = RawButton.None; - buttonMap.PrimaryIndexTrigger = RawButton.None; - buttonMap.PrimaryHandTrigger = RawButton.None; - buttonMap.PrimaryThumbstick = RawButton.None; - buttonMap.PrimaryThumbstickUp = RawButton.None; - buttonMap.PrimaryThumbstickDown = RawButton.None; - buttonMap.PrimaryThumbstickLeft = RawButton.None; - buttonMap.PrimaryThumbstickRight = RawButton.None; - buttonMap.PrimaryTouchpad = RawButton.None; - buttonMap.SecondaryShoulder = RawButton.None; - buttonMap.SecondaryIndexTrigger = RawButton.None; - buttonMap.SecondaryHandTrigger = RawButton.None; - buttonMap.SecondaryThumbstick = RawButton.None; - buttonMap.SecondaryThumbstickUp = RawButton.None; - buttonMap.SecondaryThumbstickDown = RawButton.None; - buttonMap.SecondaryThumbstickLeft = RawButton.None; - buttonMap.SecondaryThumbstickRight = RawButton.None; - buttonMap.SecondaryTouchpad = RawButton.None; - buttonMap.DpadUp = RawButton.DpadUp; - buttonMap.DpadDown = RawButton.DpadDown; - buttonMap.DpadLeft = RawButton.DpadLeft; - buttonMap.DpadRight = RawButton.DpadRight; - buttonMap.Up = RawButton.DpadUp; - buttonMap.Down = RawButton.DpadDown; - buttonMap.Left = RawButton.DpadLeft; - buttonMap.Right = RawButton.DpadRight; - } - - public override void ConfigureTouchMap() - { - touchMap.None = RawTouch.None; - touchMap.One = RawTouch.None; - touchMap.Two = RawTouch.None; - touchMap.Three = RawTouch.None; - touchMap.Four = RawTouch.None; - touchMap.PrimaryIndexTrigger = RawTouch.None; - touchMap.PrimaryThumbstick = RawTouch.None; - touchMap.PrimaryThumbRest = RawTouch.None; - touchMap.PrimaryTouchpad = RawTouch.None; - touchMap.SecondaryIndexTrigger = RawTouch.None; - touchMap.SecondaryThumbstick = RawTouch.None; - touchMap.SecondaryThumbRest = RawTouch.None; - touchMap.SecondaryTouchpad = RawTouch.None; - } - - public override void ConfigureNearTouchMap() - { - nearTouchMap.None = RawNearTouch.None; - nearTouchMap.PrimaryIndexTrigger = RawNearTouch.None; - nearTouchMap.PrimaryThumbButtons = RawNearTouch.None; - nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; - nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; - } - - public override void ConfigureAxis1DMap() - { - axis1DMap.None = RawAxis1D.None; - axis1DMap.PrimaryIndexTrigger = RawAxis1D.None; - axis1DMap.PrimaryHandTrigger = RawAxis1D.None; - axis1DMap.SecondaryIndexTrigger = RawAxis1D.None; - axis1DMap.SecondaryHandTrigger = RawAxis1D.None; - } - - public override void ConfigureAxis2DMap() - { - axis2DMap.None = RawAxis2D.None; - axis2DMap.PrimaryThumbstick = RawAxis2D.None; - axis2DMap.PrimaryTouchpad = RawAxis2D.None; - axis2DMap.SecondaryThumbstick = RawAxis2D.None; - axis2DMap.SecondaryTouchpad = RawAxis2D.None; - } - } + public override void ConfigureTouchMap() + { + touchMap.None = RawTouch.None; + touchMap.One = RawTouch.None; + touchMap.Two = RawTouch.None; + touchMap.Three = RawTouch.None; + touchMap.Four = RawTouch.None; + touchMap.PrimaryIndexTrigger = RawTouch.None; + touchMap.PrimaryThumbstick = RawTouch.None; + touchMap.PrimaryThumbRest = RawTouch.None; + touchMap.PrimaryTouchpad = RawTouch.None; + touchMap.SecondaryIndexTrigger = RawTouch.None; + touchMap.SecondaryThumbstick = RawTouch.None; + touchMap.SecondaryThumbRest = RawTouch.None; + touchMap.SecondaryTouchpad = RawTouch.None; + } - private class OVRControllerGamepadPC : OVRControllerBase - { - public OVRControllerGamepadPC() - { - controllerType = Controller.Gamepad; - } - - public override void ConfigureButtonMap() - { - buttonMap.None = RawButton.None; - buttonMap.One = RawButton.A; - buttonMap.Two = RawButton.B; - buttonMap.Three = RawButton.X; - buttonMap.Four = RawButton.Y; - buttonMap.Start = RawButton.Start; - buttonMap.Back = RawButton.Back; - buttonMap.PrimaryShoulder = RawButton.LShoulder; - buttonMap.PrimaryIndexTrigger = RawButton.LIndexTrigger; - buttonMap.PrimaryHandTrigger = RawButton.None; - buttonMap.PrimaryThumbstick = RawButton.LThumbstick; - buttonMap.PrimaryThumbstickUp = RawButton.LThumbstickUp; - buttonMap.PrimaryThumbstickDown = RawButton.LThumbstickDown; - buttonMap.PrimaryThumbstickLeft = RawButton.LThumbstickLeft; - buttonMap.PrimaryThumbstickRight = RawButton.LThumbstickRight; - buttonMap.PrimaryTouchpad = RawButton.None; - buttonMap.SecondaryShoulder = RawButton.RShoulder; - buttonMap.SecondaryIndexTrigger = RawButton.RIndexTrigger; - buttonMap.SecondaryHandTrigger = RawButton.None; - buttonMap.SecondaryThumbstick = RawButton.RThumbstick; - buttonMap.SecondaryThumbstickUp = RawButton.RThumbstickUp; - buttonMap.SecondaryThumbstickDown = RawButton.RThumbstickDown; - buttonMap.SecondaryThumbstickLeft = RawButton.RThumbstickLeft; - buttonMap.SecondaryThumbstickRight = RawButton.RThumbstickRight; - buttonMap.SecondaryTouchpad = RawButton.None; - buttonMap.DpadUp = RawButton.DpadUp; - buttonMap.DpadDown = RawButton.DpadDown; - buttonMap.DpadLeft = RawButton.DpadLeft; - buttonMap.DpadRight = RawButton.DpadRight; - buttonMap.Up = RawButton.LThumbstickUp; - buttonMap.Down = RawButton.LThumbstickDown; - buttonMap.Left = RawButton.LThumbstickLeft; - buttonMap.Right = RawButton.LThumbstickRight; - } - - public override void ConfigureTouchMap() - { - touchMap.None = RawTouch.None; - touchMap.One = RawTouch.None; - touchMap.Two = RawTouch.None; - touchMap.Three = RawTouch.None; - touchMap.Four = RawTouch.None; - touchMap.PrimaryIndexTrigger = RawTouch.None; - touchMap.PrimaryThumbstick = RawTouch.None; - touchMap.PrimaryThumbRest = RawTouch.None; - touchMap.PrimaryTouchpad = RawTouch.None; - touchMap.SecondaryIndexTrigger = RawTouch.None; - touchMap.SecondaryThumbstick = RawTouch.None; - touchMap.SecondaryThumbRest = RawTouch.None; - touchMap.SecondaryTouchpad = RawTouch.None; - } - - public override void ConfigureNearTouchMap() - { - nearTouchMap.None = RawNearTouch.None; - nearTouchMap.PrimaryIndexTrigger = RawNearTouch.None; - nearTouchMap.PrimaryThumbButtons = RawNearTouch.None; - nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; - nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; - } - - public override void ConfigureAxis1DMap() - { - axis1DMap.None = RawAxis1D.None; - axis1DMap.PrimaryIndexTrigger = RawAxis1D.LIndexTrigger; - axis1DMap.PrimaryHandTrigger = RawAxis1D.None; - axis1DMap.SecondaryIndexTrigger = RawAxis1D.RIndexTrigger; - axis1DMap.SecondaryHandTrigger = RawAxis1D.None; - } - - public override void ConfigureAxis2DMap() - { - axis2DMap.None = RawAxis2D.None; - axis2DMap.PrimaryThumbstick = RawAxis2D.LThumbstick; - axis2DMap.PrimaryTouchpad = RawAxis2D.None; - axis2DMap.SecondaryThumbstick = RawAxis2D.RThumbstick; - axis2DMap.SecondaryTouchpad = RawAxis2D.None; - } - } + public override void ConfigureNearTouchMap() + { + nearTouchMap.None = RawNearTouch.None; + nearTouchMap.PrimaryIndexTrigger = RawNearTouch.None; + nearTouchMap.PrimaryThumbButtons = RawNearTouch.None; + nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; + nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; + } - private class OVRControllerGamepadMac : OVRControllerBase - { - /// An axis on the gamepad. - private enum AxisGPC - { - None = -1, - LeftXAxis = 0, - LeftYAxis, - RightXAxis, - RightYAxis, - LeftTrigger, - RightTrigger, - DPad_X_Axis, - DPad_Y_Axis, - Max, - }; + public override void ConfigureAxis1DMap() + { + axis1DMap.None = RawAxis1D.None; + axis1DMap.PrimaryIndexTrigger = RawAxis1D.LIndexTrigger; + axis1DMap.PrimaryHandTrigger = RawAxis1D.None; + axis1DMap.SecondaryIndexTrigger = RawAxis1D.RIndexTrigger; + axis1DMap.SecondaryHandTrigger = RawAxis1D.None; + } - /// A button on the gamepad. - public enum ButtonGPC - { - None = -1, - A = 0, - B, - X, - Y, - Up, - Down, - Left, - Right, - Start, - Back, - LStick, - RStick, - LeftShoulder, - RightShoulder, - Max - }; + public override void ConfigureAxis2DMap() + { + axis2DMap.None = RawAxis2D.None; + axis2DMap.PrimaryThumbstick = RawAxis2D.LThumbstick; + axis2DMap.PrimaryTouchpad = RawAxis2D.None; + axis2DMap.SecondaryThumbstick = RawAxis2D.RThumbstick; + axis2DMap.SecondaryTouchpad = RawAxis2D.None; + } - private bool initialized = false; - - public OVRControllerGamepadMac() - { - controllerType = Controller.Gamepad; - - initialized = OVR_GamepadController_Initialize(); - } - - ~OVRControllerGamepadMac() - { - if (!initialized) - return; - - OVR_GamepadController_Destroy(); - } - - public override Controller Update() - { - if (!initialized) - { - return Controller.None; - } - - OVRPlugin.ControllerState2 state = new OVRPlugin.ControllerState2(); - - bool result = OVR_GamepadController_Update(); - - if (result) - state.ConnectedControllers = (uint)Controller.Gamepad; - - if (OVR_GamepadController_GetButton((int)ButtonGPC.A)) - state.Buttons |= (uint)RawButton.A; - if (OVR_GamepadController_GetButton((int)ButtonGPC.B)) - state.Buttons |= (uint)RawButton.B; - if (OVR_GamepadController_GetButton((int)ButtonGPC.X)) - state.Buttons |= (uint)RawButton.X; - if (OVR_GamepadController_GetButton((int)ButtonGPC.Y)) - state.Buttons |= (uint)RawButton.Y; - if (OVR_GamepadController_GetButton((int)ButtonGPC.Up)) - state.Buttons |= (uint)RawButton.DpadUp; - if (OVR_GamepadController_GetButton((int)ButtonGPC.Down)) - state.Buttons |= (uint)RawButton.DpadDown; - if (OVR_GamepadController_GetButton((int)ButtonGPC.Left)) - state.Buttons |= (uint)RawButton.DpadLeft; - if (OVR_GamepadController_GetButton((int)ButtonGPC.Right)) - state.Buttons |= (uint)RawButton.DpadRight; - if (OVR_GamepadController_GetButton((int)ButtonGPC.Start)) - state.Buttons |= (uint)RawButton.Start; - if (OVR_GamepadController_GetButton((int)ButtonGPC.Back)) - state.Buttons |= (uint)RawButton.Back; - if (OVR_GamepadController_GetButton((int)ButtonGPC.LStick)) - state.Buttons |= (uint)RawButton.LThumbstick; - if (OVR_GamepadController_GetButton((int)ButtonGPC.RStick)) - state.Buttons |= (uint)RawButton.RThumbstick; - if (OVR_GamepadController_GetButton((int)ButtonGPC.LeftShoulder)) - state.Buttons |= (uint)RawButton.LShoulder; - if (OVR_GamepadController_GetButton((int)ButtonGPC.RightShoulder)) - state.Buttons |= (uint)RawButton.RShoulder; - - state.LThumbstick.x = OVR_GamepadController_GetAxis((int)AxisGPC.LeftXAxis); - state.LThumbstick.y = OVR_GamepadController_GetAxis((int)AxisGPC.LeftYAxis); - state.RThumbstick.x = OVR_GamepadController_GetAxis((int)AxisGPC.RightXAxis); - state.RThumbstick.y = OVR_GamepadController_GetAxis((int)AxisGPC.RightYAxis); - state.LIndexTrigger = OVR_GamepadController_GetAxis((int)AxisGPC.LeftTrigger); - state.RIndexTrigger = OVR_GamepadController_GetAxis((int)AxisGPC.RightTrigger); - - if (state.LIndexTrigger >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LIndexTrigger; - if (state.LHandTrigger >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LHandTrigger; - if (state.LThumbstick.y >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LThumbstickUp; - if (state.LThumbstick.y <= -AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LThumbstickDown; - if (state.LThumbstick.x <= -AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LThumbstickLeft; - if (state.LThumbstick.x >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LThumbstickRight; - - if (state.RIndexTrigger >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RIndexTrigger; - if (state.RHandTrigger >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RHandTrigger; - if (state.RThumbstick.y >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RThumbstickUp; - if (state.RThumbstick.y <= -AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RThumbstickDown; - if (state.RThumbstick.x <= -AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RThumbstickLeft; - if (state.RThumbstick.x >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RThumbstickRight; - - previousState = currentState; - currentState = state; - - return ((Controller)currentState.ConnectedControllers & controllerType); - } - - public override void ConfigureButtonMap() - { - buttonMap.None = RawButton.None; - buttonMap.One = RawButton.A; - buttonMap.Two = RawButton.B; - buttonMap.Three = RawButton.X; - buttonMap.Four = RawButton.Y; - buttonMap.Start = RawButton.Start; - buttonMap.Back = RawButton.Back; - buttonMap.PrimaryShoulder = RawButton.LShoulder; - buttonMap.PrimaryIndexTrigger = RawButton.LIndexTrigger; - buttonMap.PrimaryHandTrigger = RawButton.None; - buttonMap.PrimaryThumbstick = RawButton.LThumbstick; - buttonMap.PrimaryThumbstickUp = RawButton.LThumbstickUp; - buttonMap.PrimaryThumbstickDown = RawButton.LThumbstickDown; - buttonMap.PrimaryThumbstickLeft = RawButton.LThumbstickLeft; - buttonMap.PrimaryThumbstickRight = RawButton.LThumbstickRight; - buttonMap.PrimaryTouchpad = RawButton.None; - buttonMap.SecondaryShoulder = RawButton.RShoulder; - buttonMap.SecondaryIndexTrigger = RawButton.RIndexTrigger; - buttonMap.SecondaryHandTrigger = RawButton.None; - buttonMap.SecondaryThumbstick = RawButton.RThumbstick; - buttonMap.SecondaryThumbstickUp = RawButton.RThumbstickUp; - buttonMap.SecondaryThumbstickDown = RawButton.RThumbstickDown; - buttonMap.SecondaryThumbstickLeft = RawButton.RThumbstickLeft; - buttonMap.SecondaryThumbstickRight = RawButton.RThumbstickRight; - buttonMap.SecondaryTouchpad = RawButton.None; - buttonMap.DpadUp = RawButton.DpadUp; - buttonMap.DpadDown = RawButton.DpadDown; - buttonMap.DpadLeft = RawButton.DpadLeft; - buttonMap.DpadRight = RawButton.DpadRight; - buttonMap.Up = RawButton.LThumbstickUp; - buttonMap.Down = RawButton.LThumbstickDown; - buttonMap.Left = RawButton.LThumbstickLeft; - buttonMap.Right = RawButton.LThumbstickRight; - } - - public override void ConfigureTouchMap() - { - touchMap.None = RawTouch.None; - touchMap.One = RawTouch.None; - touchMap.Two = RawTouch.None; - touchMap.Three = RawTouch.None; - touchMap.Four = RawTouch.None; - touchMap.PrimaryIndexTrigger = RawTouch.None; - touchMap.PrimaryThumbstick = RawTouch.None; - touchMap.PrimaryThumbRest = RawTouch.None; - touchMap.PrimaryTouchpad = RawTouch.None; - touchMap.SecondaryIndexTrigger = RawTouch.None; - touchMap.SecondaryThumbstick = RawTouch.None; - touchMap.SecondaryThumbRest = RawTouch.None; - touchMap.SecondaryTouchpad = RawTouch.None; - } - - public override void ConfigureNearTouchMap() - { - nearTouchMap.None = RawNearTouch.None; - nearTouchMap.PrimaryIndexTrigger = RawNearTouch.None; - nearTouchMap.PrimaryThumbButtons = RawNearTouch.None; - nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; - nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; - } - - public override void ConfigureAxis1DMap() - { - axis1DMap.None = RawAxis1D.None; - axis1DMap.PrimaryIndexTrigger = RawAxis1D.LIndexTrigger; - axis1DMap.PrimaryHandTrigger = RawAxis1D.None; - axis1DMap.SecondaryIndexTrigger = RawAxis1D.RIndexTrigger; - axis1DMap.SecondaryHandTrigger = RawAxis1D.None; - } - - public override void ConfigureAxis2DMap() - { - axis2DMap.None = RawAxis2D.None; - axis2DMap.PrimaryThumbstick = RawAxis2D.LThumbstick; - axis2DMap.PrimaryTouchpad = RawAxis2D.None; - axis2DMap.SecondaryThumbstick = RawAxis2D.RThumbstick; - axis2DMap.SecondaryTouchpad = RawAxis2D.None; - } - - public override void SetControllerVibration(float frequency, float amplitude) - { - int gpcNode = 0; - float gpcFrequency = frequency * 200.0f; //Map frequency from 0-1 CAPI range to 0-200 GPC range - float gpcStrength = amplitude; - - OVR_GamepadController_SetVibration(gpcNode, gpcStrength, gpcFrequency); - } - - private const string DllName = "OVRGamepad"; - - [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] - private static extern bool OVR_GamepadController_Initialize(); - [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] - private static extern bool OVR_GamepadController_Destroy(); - [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] - private static extern bool OVR_GamepadController_Update(); - [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] - private static extern float OVR_GamepadController_GetAxis(int axis); - [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] - private static extern bool OVR_GamepadController_GetButton(int button); - [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] - private static extern bool OVR_GamepadController_SetVibration(int node, float strength, float frequency); - } + public override void SetControllerVibration(float frequency, float amplitude) + { - private class OVRControllerGamepadAndroid : OVRControllerBase - { - private static class AndroidButtonNames - { - public static readonly KeyCode A = KeyCode.JoystickButton0; - public static readonly KeyCode B = KeyCode.JoystickButton1; - public static readonly KeyCode X = KeyCode.JoystickButton2; - public static readonly KeyCode Y = KeyCode.JoystickButton3; - public static readonly KeyCode Start = KeyCode.JoystickButton10; - public static readonly KeyCode Back = KeyCode.JoystickButton11; - public static readonly KeyCode LThumbstick = KeyCode.JoystickButton8; - public static readonly KeyCode RThumbstick = KeyCode.JoystickButton9; - public static readonly KeyCode LShoulder = KeyCode.JoystickButton4; - public static readonly KeyCode RShoulder = KeyCode.JoystickButton5; - } - - private static class AndroidAxisNames - { - public static readonly string LThumbstickX = "Oculus_GearVR_LThumbstickX"; - public static readonly string LThumbstickY = "Oculus_GearVR_LThumbstickY"; - public static readonly string RThumbstickX = "Oculus_GearVR_RThumbstickX"; - public static readonly string RThumbstickY = "Oculus_GearVR_RThumbstickY"; - public static readonly string LIndexTrigger = "Oculus_GearVR_LIndexTrigger"; - public static readonly string RIndexTrigger = "Oculus_GearVR_RIndexTrigger"; - public static readonly string DpadX = "Oculus_GearVR_DpadX"; - public static readonly string DpadY = "Oculus_GearVR_DpadY"; - } - - private bool joystickDetected = false; - private float joystickCheckInterval = 1.0f; - private float joystickCheckTime = 0.0f; - - public OVRControllerGamepadAndroid() - { - controllerType = Controller.Gamepad; - } - - private bool ShouldUpdate() - { - // Use Unity's joystick detection as a quick way to determine joystick availability. - if ((Time.realtimeSinceStartup - joystickCheckTime) > joystickCheckInterval) - { - joystickCheckTime = Time.realtimeSinceStartup; - joystickDetected = false; - var joystickNames = UnityEngine.Input.GetJoystickNames(); - - for (int i = 0; i < joystickNames.Length; i++) - { - if (joystickNames[i] != String.Empty) - { - joystickDetected = true; - break; - } - } - } - - return joystickDetected; - } - - public override Controller Update() - { - if (!ShouldUpdate()) - { - return Controller.None; - } - - OVRPlugin.ControllerState2 state = new OVRPlugin.ControllerState2(); - - state.ConnectedControllers = (uint)Controller.Gamepad; - - if (Input.GetKey(AndroidButtonNames.A)) - state.Buttons |= (uint)RawButton.A; - if (Input.GetKey(AndroidButtonNames.B)) - state.Buttons |= (uint)RawButton.B; - if (Input.GetKey(AndroidButtonNames.X)) - state.Buttons |= (uint)RawButton.X; - if (Input.GetKey(AndroidButtonNames.Y)) - state.Buttons |= (uint)RawButton.Y; - if (Input.GetKey(AndroidButtonNames.Start)) - state.Buttons |= (uint)RawButton.Start; - if (Input.GetKey(AndroidButtonNames.Back) || Input.GetKey(KeyCode.Escape)) - state.Buttons |= (uint)RawButton.Back; - if (Input.GetKey(AndroidButtonNames.LThumbstick)) - state.Buttons |= (uint)RawButton.LThumbstick; - if (Input.GetKey(AndroidButtonNames.RThumbstick)) - state.Buttons |= (uint)RawButton.RThumbstick; - if (Input.GetKey(AndroidButtonNames.LShoulder)) - state.Buttons |= (uint)RawButton.LShoulder; - if (Input.GetKey(AndroidButtonNames.RShoulder)) - state.Buttons |= (uint)RawButton.RShoulder; - - state.LThumbstick.x = Input.GetAxisRaw(AndroidAxisNames.LThumbstickX); - state.LThumbstick.y = Input.GetAxisRaw(AndroidAxisNames.LThumbstickY); - state.RThumbstick.x = Input.GetAxisRaw(AndroidAxisNames.RThumbstickX); - state.RThumbstick.y = Input.GetAxisRaw(AndroidAxisNames.RThumbstickY); - state.LIndexTrigger = Input.GetAxisRaw(AndroidAxisNames.LIndexTrigger); - state.RIndexTrigger = Input.GetAxisRaw(AndroidAxisNames.RIndexTrigger); - - if (state.LIndexTrigger >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LIndexTrigger; - if (state.LHandTrigger >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LHandTrigger; - if (state.LThumbstick.y >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LThumbstickUp; - if (state.LThumbstick.y <= -AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LThumbstickDown; - if (state.LThumbstick.x <= -AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LThumbstickLeft; - if (state.LThumbstick.x >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.LThumbstickRight; - - if (state.RIndexTrigger >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RIndexTrigger; - if (state.RHandTrigger >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RHandTrigger; - if (state.RThumbstick.y >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RThumbstickUp; - if (state.RThumbstick.y <= -AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RThumbstickDown; - if (state.RThumbstick.x <= -AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RThumbstickLeft; - if (state.RThumbstick.x >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.RThumbstickRight; - - float dpadX = Input.GetAxisRaw(AndroidAxisNames.DpadX); - float dpadY = Input.GetAxisRaw(AndroidAxisNames.DpadY); - - if (dpadX <= -AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.DpadLeft; - if (dpadX >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.DpadRight; - if (dpadY <= -AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.DpadDown; - if (dpadY >= AXIS_AS_BUTTON_THRESHOLD) - state.Buttons |= (uint)RawButton.DpadUp; - - previousState = currentState; - currentState = state; - - return ((Controller)currentState.ConnectedControllers & controllerType); - } - - public override void ConfigureButtonMap() - { - buttonMap.None = RawButton.None; - buttonMap.One = RawButton.A; - buttonMap.Two = RawButton.B; - buttonMap.Three = RawButton.X; - buttonMap.Four = RawButton.Y; - buttonMap.Start = RawButton.Start; - buttonMap.Back = RawButton.Back; - buttonMap.PrimaryShoulder = RawButton.LShoulder; - buttonMap.PrimaryIndexTrigger = RawButton.LIndexTrigger; - buttonMap.PrimaryHandTrigger = RawButton.None; - buttonMap.PrimaryThumbstick = RawButton.LThumbstick; - buttonMap.PrimaryThumbstickUp = RawButton.LThumbstickUp; - buttonMap.PrimaryThumbstickDown = RawButton.LThumbstickDown; - buttonMap.PrimaryThumbstickLeft = RawButton.LThumbstickLeft; - buttonMap.PrimaryThumbstickRight = RawButton.LThumbstickRight; - buttonMap.PrimaryTouchpad = RawButton.None; - buttonMap.SecondaryShoulder = RawButton.RShoulder; - buttonMap.SecondaryIndexTrigger = RawButton.RIndexTrigger; - buttonMap.SecondaryHandTrigger = RawButton.None; - buttonMap.SecondaryThumbstick = RawButton.RThumbstick; - buttonMap.SecondaryThumbstickUp = RawButton.RThumbstickUp; - buttonMap.SecondaryThumbstickDown = RawButton.RThumbstickDown; - buttonMap.SecondaryThumbstickLeft = RawButton.RThumbstickLeft; - buttonMap.SecondaryThumbstickRight = RawButton.RThumbstickRight; - buttonMap.SecondaryTouchpad = RawButton.None; - buttonMap.DpadUp = RawButton.DpadUp; - buttonMap.DpadDown = RawButton.DpadDown; - buttonMap.DpadLeft = RawButton.DpadLeft; - buttonMap.DpadRight = RawButton.DpadRight; - buttonMap.Up = RawButton.LThumbstickUp; - buttonMap.Down = RawButton.LThumbstickDown; - buttonMap.Left = RawButton.LThumbstickLeft; - buttonMap.Right = RawButton.LThumbstickRight; - } - - public override void ConfigureTouchMap() - { - touchMap.None = RawTouch.None; - touchMap.One = RawTouch.None; - touchMap.Two = RawTouch.None; - touchMap.Three = RawTouch.None; - touchMap.Four = RawTouch.None; - touchMap.PrimaryIndexTrigger = RawTouch.None; - touchMap.PrimaryThumbstick = RawTouch.None; - touchMap.PrimaryThumbRest = RawTouch.None; - touchMap.PrimaryTouchpad = RawTouch.None; - touchMap.SecondaryIndexTrigger = RawTouch.None; - touchMap.SecondaryThumbstick = RawTouch.None; - touchMap.SecondaryThumbRest = RawTouch.None; - touchMap.SecondaryTouchpad = RawTouch.None; - } - - public override void ConfigureNearTouchMap() - { - nearTouchMap.None = RawNearTouch.None; - nearTouchMap.PrimaryIndexTrigger = RawNearTouch.None; - nearTouchMap.PrimaryThumbButtons = RawNearTouch.None; - nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; - nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; - } - - public override void ConfigureAxis1DMap() - { - axis1DMap.None = RawAxis1D.None; - axis1DMap.PrimaryIndexTrigger = RawAxis1D.LIndexTrigger; - axis1DMap.PrimaryHandTrigger = RawAxis1D.None; - axis1DMap.SecondaryIndexTrigger = RawAxis1D.RIndexTrigger; - axis1DMap.SecondaryHandTrigger = RawAxis1D.None; - } - - public override void ConfigureAxis2DMap() - { - axis2DMap.None = RawAxis2D.None; - axis2DMap.PrimaryThumbstick = RawAxis2D.LThumbstick; - axis2DMap.PrimaryTouchpad = RawAxis2D.None; - axis2DMap.SecondaryThumbstick = RawAxis2D.RThumbstick; - axis2DMap.SecondaryTouchpad = RawAxis2D.None; - } - - public override void SetControllerVibration(float frequency, float amplitude) - { - - } - } + } + } - private class OVRControllerTouchpad : OVRControllerBase - { + private class OVRControllerTouchpad : OVRControllerBase + { private OVRPlugin.Vector2f moveAmount; - private float maxTapMagnitude = 0.1f; - private float minMoveMagnitude = 0.15f; + private float maxTapMagnitude = 0.1f; + private float minMoveMagnitude = 0.15f; - public OVRControllerTouchpad() - { - controllerType = Controller.Touchpad; - } + public OVRControllerTouchpad() + { + controllerType = Controller.Touchpad; + } - public override Controller Update() + public override Controller Update() { Controller res = base.Update(); if (GetDown(RawTouch.LTouchpad, OVRInput.Controller.Touchpad)) - { + { moveAmount = currentState.LTouchpad; - } + } if (GetUp(RawTouch.LTouchpad, OVRInput.Controller.Touchpad)) - { - moveAmount.x = previousState.LTouchpad.x - moveAmount.x; - moveAmount.y = previousState.LTouchpad.y - moveAmount.y; + { + moveAmount.x = previousState.LTouchpad.x - moveAmount.x; + moveAmount.y = previousState.LTouchpad.y - moveAmount.y; - Vector2 move = new Vector2(moveAmount.x, moveAmount.y); + Vector2 move = new Vector2(moveAmount.x, moveAmount.y); float moveMag = move.magnitude; if (moveMag < maxTapMagnitude) @@ -2512,223 +2512,223 @@ public override Controller Update() currentState.Buttons |= (uint)RawButton.Start; currentState.Buttons |= (uint)RawButton.LTouchpad; } - else if (moveMag >= minMoveMagnitude) - { - move.Normalize(); - - // Left/Right - if (Mathf.Abs(move.x) > Mathf.Abs(move.y)) - { - if (move.x < 0.0f) - { - currentState.Buttons |= (uint)RawButton.DpadLeft; - } - else - { - currentState.Buttons |= (uint)RawButton.DpadRight; - } - } - // Up/Down - else - { - if (move.y < 0.0f) - { - currentState.Buttons |= (uint)RawButton.DpadDown; - } - else - { - currentState.Buttons |= (uint)RawButton.DpadUp; - } - } - } - } + else if (moveMag >= minMoveMagnitude) + { + move.Normalize(); + + // Left/Right + if (Mathf.Abs(move.x) > Mathf.Abs(move.y)) + { + if (move.x < 0.0f) + { + currentState.Buttons |= (uint)RawButton.DpadLeft; + } + else + { + currentState.Buttons |= (uint)RawButton.DpadRight; + } + } + // Up/Down + else + { + if (move.y < 0.0f) + { + currentState.Buttons |= (uint)RawButton.DpadDown; + } + else + { + currentState.Buttons |= (uint)RawButton.DpadUp; + } + } + } + } return res; } - public override void ConfigureButtonMap() - { - buttonMap.None = RawButton.None; - buttonMap.One = RawButton.Start; - buttonMap.Two = RawButton.Back; - buttonMap.Three = RawButton.None; - buttonMap.Four = RawButton.None; - buttonMap.Start = RawButton.Start; - buttonMap.Back = RawButton.Back; - buttonMap.PrimaryShoulder = RawButton.None; - buttonMap.PrimaryIndexTrigger = RawButton.None; - buttonMap.PrimaryHandTrigger = RawButton.None; - buttonMap.PrimaryThumbstick = RawButton.None; - buttonMap.PrimaryThumbstickUp = RawButton.None; - buttonMap.PrimaryThumbstickDown = RawButton.None; - buttonMap.PrimaryThumbstickLeft = RawButton.None; - buttonMap.PrimaryThumbstickRight = RawButton.None; - buttonMap.PrimaryTouchpad = RawButton.LTouchpad; - buttonMap.SecondaryShoulder = RawButton.None; - buttonMap.SecondaryIndexTrigger = RawButton.None; - buttonMap.SecondaryHandTrigger = RawButton.None; - buttonMap.SecondaryThumbstick = RawButton.None; - buttonMap.SecondaryThumbstickUp = RawButton.None; - buttonMap.SecondaryThumbstickDown = RawButton.None; - buttonMap.SecondaryThumbstickLeft = RawButton.None; - buttonMap.SecondaryThumbstickRight = RawButton.None; - buttonMap.SecondaryTouchpad = RawButton.None; - buttonMap.DpadUp = RawButton.DpadUp; - buttonMap.DpadDown = RawButton.DpadDown; - buttonMap.DpadLeft = RawButton.DpadLeft; - buttonMap.DpadRight = RawButton.DpadRight; - buttonMap.Up = RawButton.DpadUp; - buttonMap.Down = RawButton.DpadDown; - buttonMap.Left = RawButton.DpadLeft; - buttonMap.Right = RawButton.DpadRight; - } - - public override void ConfigureTouchMap() - { - touchMap.None = RawTouch.None; - touchMap.One = RawTouch.None; - touchMap.Two = RawTouch.None; - touchMap.Three = RawTouch.None; - touchMap.Four = RawTouch.None; - touchMap.PrimaryIndexTrigger = RawTouch.None; - touchMap.PrimaryThumbstick = RawTouch.None; - touchMap.PrimaryThumbRest = RawTouch.None; - touchMap.PrimaryTouchpad = RawTouch.LTouchpad; - touchMap.SecondaryIndexTrigger = RawTouch.None; - touchMap.SecondaryThumbstick = RawTouch.None; - touchMap.SecondaryThumbRest = RawTouch.None; - touchMap.SecondaryTouchpad = RawTouch.None; - } - - public override void ConfigureNearTouchMap() - { - nearTouchMap.None = RawNearTouch.None; - nearTouchMap.PrimaryIndexTrigger = RawNearTouch.None; - nearTouchMap.PrimaryThumbButtons = RawNearTouch.None; - nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; - nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; - } - - public override void ConfigureAxis1DMap() - { - axis1DMap.None = RawAxis1D.None; - axis1DMap.PrimaryIndexTrigger = RawAxis1D.None; - axis1DMap.PrimaryHandTrigger = RawAxis1D.None; - axis1DMap.SecondaryIndexTrigger = RawAxis1D.None; - axis1DMap.SecondaryHandTrigger = RawAxis1D.None; - } - - public override void ConfigureAxis2DMap() - { - axis2DMap.None = RawAxis2D.None; - axis2DMap.PrimaryThumbstick = RawAxis2D.None; - axis2DMap.PrimaryTouchpad = RawAxis2D.LTouchpad; - axis2DMap.SecondaryThumbstick = RawAxis2D.None; - axis2DMap.SecondaryTouchpad = RawAxis2D.None; - } - } + public override void ConfigureButtonMap() + { + buttonMap.None = RawButton.None; + buttonMap.One = RawButton.Start; + buttonMap.Two = RawButton.Back; + buttonMap.Three = RawButton.None; + buttonMap.Four = RawButton.None; + buttonMap.Start = RawButton.Start; + buttonMap.Back = RawButton.Back; + buttonMap.PrimaryShoulder = RawButton.None; + buttonMap.PrimaryIndexTrigger = RawButton.None; + buttonMap.PrimaryHandTrigger = RawButton.None; + buttonMap.PrimaryThumbstick = RawButton.None; + buttonMap.PrimaryThumbstickUp = RawButton.None; + buttonMap.PrimaryThumbstickDown = RawButton.None; + buttonMap.PrimaryThumbstickLeft = RawButton.None; + buttonMap.PrimaryThumbstickRight = RawButton.None; + buttonMap.PrimaryTouchpad = RawButton.LTouchpad; + buttonMap.SecondaryShoulder = RawButton.None; + buttonMap.SecondaryIndexTrigger = RawButton.None; + buttonMap.SecondaryHandTrigger = RawButton.None; + buttonMap.SecondaryThumbstick = RawButton.None; + buttonMap.SecondaryThumbstickUp = RawButton.None; + buttonMap.SecondaryThumbstickDown = RawButton.None; + buttonMap.SecondaryThumbstickLeft = RawButton.None; + buttonMap.SecondaryThumbstickRight = RawButton.None; + buttonMap.SecondaryTouchpad = RawButton.None; + buttonMap.DpadUp = RawButton.DpadUp; + buttonMap.DpadDown = RawButton.DpadDown; + buttonMap.DpadLeft = RawButton.DpadLeft; + buttonMap.DpadRight = RawButton.DpadRight; + buttonMap.Up = RawButton.DpadUp; + buttonMap.Down = RawButton.DpadDown; + buttonMap.Left = RawButton.DpadLeft; + buttonMap.Right = RawButton.DpadRight; + } + + public override void ConfigureTouchMap() + { + touchMap.None = RawTouch.None; + touchMap.One = RawTouch.None; + touchMap.Two = RawTouch.None; + touchMap.Three = RawTouch.None; + touchMap.Four = RawTouch.None; + touchMap.PrimaryIndexTrigger = RawTouch.None; + touchMap.PrimaryThumbstick = RawTouch.None; + touchMap.PrimaryThumbRest = RawTouch.None; + touchMap.PrimaryTouchpad = RawTouch.LTouchpad; + touchMap.SecondaryIndexTrigger = RawTouch.None; + touchMap.SecondaryThumbstick = RawTouch.None; + touchMap.SecondaryThumbRest = RawTouch.None; + touchMap.SecondaryTouchpad = RawTouch.None; + } + + public override void ConfigureNearTouchMap() + { + nearTouchMap.None = RawNearTouch.None; + nearTouchMap.PrimaryIndexTrigger = RawNearTouch.None; + nearTouchMap.PrimaryThumbButtons = RawNearTouch.None; + nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; + nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; + } + + public override void ConfigureAxis1DMap() + { + axis1DMap.None = RawAxis1D.None; + axis1DMap.PrimaryIndexTrigger = RawAxis1D.None; + axis1DMap.PrimaryHandTrigger = RawAxis1D.None; + axis1DMap.SecondaryIndexTrigger = RawAxis1D.None; + axis1DMap.SecondaryHandTrigger = RawAxis1D.None; + } + + public override void ConfigureAxis2DMap() + { + axis2DMap.None = RawAxis2D.None; + axis2DMap.PrimaryThumbstick = RawAxis2D.None; + axis2DMap.PrimaryTouchpad = RawAxis2D.LTouchpad; + axis2DMap.SecondaryThumbstick = RawAxis2D.None; + axis2DMap.SecondaryTouchpad = RawAxis2D.None; + } + } - private class OVRControllerLTrackedRemote : OVRControllerBase - { + private class OVRControllerLTrackedRemote : OVRControllerBase + { private bool emitSwipe; private OVRPlugin.Vector2f moveAmount; - private float minMoveMagnitude = 0.3f; - - public OVRControllerLTrackedRemote() - { - controllerType = Controller.LTrackedRemote; - } - - public override void ConfigureButtonMap() - { - buttonMap.None = RawButton.None; - buttonMap.One = RawButton.LTouchpad; - buttonMap.Two = RawButton.Back; - buttonMap.Three = RawButton.None; - buttonMap.Four = RawButton.None; - buttonMap.Start = RawButton.Start; - buttonMap.Back = RawButton.Back; - buttonMap.PrimaryShoulder = RawButton.None; - buttonMap.PrimaryIndexTrigger = RawButton.LIndexTrigger; - buttonMap.PrimaryHandTrigger = RawButton.None; - buttonMap.PrimaryThumbstick = RawButton.None; - buttonMap.PrimaryThumbstickUp = RawButton.None; - buttonMap.PrimaryThumbstickDown = RawButton.None; - buttonMap.PrimaryThumbstickLeft = RawButton.None; - buttonMap.PrimaryThumbstickRight = RawButton.None; - buttonMap.PrimaryTouchpad = RawButton.LTouchpad; - buttonMap.SecondaryShoulder = RawButton.None; - buttonMap.SecondaryIndexTrigger = RawButton.None; - buttonMap.SecondaryHandTrigger = RawButton.None; - buttonMap.SecondaryThumbstick = RawButton.None; - buttonMap.SecondaryThumbstickUp = RawButton.None; - buttonMap.SecondaryThumbstickDown = RawButton.None; - buttonMap.SecondaryThumbstickLeft = RawButton.None; - buttonMap.SecondaryThumbstickRight = RawButton.None; - buttonMap.SecondaryTouchpad = RawButton.None; - buttonMap.DpadUp = RawButton.DpadUp; - buttonMap.DpadDown = RawButton.DpadDown; - buttonMap.DpadLeft = RawButton.DpadLeft; - buttonMap.DpadRight = RawButton.DpadRight; - buttonMap.Up = RawButton.DpadUp; - buttonMap.Down = RawButton.DpadDown; - buttonMap.Left = RawButton.DpadLeft; - buttonMap.Right = RawButton.DpadRight; - } - - public override void ConfigureTouchMap() - { - touchMap.None = RawTouch.None; - touchMap.One = RawTouch.None; - touchMap.Two = RawTouch.None; - touchMap.Three = RawTouch.None; - touchMap.Four = RawTouch.None; - touchMap.PrimaryIndexTrigger = RawTouch.None; - touchMap.PrimaryThumbstick = RawTouch.None; - touchMap.PrimaryThumbRest = RawTouch.None; - touchMap.PrimaryTouchpad = RawTouch.LTouchpad; - touchMap.SecondaryIndexTrigger = RawTouch.None; - touchMap.SecondaryThumbstick = RawTouch.None; - touchMap.SecondaryThumbRest = RawTouch.None; - touchMap.SecondaryTouchpad = RawTouch.None; - } - - public override void ConfigureNearTouchMap() - { - nearTouchMap.None = RawNearTouch.None; - nearTouchMap.PrimaryIndexTrigger = RawNearTouch.None; - nearTouchMap.PrimaryThumbButtons = RawNearTouch.None; - nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; - nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; - } - - public override void ConfigureAxis1DMap() - { - axis1DMap.None = RawAxis1D.None; - axis1DMap.PrimaryIndexTrigger = RawAxis1D.None; - axis1DMap.PrimaryHandTrigger = RawAxis1D.None; - axis1DMap.SecondaryIndexTrigger = RawAxis1D.None; - axis1DMap.SecondaryHandTrigger = RawAxis1D.None; - } - - public override void ConfigureAxis2DMap() - { - axis2DMap.None = RawAxis2D.None; - axis2DMap.PrimaryThumbstick = RawAxis2D.None; - axis2DMap.PrimaryTouchpad = RawAxis2D.LTouchpad; - axis2DMap.SecondaryThumbstick = RawAxis2D.None; - axis2DMap.SecondaryTouchpad = RawAxis2D.None; - } + private float minMoveMagnitude = 0.3f; + + public OVRControllerLTrackedRemote() + { + controllerType = Controller.LTrackedRemote; + } + + public override void ConfigureButtonMap() + { + buttonMap.None = RawButton.None; + buttonMap.One = RawButton.LTouchpad; + buttonMap.Two = RawButton.Back; + buttonMap.Three = RawButton.None; + buttonMap.Four = RawButton.None; + buttonMap.Start = RawButton.Start; + buttonMap.Back = RawButton.Back; + buttonMap.PrimaryShoulder = RawButton.None; + buttonMap.PrimaryIndexTrigger = RawButton.LIndexTrigger; + buttonMap.PrimaryHandTrigger = RawButton.None; + buttonMap.PrimaryThumbstick = RawButton.None; + buttonMap.PrimaryThumbstickUp = RawButton.None; + buttonMap.PrimaryThumbstickDown = RawButton.None; + buttonMap.PrimaryThumbstickLeft = RawButton.None; + buttonMap.PrimaryThumbstickRight = RawButton.None; + buttonMap.PrimaryTouchpad = RawButton.LTouchpad; + buttonMap.SecondaryShoulder = RawButton.None; + buttonMap.SecondaryIndexTrigger = RawButton.None; + buttonMap.SecondaryHandTrigger = RawButton.None; + buttonMap.SecondaryThumbstick = RawButton.None; + buttonMap.SecondaryThumbstickUp = RawButton.None; + buttonMap.SecondaryThumbstickDown = RawButton.None; + buttonMap.SecondaryThumbstickLeft = RawButton.None; + buttonMap.SecondaryThumbstickRight = RawButton.None; + buttonMap.SecondaryTouchpad = RawButton.None; + buttonMap.DpadUp = RawButton.DpadUp; + buttonMap.DpadDown = RawButton.DpadDown; + buttonMap.DpadLeft = RawButton.DpadLeft; + buttonMap.DpadRight = RawButton.DpadRight; + buttonMap.Up = RawButton.DpadUp; + buttonMap.Down = RawButton.DpadDown; + buttonMap.Left = RawButton.DpadLeft; + buttonMap.Right = RawButton.DpadRight; + } + + public override void ConfigureTouchMap() + { + touchMap.None = RawTouch.None; + touchMap.One = RawTouch.None; + touchMap.Two = RawTouch.None; + touchMap.Three = RawTouch.None; + touchMap.Four = RawTouch.None; + touchMap.PrimaryIndexTrigger = RawTouch.None; + touchMap.PrimaryThumbstick = RawTouch.None; + touchMap.PrimaryThumbRest = RawTouch.None; + touchMap.PrimaryTouchpad = RawTouch.LTouchpad; + touchMap.SecondaryIndexTrigger = RawTouch.None; + touchMap.SecondaryThumbstick = RawTouch.None; + touchMap.SecondaryThumbRest = RawTouch.None; + touchMap.SecondaryTouchpad = RawTouch.None; + } + + public override void ConfigureNearTouchMap() + { + nearTouchMap.None = RawNearTouch.None; + nearTouchMap.PrimaryIndexTrigger = RawNearTouch.None; + nearTouchMap.PrimaryThumbButtons = RawNearTouch.None; + nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; + nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; + } + + public override void ConfigureAxis1DMap() + { + axis1DMap.None = RawAxis1D.None; + axis1DMap.PrimaryIndexTrigger = RawAxis1D.None; + axis1DMap.PrimaryHandTrigger = RawAxis1D.None; + axis1DMap.SecondaryIndexTrigger = RawAxis1D.None; + axis1DMap.SecondaryHandTrigger = RawAxis1D.None; + } + + public override void ConfigureAxis2DMap() + { + axis2DMap.None = RawAxis2D.None; + axis2DMap.PrimaryThumbstick = RawAxis2D.None; + axis2DMap.PrimaryTouchpad = RawAxis2D.LTouchpad; + axis2DMap.SecondaryThumbstick = RawAxis2D.None; + axis2DMap.SecondaryTouchpad = RawAxis2D.None; + } public override Controller Update() { Controller res = base.Update(); if (GetDown(RawTouch.LTouchpad, OVRInput.Controller.LTrackedRemote)) - { + { emitSwipe = true; moveAmount = currentState.LTouchpad; - } + } if (GetDown(RawButton.LTouchpad, OVRInput.Controller.LTrackedRemote)) { @@ -2736,195 +2736,195 @@ public override Controller Update() } if (GetUp(RawTouch.LTouchpad, OVRInput.Controller.LTrackedRemote) && emitSwipe) - { + { emitSwipe = false; - moveAmount.x = previousState.LTouchpad.x - moveAmount.x; - moveAmount.y = previousState.LTouchpad.y - moveAmount.y; - - Vector2 move = new Vector2(moveAmount.x, moveAmount.y); - - if (move.magnitude >= minMoveMagnitude) - { - move.Normalize(); - - // Left/Right - if (Mathf.Abs(move.x) > Mathf.Abs(move.y)) - { - if (move.x < 0.0f) - { - currentState.Buttons |= (uint)RawButton.DpadLeft; - } - else - { - currentState.Buttons |= (uint)RawButton.DpadRight; - } - } - // Up/Down - else - { - if (move.y < 0.0f) - { - currentState.Buttons |= (uint)RawButton.DpadDown; - } - else - { - currentState.Buttons |= (uint)RawButton.DpadUp; - } - } - } - } + moveAmount.x = previousState.LTouchpad.x - moveAmount.x; + moveAmount.y = previousState.LTouchpad.y - moveAmount.y; + + Vector2 move = new Vector2(moveAmount.x, moveAmount.y); + + if (move.magnitude >= minMoveMagnitude) + { + move.Normalize(); + + // Left/Right + if (Mathf.Abs(move.x) > Mathf.Abs(move.y)) + { + if (move.x < 0.0f) + { + currentState.Buttons |= (uint)RawButton.DpadLeft; + } + else + { + currentState.Buttons |= (uint)RawButton.DpadRight; + } + } + // Up/Down + else + { + if (move.y < 0.0f) + { + currentState.Buttons |= (uint)RawButton.DpadDown; + } + else + { + currentState.Buttons |= (uint)RawButton.DpadUp; + } + } + } + } return res; } - } + } - private class OVRControllerRTrackedRemote : OVRControllerBase - { + private class OVRControllerRTrackedRemote : OVRControllerBase + { private bool emitSwipe; private OVRPlugin.Vector2f moveAmount; - private float minMoveMagnitude = 0.3f; - - public OVRControllerRTrackedRemote() - { - controllerType = Controller.RTrackedRemote; - } - - public override void ConfigureButtonMap() - { - buttonMap.None = RawButton.None; - buttonMap.One = RawButton.RTouchpad; - buttonMap.Two = RawButton.Back; - buttonMap.Three = RawButton.None; - buttonMap.Four = RawButton.None; - buttonMap.Start = RawButton.Start; - buttonMap.Back = RawButton.Back; - buttonMap.PrimaryShoulder = RawButton.None; - buttonMap.PrimaryIndexTrigger = RawButton.RIndexTrigger; - buttonMap.PrimaryHandTrigger = RawButton.None; - buttonMap.PrimaryThumbstick = RawButton.None; - buttonMap.PrimaryThumbstickUp = RawButton.None; - buttonMap.PrimaryThumbstickDown = RawButton.None; - buttonMap.PrimaryThumbstickLeft = RawButton.None; - buttonMap.PrimaryThumbstickRight = RawButton.None; - buttonMap.PrimaryTouchpad = RawButton.RTouchpad; - buttonMap.SecondaryShoulder = RawButton.None; - buttonMap.SecondaryIndexTrigger = RawButton.None; - buttonMap.SecondaryHandTrigger = RawButton.None; - buttonMap.SecondaryThumbstick = RawButton.None; - buttonMap.SecondaryThumbstickUp = RawButton.None; - buttonMap.SecondaryThumbstickDown = RawButton.None; - buttonMap.SecondaryThumbstickLeft = RawButton.None; - buttonMap.SecondaryThumbstickRight = RawButton.None; - buttonMap.SecondaryTouchpad = RawButton.None; - buttonMap.DpadUp = RawButton.DpadUp; - buttonMap.DpadDown = RawButton.DpadDown; - buttonMap.DpadLeft = RawButton.DpadLeft; - buttonMap.DpadRight = RawButton.DpadRight; - buttonMap.Up = RawButton.DpadUp; - buttonMap.Down = RawButton.DpadDown; - buttonMap.Left = RawButton.DpadLeft; - buttonMap.Right = RawButton.DpadRight; - } - - public override void ConfigureTouchMap() - { - touchMap.None = RawTouch.None; - touchMap.One = RawTouch.None; - touchMap.Two = RawTouch.None; - touchMap.Three = RawTouch.None; - touchMap.Four = RawTouch.None; - touchMap.PrimaryIndexTrigger = RawTouch.None; - touchMap.PrimaryThumbstick = RawTouch.None; - touchMap.PrimaryThumbRest = RawTouch.None; - touchMap.PrimaryTouchpad = RawTouch.RTouchpad; - touchMap.SecondaryIndexTrigger = RawTouch.None; - touchMap.SecondaryThumbstick = RawTouch.None; - touchMap.SecondaryThumbRest = RawTouch.None; - touchMap.SecondaryTouchpad = RawTouch.None; - } - - public override void ConfigureNearTouchMap() - { - nearTouchMap.None = RawNearTouch.None; - nearTouchMap.PrimaryIndexTrigger = RawNearTouch.None; - nearTouchMap.PrimaryThumbButtons = RawNearTouch.None; - nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; - nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; - } - - public override void ConfigureAxis1DMap() - { - axis1DMap.None = RawAxis1D.None; - axis1DMap.PrimaryIndexTrigger = RawAxis1D.None; - axis1DMap.PrimaryHandTrigger = RawAxis1D.None; - axis1DMap.SecondaryIndexTrigger = RawAxis1D.None; - axis1DMap.SecondaryHandTrigger = RawAxis1D.None; - } - - public override void ConfigureAxis2DMap() - { - axis2DMap.None = RawAxis2D.None; - axis2DMap.PrimaryThumbstick = RawAxis2D.None; - axis2DMap.PrimaryTouchpad = RawAxis2D.RTouchpad; - axis2DMap.SecondaryThumbstick = RawAxis2D.None; - axis2DMap.SecondaryTouchpad = RawAxis2D.None; - } + private float minMoveMagnitude = 0.3f; + + public OVRControllerRTrackedRemote() + { + controllerType = Controller.RTrackedRemote; + } + + public override void ConfigureButtonMap() + { + buttonMap.None = RawButton.None; + buttonMap.One = RawButton.RTouchpad; + buttonMap.Two = RawButton.Back; + buttonMap.Three = RawButton.None; + buttonMap.Four = RawButton.None; + buttonMap.Start = RawButton.Start; + buttonMap.Back = RawButton.Back; + buttonMap.PrimaryShoulder = RawButton.None; + buttonMap.PrimaryIndexTrigger = RawButton.RIndexTrigger; + buttonMap.PrimaryHandTrigger = RawButton.None; + buttonMap.PrimaryThumbstick = RawButton.None; + buttonMap.PrimaryThumbstickUp = RawButton.None; + buttonMap.PrimaryThumbstickDown = RawButton.None; + buttonMap.PrimaryThumbstickLeft = RawButton.None; + buttonMap.PrimaryThumbstickRight = RawButton.None; + buttonMap.PrimaryTouchpad = RawButton.RTouchpad; + buttonMap.SecondaryShoulder = RawButton.None; + buttonMap.SecondaryIndexTrigger = RawButton.None; + buttonMap.SecondaryHandTrigger = RawButton.None; + buttonMap.SecondaryThumbstick = RawButton.None; + buttonMap.SecondaryThumbstickUp = RawButton.None; + buttonMap.SecondaryThumbstickDown = RawButton.None; + buttonMap.SecondaryThumbstickLeft = RawButton.None; + buttonMap.SecondaryThumbstickRight = RawButton.None; + buttonMap.SecondaryTouchpad = RawButton.None; + buttonMap.DpadUp = RawButton.DpadUp; + buttonMap.DpadDown = RawButton.DpadDown; + buttonMap.DpadLeft = RawButton.DpadLeft; + buttonMap.DpadRight = RawButton.DpadRight; + buttonMap.Up = RawButton.DpadUp; + buttonMap.Down = RawButton.DpadDown; + buttonMap.Left = RawButton.DpadLeft; + buttonMap.Right = RawButton.DpadRight; + } + + public override void ConfigureTouchMap() + { + touchMap.None = RawTouch.None; + touchMap.One = RawTouch.None; + touchMap.Two = RawTouch.None; + touchMap.Three = RawTouch.None; + touchMap.Four = RawTouch.None; + touchMap.PrimaryIndexTrigger = RawTouch.None; + touchMap.PrimaryThumbstick = RawTouch.None; + touchMap.PrimaryThumbRest = RawTouch.None; + touchMap.PrimaryTouchpad = RawTouch.RTouchpad; + touchMap.SecondaryIndexTrigger = RawTouch.None; + touchMap.SecondaryThumbstick = RawTouch.None; + touchMap.SecondaryThumbRest = RawTouch.None; + touchMap.SecondaryTouchpad = RawTouch.None; + } + + public override void ConfigureNearTouchMap() + { + nearTouchMap.None = RawNearTouch.None; + nearTouchMap.PrimaryIndexTrigger = RawNearTouch.None; + nearTouchMap.PrimaryThumbButtons = RawNearTouch.None; + nearTouchMap.SecondaryIndexTrigger = RawNearTouch.None; + nearTouchMap.SecondaryThumbButtons = RawNearTouch.None; + } + + public override void ConfigureAxis1DMap() + { + axis1DMap.None = RawAxis1D.None; + axis1DMap.PrimaryIndexTrigger = RawAxis1D.None; + axis1DMap.PrimaryHandTrigger = RawAxis1D.None; + axis1DMap.SecondaryIndexTrigger = RawAxis1D.None; + axis1DMap.SecondaryHandTrigger = RawAxis1D.None; + } + + public override void ConfigureAxis2DMap() + { + axis2DMap.None = RawAxis2D.None; + axis2DMap.PrimaryThumbstick = RawAxis2D.None; + axis2DMap.PrimaryTouchpad = RawAxis2D.RTouchpad; + axis2DMap.SecondaryThumbstick = RawAxis2D.None; + axis2DMap.SecondaryTouchpad = RawAxis2D.None; + } public override Controller Update() { Controller res = base.Update(); if (GetDown(RawTouch.RTouchpad, OVRInput.Controller.RTrackedRemote)) - { + { emitSwipe = true; moveAmount = currentState.RTouchpad; - } + } if (GetDown(RawButton.RTouchpad, OVRInput.Controller.RTrackedRemote)) - { + { emitSwipe = false; - } + } if (GetUp(RawTouch.RTouchpad, OVRInput.Controller.RTrackedRemote) && emitSwipe) - { + { emitSwipe = false; - moveAmount.x = previousState.RTouchpad.x - moveAmount.x; - moveAmount.y = previousState.RTouchpad.y - moveAmount.y; - - Vector2 move = new Vector2(moveAmount.x, moveAmount.y); - - if (move.magnitude >= minMoveMagnitude) - { - move.Normalize(); - - // Left/Right - if (Mathf.Abs(move.x) > Mathf.Abs(move.y)) - { - if (move.x < 0.0f) - { - currentState.Buttons |= (uint)RawButton.DpadLeft; - } - else - { - currentState.Buttons |= (uint)RawButton.DpadRight; - } - } - // Up/Down - else - { - if (move.y < 0.0f) - { - currentState.Buttons |= (uint)RawButton.DpadDown; - } - else - { - currentState.Buttons |= (uint)RawButton.DpadUp; - } - } - } - } + moveAmount.x = previousState.RTouchpad.x - moveAmount.x; + moveAmount.y = previousState.RTouchpad.y - moveAmount.y; + + Vector2 move = new Vector2(moveAmount.x, moveAmount.y); + + if (move.magnitude >= minMoveMagnitude) + { + move.Normalize(); + + // Left/Right + if (Mathf.Abs(move.x) > Mathf.Abs(move.y)) + { + if (move.x < 0.0f) + { + currentState.Buttons |= (uint)RawButton.DpadLeft; + } + else + { + currentState.Buttons |= (uint)RawButton.DpadRight; + } + } + // Up/Down + else + { + if (move.y < 0.0f) + { + currentState.Buttons |= (uint)RawButton.DpadDown; + } + else + { + currentState.Buttons |= (uint)RawButton.DpadUp; + } + } + } + } return res; } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRLint.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRLint.cs index 03b9a503..cdfbd466 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRLint.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRLint.cs @@ -222,16 +222,16 @@ static void RunCheck() mRecords.Clear(); CheckStaticCommonIssues(); - #if UNITY_ANDROID +#if UNITY_ANDROID CheckStaticAndroidIssues(); - #endif +#endif if (EditorApplication.isPlaying) { CheckRuntimeCommonIssues(); - #if UNITY_ANDROID +#if UNITY_ANDROID CheckRuntimeAndroidIssues(); - #endif +#endif } mRecords.Sort(delegate(FixRecord record1, FixRecord record2) @@ -255,11 +255,11 @@ static void CheckStaticCommonIssues () }, null, "Fix"); } - #if UNITY_ANDROID +#if UNITY_ANDROID int recommendedPixelLightCount = 1; - #else +#else int recommendedPixelLightCount = 3; - #endif +#endif if (QualitySettings.pixelLightCount > recommendedPixelLightCount) { @@ -728,15 +728,15 @@ enum LightmapType {Realtime = 4, Baked = 2, Mixed = 1}; static bool IsLightBaked(Light light) { - #if UNITY_5_6_OR_NEWER +#if UNITY_5_6_OR_NEWER return light.lightmapBakeType == LightmapBakeType.Baked; - #elif UNITY_5_5_OR_NEWER +#elif UNITY_5_5_OR_NEWER return light.lightmappingMode == LightmappingMode.Baked; - #else +#else SerializedObject serialObj = new SerializedObject(light); SerializedProperty lightmapProp = serialObj.FindProperty("m_Lightmapping"); return (LightmapType)lightmapProp.intValue == LightmapType.Baked; - #endif +#endif } static void SetAudioPreload( AudioClip clip, bool preload, bool refreshImmediately) diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRManager.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRManager.cs index 38e64c76..81312aad 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRManager.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRManager.cs @@ -34,255 +34,264 @@ limitations under the License. /// public class OVRManager : MonoBehaviour { - public enum TrackingOrigin - { - EyeLevel = OVRPlugin.TrackingOrigin.EyeLevel, - FloorLevel = OVRPlugin.TrackingOrigin.FloorLevel, - } + public enum TrackingOrigin + { + EyeLevel = OVRPlugin.TrackingOrigin.EyeLevel, + FloorLevel = OVRPlugin.TrackingOrigin.FloorLevel, + } - public enum EyeTextureFormat - { - Default = OVRPlugin.EyeTextureFormat.Default, - R16G16B16A16_FP = OVRPlugin.EyeTextureFormat.R16G16B16A16_FP, - R11G11B10_FP = OVRPlugin.EyeTextureFormat.R11G11B10_FP, - } + public enum EyeTextureFormat + { + Default = OVRPlugin.EyeTextureFormat.Default, + R16G16B16A16_FP = OVRPlugin.EyeTextureFormat.R16G16B16A16_FP, + R11G11B10_FP = OVRPlugin.EyeTextureFormat.R11G11B10_FP, + } - /// - /// Gets the singleton instance. - /// - public static OVRManager instance { get; private set; } - - /// - /// Gets a reference to the active display. - /// - public static OVRDisplay display { get; private set; } - - /// - /// Gets a reference to the active sensor. - /// - public static OVRTracker tracker { get; private set; } - - /// - /// Gets a reference to the active boundary system. - /// - public static OVRBoundary boundary { get; private set; } - - private static OVRProfile _profile; - /// - /// Gets the current profile, which contains information about the user's settings and body dimensions. - /// - public static OVRProfile profile - { - get { - if (_profile == null) - _profile = new OVRProfile(); + /// + /// Gets the singleton instance. + /// + public static OVRManager instance { get; private set; } - return _profile; - } - } + /// + /// Gets a reference to the active display. + /// + public static OVRDisplay display { get; private set; } - private IEnumerable disabledCameras; - float prevTimeScale; - - /// - /// Occurs when an HMD attached. - /// - public static event Action HMDAcquired; - - /// - /// Occurs when an HMD detached. - /// - public static event Action HMDLost; - - /// - /// Occurs when an HMD is put on the user's head. - /// - public static event Action HMDMounted; - - /// - /// Occurs when an HMD is taken off the user's head. - /// - public static event Action HMDUnmounted; - - /// - /// Occurs when VR Focus is acquired. - /// - public static event Action VrFocusAcquired; - - /// - /// Occurs when VR Focus is lost. - /// - public static event Action VrFocusLost; - - /// - /// Occurs when the active Audio Out device has changed and a restart is needed. - /// - public static event Action AudioOutChanged; - - /// - /// Occurs when the active Audio In device has changed and a restart is needed. - /// - public static event Action AudioInChanged; - - /// - /// Occurs when the sensor gained tracking. - /// - public static event Action TrackingAcquired; - - /// - /// Occurs when the sensor lost tracking. - /// - public static event Action TrackingLost; - - /// - /// Occurs when Health & Safety Warning is dismissed. - /// - //Disable the warning about it being unused. It's deprecated. - #pragma warning disable 0067 - [Obsolete] - public static event Action HSWDismissed; - #pragma warning restore - - private static bool _isHmdPresentCached = false; - private static bool _isHmdPresent = false; - private static bool _wasHmdPresent = false; - /// - /// If true, a head-mounted display is connected and present. - /// - public static bool isHmdPresent - { - get { - if (!_isHmdPresentCached) - { - _isHmdPresentCached = true; - _isHmdPresent = OVRPlugin.hmdPresent; - } - - return _isHmdPresent; - } + /// + /// Gets a reference to the active sensor. + /// + public static OVRTracker tracker { get; private set; } - private set { - _isHmdPresentCached = true; - _isHmdPresent = value; - } - } + /// + /// Gets a reference to the active boundary system. + /// + public static OVRBoundary boundary { get; private set; } - /// - /// Gets the audio output device identifier. - /// - /// - /// On Windows, this is a string containing the GUID of the IMMDevice for the Windows audio endpoint to use. - /// - public static string audioOutId - { - get { return OVRPlugin.audioOutId; } - } + private static OVRProfile _profile; + /// + /// Gets the current profile, which contains information about the user's settings and body dimensions. + /// + public static OVRProfile profile + { + get + { + if (_profile == null) + _profile = new OVRProfile(); - /// - /// Gets the audio input device identifier. - /// - /// - /// On Windows, this is a string containing the GUID of the IMMDevice for the Windows audio endpoint to use. - /// - public static string audioInId - { - get { return OVRPlugin.audioInId; } - } + return _profile; + } + } - private static bool _hasVrFocusCached = false; - private static bool _hasVrFocus = false; - private static bool _hadVrFocus = false; - /// - /// If true, the app has VR Focus. - /// - public static bool hasVrFocus - { - get { - if (!_hasVrFocusCached) - { - _hasVrFocusCached = true; - _hasVrFocus = OVRPlugin.hasVrFocus; - } - - return _hasVrFocus; - } + private IEnumerable disabledCameras; + float prevTimeScale; - private set { - _hasVrFocusCached = true; - _hasVrFocus = value; - } - } + /// + /// Occurs when an HMD attached. + /// + public static event Action HMDAcquired; - /// - /// If true, then the Oculus health and safety warning (HSW) is currently visible. - /// - [Obsolete] - public static bool isHSWDisplayed { get { return false; } } - - /// - /// If the HSW has been visible for the necessary amount of time, this will make it disappear. - /// - [Obsolete] - public static void DismissHSWDisplay() {} - - /// - /// If true, chromatic de-aberration will be applied, improving the image at the cost of texture bandwidth. - /// - public bool chromatic - { - get { - if (!isHmdPresent) - return false; + /// + /// Occurs when an HMD detached. + /// + public static event Action HMDLost; - return OVRPlugin.chromatic; - } + /// + /// Occurs when an HMD is put on the user's head. + /// + public static event Action HMDMounted; - set { - if (!isHmdPresent) - return; + /// + /// Occurs when an HMD is taken off the user's head. + /// + public static event Action HMDUnmounted; - OVRPlugin.chromatic = value; - } - } - - /// - /// If true, both eyes will see the same image, rendered from the center eye pose, saving performance. - /// - public bool monoscopic - { - get { - if (!isHmdPresent) - return true; + /// + /// Occurs when VR Focus is acquired. + /// + public static event Action VrFocusAcquired; - return OVRPlugin.monoscopic; - } - - set { - if (!isHmdPresent) - return; + /// + /// Occurs when VR Focus is lost. + /// + public static event Action VrFocusLost; - OVRPlugin.monoscopic = value; - } - } + /// + /// Occurs when the active Audio Out device has changed and a restart is needed. + /// + public static event Action AudioOutChanged; + + /// + /// Occurs when the active Audio In device has changed and a restart is needed. + /// + public static event Action AudioInChanged; + + /// + /// Occurs when the sensor gained tracking. + /// + public static event Action TrackingAcquired; + + /// + /// Occurs when the sensor lost tracking. + /// + public static event Action TrackingLost; + + /// + /// Occurs when Health & Safety Warning is dismissed. + /// + //Disable the warning about it being unused. It's deprecated. +#pragma warning disable 0067 + [Obsolete] + public static event Action HSWDismissed; +#pragma warning restore + + private static bool _isHmdPresentCached = false; + private static bool _isHmdPresent = false; + private static bool _wasHmdPresent = false; + /// + /// If true, a head-mounted display is connected and present. + /// + public static bool isHmdPresent + { + get + { + if (!_isHmdPresentCached) + { + _isHmdPresentCached = true; + _isHmdPresent = OVRPlugin.hmdPresent; + } + + return _isHmdPresent; + } + + private set + { + _isHmdPresentCached = true; + _isHmdPresent = value; + } + } + + /// + /// Gets the audio output device identifier. + /// + /// + /// On Windows, this is a string containing the GUID of the IMMDevice for the Windows audio endpoint to use. + /// + public static string audioOutId + { + get { return OVRPlugin.audioOutId; } + } + + /// + /// Gets the audio input device identifier. + /// + /// + /// On Windows, this is a string containing the GUID of the IMMDevice for the Windows audio endpoint to use. + /// + public static string audioInId + { + get { return OVRPlugin.audioInId; } + } + + private static bool _hasVrFocusCached = false; + private static bool _hasVrFocus = false; + private static bool _hadVrFocus = false; + /// + /// If true, the app has VR Focus. + /// + public static bool hasVrFocus + { + get + { + if (!_hasVrFocusCached) + { + _hasVrFocusCached = true; + _hasVrFocus = OVRPlugin.hasVrFocus; + } + + return _hasVrFocus; + } + + private set + { + _hasVrFocusCached = true; + _hasVrFocus = value; + } + } + + /// + /// If true, then the Oculus health and safety warning (HSW) is currently visible. + /// + [Obsolete] + public static bool isHSWDisplayed { get { return false; } } + + /// + /// If the HSW has been visible for the necessary amount of time, this will make it disappear. + /// + [Obsolete] + public static void DismissHSWDisplay() { } + + /// + /// If true, chromatic de-aberration will be applied, improving the image at the cost of texture bandwidth. + /// + public bool chromatic + { + get + { + if (!isHmdPresent) + return false; + + return OVRPlugin.chromatic; + } + + set + { + if (!isHmdPresent) + return; + + OVRPlugin.chromatic = value; + } + } + + /// + /// If true, both eyes will see the same image, rendered from the center eye pose, saving performance. + /// + public bool monoscopic + { + get + { + if (!isHmdPresent) + return true; + + return OVRPlugin.monoscopic; + } + + set + { + if (!isHmdPresent) + return; + + OVRPlugin.monoscopic = value; + } + } [Header("Performance/Quality")] - /// - /// If true, distortion rendering work is submitted a quarter-frame early to avoid pipeline stalls and increase CPU-GPU parallelism. - /// + /// + /// If true, distortion rendering work is submitted a quarter-frame early to avoid pipeline stalls and increase CPU-GPU parallelism. + /// [Tooltip("If true, distortion rendering work is submitted a quarter-frame early to avoid pipeline stalls and increase CPU-GPU parallelism.")] - public bool queueAhead = true; + public bool queueAhead = true; - /// - /// If true, Unity will use the optimal antialiasing level for quality/performance on the current hardware. - /// + /// + /// If true, Unity will use the optimal antialiasing level for quality/performance on the current hardware. + /// [Tooltip("If true, Unity will use the optimal antialiasing level for quality/performance on the current hardware.")] - public bool useRecommendedMSAALevel = false; + public bool useRecommendedMSAALevel = false; - /// - /// If true, dynamic resolution will be enabled - /// + /// + /// If true, dynamic resolution will be enabled + /// [Tooltip("If true, dynamic resolution will be enabled")] - public bool enableAdaptiveResolution = false; + public bool enableAdaptiveResolution = false; /// /// Min RenderScale the app can reach under adaptive resolution mode ( enableAdaptiveResolution = true ); @@ -302,256 +311,271 @@ public bool monoscopic /// The number of expected display frames per rendered frame. /// public int vsyncCount - { - get { - if (!isHmdPresent) - return 1; + { + get + { + if (!isHmdPresent) + return 1; - return OVRPlugin.vsyncCount; - } + return OVRPlugin.vsyncCount; + } - set { - if (!isHmdPresent) - return; + set + { + if (!isHmdPresent) + return; - OVRPlugin.vsyncCount = value; - } - } - - /// - /// Gets the current battery level. - /// - /// battery level in the range [0.0,1.0] - /// Battery level. - public static float batteryLevel - { - get { - if (!isHmdPresent) - return 1f; + OVRPlugin.vsyncCount = value; + } + } - return OVRPlugin.batteryLevel; - } - } - - /// - /// Gets the current battery temperature. - /// - /// battery temperature in Celsius - /// Battery temperature. - public static float batteryTemperature - { - get { - if (!isHmdPresent) - return 0f; + /// + /// Gets the current battery level. + /// + /// battery level in the range [0.0,1.0] + /// Battery level. + public static float batteryLevel + { + get + { + if (!isHmdPresent) + return 1f; - return OVRPlugin.batteryTemperature; - } - } - - /// - /// Gets the current battery status. - /// - /// battery status - /// Battery status. - public static int batteryStatus - { - get { - if (!isHmdPresent) - return -1; + return OVRPlugin.batteryLevel; + } + } - return (int)OVRPlugin.batteryStatus; - } - } + /// + /// Gets the current battery temperature. + /// + /// battery temperature in Celsius + /// Battery temperature. + public static float batteryTemperature + { + get + { + if (!isHmdPresent) + return 0f; - /// - /// Gets the current volume level. - /// - /// volume level in the range [0,1]. - public static float volumeLevel - { - get { - if (!isHmdPresent) - return 0f; + return OVRPlugin.batteryTemperature; + } + } - return OVRPlugin.systemVolume; - } - } + /// + /// Gets the current battery status. + /// + /// battery status + /// Battery status. + public static int batteryStatus + { + get + { + if (!isHmdPresent) + return -1; - /// - /// Gets or sets the current CPU performance level (0-2). Lower performance levels save more power. - /// - public static int cpuLevel - { - get { - if (!isHmdPresent) - return 2; + return (int)OVRPlugin.batteryStatus; + } + } - return OVRPlugin.cpuLevel; - } + /// + /// Gets the current volume level. + /// + /// volume level in the range [0,1]. + public static float volumeLevel + { + get + { + if (!isHmdPresent) + return 0f; - set { - if (!isHmdPresent) - return; + return OVRPlugin.systemVolume; + } + } - OVRPlugin.cpuLevel = value; - } - } + /// + /// Gets or sets the current CPU performance level (0-2). Lower performance levels save more power. + /// + public static int cpuLevel + { + get + { + if (!isHmdPresent) + return 2; - /// - /// Gets or sets the current GPU performance level (0-2). Lower performance levels save more power. - /// - public static int gpuLevel - { - get { - if (!isHmdPresent) - return 2; + return OVRPlugin.cpuLevel; + } - return OVRPlugin.gpuLevel; - } + set + { + if (!isHmdPresent) + return; - set { - if (!isHmdPresent) - return; + OVRPlugin.cpuLevel = value; + } + } - OVRPlugin.gpuLevel = value; - } - } + /// + /// Gets or sets the current GPU performance level (0-2). Lower performance levels save more power. + /// + public static int gpuLevel + { + get + { + if (!isHmdPresent) + return 2; - /// - /// If true, the CPU and GPU are currently throttled to save power and/or reduce the temperature. - /// - public static bool isPowerSavingActive - { - get { - if (!isHmdPresent) - return false; + return OVRPlugin.gpuLevel; + } - return OVRPlugin.powerSaving; - } - } + set + { + if (!isHmdPresent) + return; - /// - /// Gets or sets the eye texture format. - /// This feature is only for UNITY_5_6_OR_NEWER On PC - /// - public static EyeTextureFormat eyeTextureFormat - { - get - { - return (OVRManager.EyeTextureFormat)OVRPlugin.GetDesiredEyeTextureFormat(); - } + OVRPlugin.gpuLevel = value; + } + } - set - { - OVRPlugin.SetDesiredEyeTextureFormat((OVRPlugin.EyeTextureFormat)value); - } - } + /// + /// If true, the CPU and GPU are currently throttled to save power and/or reduce the temperature. + /// + public static bool isPowerSavingActive + { + get + { + if (!isHmdPresent) + return false; + + return OVRPlugin.powerSaving; + } + } + + /// + /// Gets or sets the eye texture format. + /// This feature is only for UNITY_5_6_OR_NEWER On PC + /// + public static EyeTextureFormat eyeTextureFormat + { + get + { + return (OVRManager.EyeTextureFormat)OVRPlugin.GetDesiredEyeTextureFormat(); + } + + set + { + OVRPlugin.SetDesiredEyeTextureFormat((OVRPlugin.EyeTextureFormat)value); + } + } [Header("Tracking")] [SerializeField] [Tooltip("Defines the current tracking origin type.")] - private OVRManager.TrackingOrigin _trackingOriginType = OVRManager.TrackingOrigin.EyeLevel; - /// - /// Defines the current tracking origin type. - /// - public OVRManager.TrackingOrigin trackingOriginType - { - get { - if (!isHmdPresent) - return _trackingOriginType; + private OVRManager.TrackingOrigin _trackingOriginType = OVRManager.TrackingOrigin.EyeLevel; + /// + /// Defines the current tracking origin type. + /// + public OVRManager.TrackingOrigin trackingOriginType + { + get + { + if (!isHmdPresent) + return _trackingOriginType; - return (OVRManager.TrackingOrigin)OVRPlugin.GetTrackingOriginType(); - } - - set { - if (!isHmdPresent) - return; - - if (OVRPlugin.SetTrackingOriginType((OVRPlugin.TrackingOrigin)value)) - { - // Keep the field exposed in the Unity Editor synchronized with any changes. - _trackingOriginType = value; - } - } - } + return (OVRManager.TrackingOrigin)OVRPlugin.GetTrackingOriginType(); + } + + set + { + if (!isHmdPresent) + return; + + if (OVRPlugin.SetTrackingOriginType((OVRPlugin.TrackingOrigin)value)) + { + // Keep the field exposed in the Unity Editor synchronized with any changes. + _trackingOriginType = value; + } + } + } - /// - /// If true, head tracking will affect the position of each OVRCameraRig's cameras. - /// + /// + /// If true, head tracking will affect the position of each OVRCameraRig's cameras. + /// [Tooltip("If true, head tracking will affect the position of each OVRCameraRig's cameras.")] - public bool usePositionTracking = true; + public bool usePositionTracking = true; - /// - /// If true, head tracking will affect the rotation of each OVRCameraRig's cameras. - /// - [HideInInspector] - public bool useRotationTracking = true; + /// + /// If true, head tracking will affect the rotation of each OVRCameraRig's cameras. + /// + [HideInInspector] + public bool useRotationTracking = true; - /// - /// If true, the distance between the user's eyes will affect the position of each OVRCameraRig's cameras. - /// + /// + /// If true, the distance between the user's eyes will affect the position of each OVRCameraRig's cameras. + /// [Tooltip("If true, the distance between the user's eyes will affect the position of each OVRCameraRig's cameras.")] - public bool useIPDInPositionTracking = true; + public bool useIPDInPositionTracking = true; - /// - /// If true, each scene load will cause the head pose to reset. - /// + /// + /// If true, each scene load will cause the head pose to reset. + /// [Tooltip("If true, each scene load will cause the head pose to reset.")] - public bool resetTrackerOnLoad = false; + public bool resetTrackerOnLoad = false; - /// - /// True if the current platform supports virtual reality. - /// + /// + /// True if the current platform supports virtual reality. + /// public bool isSupportedPlatform { get; private set; } - private static bool _isUserPresentCached = false; - private static bool _isUserPresent = false; - private static bool _wasUserPresent = false; - /// - /// True if the user is currently wearing the display. - /// - public bool isUserPresent - { - get { - if (!_isUserPresentCached) - { - _isUserPresentCached = true; - _isUserPresent = OVRPlugin.userPresent; - } - - return _isUserPresent; - } + private static bool _isUserPresentCached = false; + private static bool _isUserPresent = false; + private static bool _wasUserPresent = false; + /// + /// True if the user is currently wearing the display. + /// + public bool isUserPresent + { + get + { + if (!_isUserPresentCached) + { + _isUserPresentCached = true; + _isUserPresent = OVRPlugin.userPresent; + } - private set { - _isUserPresentCached = true; - _isUserPresent = value; - } - } + return _isUserPresent; + } + + private set + { + _isUserPresentCached = true; + _isUserPresent = value; + } + } - private static bool prevAudioOutIdIsCached = false; - private static bool prevAudioInIdIsCached = false; - private static string prevAudioOutId = string.Empty; - private static string prevAudioInId = string.Empty; - private static bool wasPositionTracked = false; + private static bool prevAudioOutIdIsCached = false; + private static bool prevAudioInIdIsCached = false; + private static string prevAudioOutId = string.Empty; + private static string prevAudioInId = string.Empty; + private static bool wasPositionTracked = false; -#region Unity Messages + #region Unity Messages - private void Awake() - { - // Only allow one instance at runtime. - if (instance != null) - { - enabled = false; - DestroyImmediate(this); - return; - } + private void Awake() + { + // Only allow one instance at runtime. + if (instance != null) + { + enabled = false; + DestroyImmediate(this); + return; + } - instance = this; + instance = this; - Debug.Log("Unity v" + Application.unityVersion + ", " + - "Oculus Utilities v" + OVRPlugin.wrapperVersion + ", " + - "OVRPlugin v" + OVRPlugin.version + ", " + - "SDK v" + OVRPlugin.nativeSDKVersion + "."); + Debug.Log("Unity v" + Application.unityVersion + ", " + + "Oculus Utilities v" + OVRPlugin.wrapperVersion + ", " + + "OVRPlugin v" + OVRPlugin.version + ", " + + "SDK v" + OVRPlugin.nativeSDKVersion + "."); #if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN var supportedTypes = @@ -586,12 +610,12 @@ private void Awake() Initialize(); - if (resetTrackerOnLoad) - display.RecenterPose(); - - // Disable the occlusion mesh by default until open issues with the preview window are resolved. - OVRPlugin.occlusionMesh = false; - } + if (resetTrackerOnLoad) + display.RecenterPose(); + + // Disable the occlusion mesh by default until open issues with the preview window are resolved. + OVRPlugin.occlusionMesh = false; + } #if UNITY_EDITOR private static bool _scriptsReloaded; @@ -603,18 +627,18 @@ static void ScriptsReloaded() } #endif - void Initialize() - { - if (display == null) - display = new OVRDisplay(); - if (tracker == null) - tracker = new OVRTracker(); - if (boundary == null) - boundary = new OVRBoundary(); - } + void Initialize() + { + if (display == null) + display = new OVRDisplay(); + if (tracker == null) + tracker = new OVRTracker(); + if (boundary == null) + boundary = new OVRBoundary(); + } - private void Update() - { + private void Update() + { #if UNITY_EDITOR if (_scriptsReloaded) { @@ -624,128 +648,128 @@ private void Update() } #endif - if (OVRPlugin.shouldQuit) - Application.Quit(); + if (OVRPlugin.shouldQuit) + Application.Quit(); - if (OVRPlugin.shouldRecenter) - OVRManager.display.RecenterPose(); + if (OVRPlugin.shouldRecenter) + OVRManager.display.RecenterPose(); - if (trackingOriginType != _trackingOriginType) - trackingOriginType = _trackingOriginType; + if (trackingOriginType != _trackingOriginType) + trackingOriginType = _trackingOriginType; - tracker.isEnabled = usePositionTracking; + tracker.isEnabled = usePositionTracking; - OVRPlugin.rotation = useRotationTracking; + OVRPlugin.rotation = useRotationTracking; - OVRPlugin.useIPDInPositionTracking = useIPDInPositionTracking; + OVRPlugin.useIPDInPositionTracking = useIPDInPositionTracking; - // Dispatch HMD events. + // Dispatch HMD events. - isHmdPresent = OVRPlugin.hmdPresent; + isHmdPresent = OVRPlugin.hmdPresent; - if (useRecommendedMSAALevel && QualitySettings.antiAliasing != display.recommendedMSAALevel) - { - Debug.Log("The current MSAA level is " + QualitySettings.antiAliasing + - ", but the recommended MSAA level is " + display.recommendedMSAALevel + - ". Switching to the recommended level."); + if (useRecommendedMSAALevel && QualitySettings.antiAliasing != display.recommendedMSAALevel) + { + Debug.Log("The current MSAA level is " + QualitySettings.antiAliasing + + ", but the recommended MSAA level is " + display.recommendedMSAALevel + + ". Switching to the recommended level."); - QualitySettings.antiAliasing = display.recommendedMSAALevel; - } + QualitySettings.antiAliasing = display.recommendedMSAALevel; + } - if (_wasHmdPresent && !isHmdPresent) - { - try - { - if (HMDLost != null) - HMDLost(); - } - catch (Exception e) - { - Debug.LogError("Caught Exception: " + e); - } - } + if (_wasHmdPresent && !isHmdPresent) + { + try + { + if (HMDLost != null) + HMDLost(); + } + catch (Exception e) + { + Debug.LogError("Caught Exception: " + e); + } + } if (!_wasHmdPresent && isHmdPresent) - { - try - { - if (HMDAcquired != null) - HMDAcquired(); - } - catch (Exception e) - { - Debug.LogError("Caught Exception: " + e); - } - } + { + try + { + if (HMDAcquired != null) + HMDAcquired(); + } + catch (Exception e) + { + Debug.LogError("Caught Exception: " + e); + } + } - _wasHmdPresent = isHmdPresent; + _wasHmdPresent = isHmdPresent; - // Dispatch HMD mounted events. + // Dispatch HMD mounted events. - isUserPresent = OVRPlugin.userPresent; + isUserPresent = OVRPlugin.userPresent; - if (_wasUserPresent && !isUserPresent) - { - try - { - if (HMDUnmounted != null) - HMDUnmounted(); - } - catch (Exception e) - { - Debug.LogError("Caught Exception: " + e); - } - } + if (_wasUserPresent && !isUserPresent) + { + try + { + if (HMDUnmounted != null) + HMDUnmounted(); + } + catch (Exception e) + { + Debug.LogError("Caught Exception: " + e); + } + } - if (!_wasUserPresent && isUserPresent) - { - try - { - if (HMDMounted != null) - HMDMounted(); - } - catch (Exception e) - { - Debug.LogError("Caught Exception: " + e); - } - } + if (!_wasUserPresent && isUserPresent) + { + try + { + if (HMDMounted != null) + HMDMounted(); + } + catch (Exception e) + { + Debug.LogError("Caught Exception: " + e); + } + } - _wasUserPresent = isUserPresent; + _wasUserPresent = isUserPresent; - // Dispatch VR Focus events. + // Dispatch VR Focus events. - hasVrFocus = OVRPlugin.hasVrFocus; + hasVrFocus = OVRPlugin.hasVrFocus; - if (_hadVrFocus && !hasVrFocus) - { - try - { - if (VrFocusLost != null) - VrFocusLost(); - } - catch (Exception e) - { - Debug.LogError("Caught Exception: " + e); - } - } + if (_hadVrFocus && !hasVrFocus) + { + try + { + if (VrFocusLost != null) + VrFocusLost(); + } + catch (Exception e) + { + Debug.LogError("Caught Exception: " + e); + } + } if (!_hadVrFocus && hasVrFocus) - { - try - { - if (VrFocusAcquired != null) - VrFocusAcquired(); - } - catch (Exception e) - { - Debug.LogError("Caught Exception: " + e); - } - } + { + try + { + if (VrFocusAcquired != null) + VrFocusAcquired(); + } + catch (Exception e) + { + Debug.LogError("Caught Exception: " + e); + } + } - _hadVrFocus = hasVrFocus; + _hadVrFocus = hasVrFocus; - // Changing effective rendering resolution dynamically according performance + // Changing effective rendering resolution dynamically according performance #if (UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN) && UNITY_5_4_OR_NEWER if (enableAdaptiveResolution) @@ -768,118 +792,118 @@ private void Update() } #endif - // Dispatch Audio Device events. + // Dispatch Audio Device events. - string audioOutId = OVRPlugin.audioOutId; - if (!prevAudioOutIdIsCached) - { - prevAudioOutId = audioOutId; - prevAudioOutIdIsCached = true; - } - else if (audioOutId != prevAudioOutId) - { - try - { - if (AudioOutChanged != null) - AudioOutChanged(); - } - catch (Exception e) - { - Debug.LogError("Caught Exception: " + e); - } - - prevAudioOutId = audioOutId; - } + string audioOutId = OVRPlugin.audioOutId; + if (!prevAudioOutIdIsCached) + { + prevAudioOutId = audioOutId; + prevAudioOutIdIsCached = true; + } + else if (audioOutId != prevAudioOutId) + { + try + { + if (AudioOutChanged != null) + AudioOutChanged(); + } + catch (Exception e) + { + Debug.LogError("Caught Exception: " + e); + } - string audioInId = OVRPlugin.audioInId; - if (!prevAudioInIdIsCached) - { - prevAudioInId = audioInId; - prevAudioInIdIsCached = true; - } - else if (audioInId != prevAudioInId) - { - try - { - if (AudioInChanged != null) - AudioInChanged(); - } - catch (Exception e) - { - Debug.LogError("Caught Exception: " + e); - } - - prevAudioInId = audioInId; - } + prevAudioOutId = audioOutId; + } + + string audioInId = OVRPlugin.audioInId; + if (!prevAudioInIdIsCached) + { + prevAudioInId = audioInId; + prevAudioInIdIsCached = true; + } + else if (audioInId != prevAudioInId) + { + try + { + if (AudioInChanged != null) + AudioInChanged(); + } + catch (Exception e) + { + Debug.LogError("Caught Exception: " + e); + } - // Dispatch tracking events. + prevAudioInId = audioInId; + } - if (wasPositionTracked && !tracker.isPositionTracked) - { - try - { - if (TrackingLost != null) - TrackingLost(); - } - catch (Exception e) - { - Debug.LogError("Caught Exception: " + e); - } - } + // Dispatch tracking events. - if (!wasPositionTracked && tracker.isPositionTracked) - { - try - { - if (TrackingAcquired != null) - TrackingAcquired(); - } - catch (Exception e) - { - Debug.LogError("Caught Exception: " + e); - } - } + if (wasPositionTracked && !tracker.isPositionTracked) + { + try + { + if (TrackingLost != null) + TrackingLost(); + } + catch (Exception e) + { + Debug.LogError("Caught Exception: " + e); + } + } + + if (!wasPositionTracked && tracker.isPositionTracked) + { + try + { + if (TrackingAcquired != null) + TrackingAcquired(); + } + catch (Exception e) + { + Debug.LogError("Caught Exception: " + e); + } + } - wasPositionTracked = tracker.isPositionTracked; + wasPositionTracked = tracker.isPositionTracked; - display.Update(); - OVRInput.Update(); + display.Update(); + OVRInput.Update(); } - private void LateUpdate() - { - OVRHaptics.Process(); - } + private void LateUpdate() + { + OVRHaptics.Process(); + } - private void FixedUpdate() - { - OVRInput.FixedUpdate(); - } + private void FixedUpdate() + { + OVRInput.FixedUpdate(); + } - /// - /// Leaves the application/game and returns to the launcher/dashboard - /// - public void ReturnToLauncher() - { - // show the platform UI quit prompt - OVRManager.PlatformUIConfirmQuit(); - } + /// + /// Leaves the application/game and returns to the launcher/dashboard + /// + public void ReturnToLauncher() + { + // show the platform UI quit prompt + OVRManager.PlatformUIConfirmQuit(); + } -#endregion + #endregion public static void PlatformUIConfirmQuit() - { - if (!isHmdPresent) - return; + { + if (!isHmdPresent) + return; - OVRPlugin.ShowUI(OVRPlugin.PlatformUI.ConfirmQuit); + OVRPlugin.ShowUI(OVRPlugin.PlatformUI.ConfirmQuit); } public static void PlatformUIGlobalMenu() - { - if (!isHmdPresent) - return; + { + if (!isHmdPresent) + return; - OVRPlugin.ShowUI(OVRPlugin.PlatformUI.GlobalMenu); + OVRPlugin.ShowUI(OVRPlugin.PlatformUI.GlobalMenu); } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/OVROverlay.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/OVROverlay.cs index 0607843d..c3c037b5 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/OVROverlay.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/OVROverlay.cs @@ -57,197 +57,197 @@ limitations under the License. public class OVROverlay : MonoBehaviour { - public enum OverlayShape - { - Quad = 0, // Display overlay as a quad - Cylinder = 1, // [Mobile Only][Experimental] Display overlay as a cylinder, Translation only works correctly with vrDriver 1.04 or above - Cubemap = 2, // Display overlay as a cube map - OffcenterCubemap = 4, // Display overlay as a cube map with a center offset - } - - public enum OverlayType - { - None, // Disabled the overlay - Underlay, // Eye buffers blend on top - Overlay, // Blends on top of the eye buffer - OverlayShowLod // (Deprecated) Blends on top and colorizes texture level of detail - }; + public enum OverlayShape + { + Quad = 0, // Display overlay as a quad + Cylinder = 1, // [Mobile Only][Experimental] Display overlay as a cylinder, Translation only works correctly with vrDriver 1.04 or above + Cubemap = 2, // Display overlay as a cube map + OffcenterCubemap = 4, // Display overlay as a cube map with a center offset + } + + public enum OverlayType + { + None, // Disabled the overlay + Underlay, // Eye buffers blend on top + Overlay, // Blends on top of the eye buffer + OverlayShowLod // (Deprecated) Blends on top and colorizes texture level of detail + }; #if UNITY_ANDROID && !UNITY_EDITOR const int maxInstances = 3; #else - const int maxInstances = 15; + const int maxInstances = 15; #endif - internal static OVROverlay[] instances = new OVROverlay[maxInstances]; - - /// - /// Specify overlay's type - /// - public OverlayType currentOverlayType = OverlayType.Overlay; - - /// - /// Specify overlay's shape - /// - public OverlayShape currentOverlayShape = OverlayShape.Quad; - private OverlayShape _prevOverlayShape = OverlayShape.Quad; - - /// - /// Try to avoid setting texture frequently when app is running, texNativePtr updating is slow since rendering thread synchronization - /// Please cache your nativeTexturePtr and use OverrideOverlayTextureInfo - /// - public Texture[] textures = new Texture[] { null, null }; - private Texture[] cachedTextures = new Texture[] { null, null }; - private IntPtr[] texNativePtrs = new IntPtr[] { IntPtr.Zero, IntPtr.Zero }; - - private int layerIndex = -1; - Renderer rend; - - /// - /// Use this function to set texture and texNativePtr when app is running - /// GetNativeTexturePtr is a slow behavior, the value should be pre-cached - /// - public void OverrideOverlayTextureInfo(Texture srcTexture, IntPtr nativePtr, UnityEngine.XR.XRNode node) - { - int index = (node == UnityEngine.XR.XRNode.RightEye) ? 1 : 0; - - textures[index] = srcTexture; - cachedTextures[index] = srcTexture; - texNativePtrs[index] = nativePtr; - } - - void Awake() - { - Debug.Log("Overlay Awake"); - rend = GetComponent(); - for (int i = 0; i < 2; ++i) - { - // Backward compatibility - if (rend != null && textures[i] == null) - textures[i] = rend.material.mainTexture; - - if (textures[i] != null) - { - cachedTextures[i] = textures[i]; - texNativePtrs[i] = textures[i].GetNativeTexturePtr(); - } - } - } - - void OnEnable() - { - if (!OVRManager.isHmdPresent) - { - enabled = false; - return; - } - - OnDisable(); - - for (int i = 0; i < maxInstances; ++i) - { - if (instances[i] == null || instances[i] == this) - { - layerIndex = i; - instances[i] = this; - break; - } - } - } - - void OnDisable() - { - if (layerIndex != -1) - { - // Turn off the overlay if it was on. - OVRPlugin.SetOverlayQuad(true, false, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, OVRPose.identity.ToPosef(), Vector3.one.ToVector3f(), layerIndex, (OVRPlugin.OverlayShape)_prevOverlayShape); - instances[layerIndex] = null; - } - layerIndex = -1; - } - - void OnRenderObject() - { - // The overlay must be specified every eye frame, because it is positioned relative to the - // current head location. If frames are dropped, it will be time warped appropriately, - // just like the eye buffers. - if (!Camera.current.CompareTag("MainCamera") || Camera.current.cameraType != CameraType.Game || layerIndex == -1 || currentOverlayType == OverlayType.None) - return; + internal static OVROverlay[] instances = new OVROverlay[maxInstances]; + + /// + /// Specify overlay's type + /// + public OverlayType currentOverlayType = OverlayType.Overlay; + + /// + /// Specify overlay's shape + /// + public OverlayShape currentOverlayShape = OverlayShape.Quad; + private OverlayShape _prevOverlayShape = OverlayShape.Quad; + + /// + /// Try to avoid setting texture frequently when app is running, texNativePtr updating is slow since rendering thread synchronization + /// Please cache your nativeTexturePtr and use OverrideOverlayTextureInfo + /// + public Texture[] textures = new Texture[] { null, null }; + private Texture[] cachedTextures = new Texture[] { null, null }; + private IntPtr[] texNativePtrs = new IntPtr[] { IntPtr.Zero, IntPtr.Zero }; + + private int layerIndex = -1; + Renderer rend; + + /// + /// Use this function to set texture and texNativePtr when app is running + /// GetNativeTexturePtr is a slow behavior, the value should be pre-cached + /// + public void OverrideOverlayTextureInfo(Texture srcTexture, IntPtr nativePtr, UnityEngine.XR.XRNode node) + { + int index = (node == UnityEngine.XR.XRNode.RightEye) ? 1 : 0; + + textures[index] = srcTexture; + cachedTextures[index] = srcTexture; + texNativePtrs[index] = nativePtr; + } + + void Awake() + { + Debug.Log("Overlay Awake"); + rend = GetComponent(); + for (int i = 0; i < 2; ++i) + { + // Backward compatibility + if (rend != null && textures[i] == null) + textures[i] = rend.material.mainTexture; + + if (textures[i] != null) + { + cachedTextures[i] = textures[i]; + texNativePtrs[i] = textures[i].GetNativeTexturePtr(); + } + } + } + + void OnEnable() + { + if (!OVRManager.isHmdPresent) + { + enabled = false; + return; + } + + OnDisable(); + + for (int i = 0; i < maxInstances; ++i) + { + if (instances[i] == null || instances[i] == this) + { + layerIndex = i; + instances[i] = this; + break; + } + } + } + + void OnDisable() + { + if (layerIndex != -1) + { + // Turn off the overlay if it was on. + OVRPlugin.SetOverlayQuad(true, false, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, OVRPose.identity.ToPosef(), Vector3.one.ToVector3f(), layerIndex, (OVRPlugin.OverlayShape)_prevOverlayShape); + instances[layerIndex] = null; + } + layerIndex = -1; + } + + void OnRenderObject() + { + // The overlay must be specified every eye frame, because it is positioned relative to the + // current head location. If frames are dropped, it will be time warped appropriately, + // just like the eye buffers. + if (!Camera.current.CompareTag("MainCamera") || Camera.current.cameraType != CameraType.Game || layerIndex == -1 || currentOverlayType == OverlayType.None) + return; #if !UNITY_ANDROID || UNITY_EDITOR - if (currentOverlayShape == OverlayShape.Cylinder || currentOverlayShape == OverlayShape.OffcenterCubemap) - { - Debug.LogWarning("Overlay shape " + currentOverlayShape + " is not supported on current platform"); - } + if (currentOverlayShape == OverlayShape.Cylinder || currentOverlayShape == OverlayShape.OffcenterCubemap) + { + Debug.LogWarning("Overlay shape " + currentOverlayShape + " is not supported on current platform"); + } #endif - for (int i = 0; i < 2; ++i) - { - if (i >= textures.Length) - continue; - - if (textures[i] != cachedTextures[i]) - { - cachedTextures[i] = textures[i]; - if (cachedTextures[i] != null) - texNativePtrs[i] = cachedTextures[i].GetNativeTexturePtr(); - } - - if (currentOverlayShape == OverlayShape.Cubemap) - { - if (textures[i] != null && textures[i].GetType() != typeof(Cubemap)) - { - Debug.LogError("Need Cubemap texture for cube map overlay"); - return; - } - } - } - - if (cachedTextures[0] == null || texNativePtrs[0] == IntPtr.Zero) - return; - - bool overlay = (currentOverlayType == OverlayType.Overlay); - bool headLocked = false; - for (var t = transform; t != null && !headLocked; t = t.parent) - headLocked |= (t == Camera.current.transform); - - OVRPose pose = (headLocked) ? transform.ToHeadSpacePose() : transform.ToTrackingSpacePose(); - Vector3 scale = transform.lossyScale; - for (int i = 0; i < 3; ++i) - scale[i] /= Camera.current.transform.lossyScale[i]; + for (int i = 0; i < 2; ++i) + { + if (i >= textures.Length) + continue; + + if (textures[i] != cachedTextures[i]) + { + cachedTextures[i] = textures[i]; + if (cachedTextures[i] != null) + texNativePtrs[i] = cachedTextures[i].GetNativeTexturePtr(); + } + + if (currentOverlayShape == OverlayShape.Cubemap) + { + if (textures[i] != null && textures[i].GetType() != typeof(Cubemap)) + { + Debug.LogError("Need Cubemap texture for cube map overlay"); + return; + } + } + } + + if (cachedTextures[0] == null || texNativePtrs[0] == IntPtr.Zero) + return; + + bool overlay = (currentOverlayType == OverlayType.Overlay); + bool headLocked = false; + for (var t = transform; t != null && !headLocked; t = t.parent) + headLocked |= (t == Camera.current.transform); + + OVRPose pose = (headLocked) ? transform.ToHeadSpacePose() : transform.ToTrackingSpacePose(); + Vector3 scale = transform.lossyScale; + for (int i = 0; i < 3; ++i) + scale[i] /= Camera.current.transform.lossyScale[i]; #if !UNITY_ANDROID - if (currentOverlayShape == OverlayShape.Cubemap) - { - pose.position = Camera.current.transform.position; - } + if (currentOverlayShape == OverlayShape.Cubemap) + { + pose.position = Camera.current.transform.position; + } #endif - // Pack the offsetCenter directly into pose.position for offcenterCubemap - if (currentOverlayShape == OverlayShape.OffcenterCubemap) - { - pose.position = transform.position; - - if ( pose.position.magnitude > 1.0f ) - { - Debug.LogWarning("your cube map center offset's magnitude is greater than 1, which will cause some cube map pixel always invisible ."); - } - } - - // Cylinder overlay sanity checking - if (currentOverlayShape == OverlayShape.Cylinder) - { - float arcAngle = scale.x / scale.z / (float)Math.PI * 180.0f; - if (arcAngle > 180.0f) - { - Debug.LogError("Cylinder overlay's arc angle has to be below 180 degree, current arc angle is " + arcAngle + " degree." ); - return ; - } - } - - bool isOverlayVisible = OVRPlugin.SetOverlayQuad(overlay, headLocked, texNativePtrs[0], texNativePtrs[1], IntPtr.Zero, pose.flipZ().ToPosef(), scale.ToVector3f(), layerIndex, (OVRPlugin.OverlayShape)currentOverlayShape); - _prevOverlayShape = currentOverlayShape; - if (rend) - rend.enabled = !isOverlayVisible; - } + // Pack the offsetCenter directly into pose.position for offcenterCubemap + if (currentOverlayShape == OverlayShape.OffcenterCubemap) + { + pose.position = transform.position; + + if (pose.position.magnitude > 1.0f) + { + Debug.LogWarning("your cube map center offset's magnitude is greater than 1, which will cause some cube map pixel always invisible ."); + } + } + + // Cylinder overlay sanity checking + if (currentOverlayShape == OverlayShape.Cylinder) + { + float arcAngle = scale.x / scale.z / (float)Math.PI * 180.0f; + if (arcAngle > 180.0f) + { + Debug.LogError("Cylinder overlay's arc angle has to be below 180 degree, current arc angle is " + arcAngle + " degree."); + return; + } + } + + bool isOverlayVisible = OVRPlugin.SetOverlayQuad(overlay, headLocked, texNativePtrs[0], texNativePtrs[1], IntPtr.Zero, pose.flipZ().ToPosef(), scale.ToVector3f(), layerIndex, (OVRPlugin.OverlayShape)currentOverlayShape); + _prevOverlayShape = currentOverlayShape; + if (rend) + rend.enabled = !isOverlayVisible; + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRPlatformMenu.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRPlatformMenu.cs index 4dc206c9..623d9760 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRPlatformMenu.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRPlatformMenu.cs @@ -29,184 +29,184 @@ limitations under the License. /// public class OVRPlatformMenu : MonoBehaviour { - /// - /// The key code. - /// - public KeyCode keyCode = KeyCode.Escape; - - public enum eHandler - { - ShowConfirmQuit, - RetreatOneLevel, - }; - - public eHandler shortPressHandler = eHandler.ShowConfirmQuit; - - /// - /// Callback to handle short press. Returns true if ConfirmQuit menu should be shown. - /// - public System.Func OnShortPress; - private static Stack sceneStack = new Stack(); - - private float doubleTapDelay = 0.25f; - private float shortPressDelay = 0.25f; - private float longPressDelay = 0.75f; - - enum eBackButtonAction - { - NONE, - DOUBLE_TAP, - SHORT_PRESS - }; - - private int downCount = 0; - private int upCount = 0; - private float initialDownTime = -1.0f; - - eBackButtonAction ResetAndSendAction( eBackButtonAction action ) - { - print( "ResetAndSendAction( " + action + " );" ); - downCount = 0; - upCount = 0; - initialDownTime = -1.0f; - return action; - } - - eBackButtonAction HandleBackButtonState() - { - if ( Input.GetKeyDown( keyCode ) ) - { - // just came down - downCount++; - if ( downCount == 1 ) - { - initialDownTime = Time.realtimeSinceStartup; - } - } - else if ( downCount > 0 ) - { - if ( Input.GetKey( keyCode ) ) - { - if ( downCount <= upCount ) - { - // just went down - downCount++; - } - - float timeSinceFirstDown = Time.realtimeSinceStartup - initialDownTime; - if ( timeSinceFirstDown > longPressDelay ) - { - return ResetAndSendAction( eBackButtonAction.NONE ); - } - } - else - { - bool started = initialDownTime >= 0.0f; - if ( started ) - { - if ( upCount < downCount ) - { - // just came up - upCount++; - } - - float timeSinceFirstDown = Time.realtimeSinceStartup - initialDownTime; - if (timeSinceFirstDown < doubleTapDelay) - { - if (downCount == 2 && upCount == 2) - { - return ResetAndSendAction(eBackButtonAction.DOUBLE_TAP); - } - } - else if (timeSinceFirstDown > shortPressDelay && timeSinceFirstDown < longPressDelay) - { - if (downCount == 1 && upCount == 1) - { - return ResetAndSendAction(eBackButtonAction.SHORT_PRESS); - } - } - else if (timeSinceFirstDown > longPressDelay) - { - return ResetAndSendAction(eBackButtonAction.NONE); - } - } - } - } - - // down reset, but perform no action - return eBackButtonAction.NONE; - } - - /// - /// Instantiate the cursor timer - /// - void Awake() - { - if (shortPressHandler == eHandler.RetreatOneLevel && OnShortPress == null) - OnShortPress = RetreatOneLevel; - - if (!OVRManager.isHmdPresent) - { - enabled = false; - return; - } - - sceneStack.Push(UnityEngine.SceneManagement.SceneManager.GetActiveScene().name); - } - - /// - /// Reset when resuming - /// - void OnApplicationFocus( bool focusState ) - { - //Input.ResetInputAxes(); - //ResetAndSendAction( eBackButtonAction.NONE ); - } - - /// - /// Reset when resuming - /// - void OnApplicationPause( bool pauseStatus ) - { - if ( !pauseStatus ) - { - Input.ResetInputAxes(); - } - //ResetAndSendAction( eBackButtonAction.NONE ); - } - - /// - /// Show the confirm quit menu - /// - void ShowConfirmQuitMenu() - { + /// + /// The key code. + /// + public KeyCode keyCode = KeyCode.Escape; + + public enum eHandler + { + ShowConfirmQuit, + RetreatOneLevel, + }; + + public eHandler shortPressHandler = eHandler.ShowConfirmQuit; + + /// + /// Callback to handle short press. Returns true if ConfirmQuit menu should be shown. + /// + public System.Func OnShortPress; + private static Stack sceneStack = new Stack(); + + private float doubleTapDelay = 0.25f; + private float shortPressDelay = 0.25f; + private float longPressDelay = 0.75f; + + enum eBackButtonAction + { + NONE, + DOUBLE_TAP, + SHORT_PRESS + }; + + private int downCount = 0; + private int upCount = 0; + private float initialDownTime = -1.0f; + + eBackButtonAction ResetAndSendAction(eBackButtonAction action) + { + print("ResetAndSendAction( " + action + " );"); + downCount = 0; + upCount = 0; + initialDownTime = -1.0f; + return action; + } + + eBackButtonAction HandleBackButtonState() + { + if (Input.GetKeyDown(keyCode)) + { + // just came down + downCount++; + if (downCount == 1) + { + initialDownTime = Time.realtimeSinceStartup; + } + } + else if (downCount > 0) + { + if (Input.GetKey(keyCode)) + { + if (downCount <= upCount) + { + // just went down + downCount++; + } + + float timeSinceFirstDown = Time.realtimeSinceStartup - initialDownTime; + if (timeSinceFirstDown > longPressDelay) + { + return ResetAndSendAction(eBackButtonAction.NONE); + } + } + else + { + bool started = initialDownTime >= 0.0f; + if (started) + { + if (upCount < downCount) + { + // just came up + upCount++; + } + + float timeSinceFirstDown = Time.realtimeSinceStartup - initialDownTime; + if (timeSinceFirstDown < doubleTapDelay) + { + if (downCount == 2 && upCount == 2) + { + return ResetAndSendAction(eBackButtonAction.DOUBLE_TAP); + } + } + else if (timeSinceFirstDown > shortPressDelay && timeSinceFirstDown < longPressDelay) + { + if (downCount == 1 && upCount == 1) + { + return ResetAndSendAction(eBackButtonAction.SHORT_PRESS); + } + } + else if (timeSinceFirstDown > longPressDelay) + { + return ResetAndSendAction(eBackButtonAction.NONE); + } + } + } + } + + // down reset, but perform no action + return eBackButtonAction.NONE; + } + + /// + /// Instantiate the cursor timer + /// + void Awake() + { + if (shortPressHandler == eHandler.RetreatOneLevel && OnShortPress == null) + OnShortPress = RetreatOneLevel; + + if (!OVRManager.isHmdPresent) + { + enabled = false; + return; + } + + sceneStack.Push(UnityEngine.SceneManagement.SceneManager.GetActiveScene().name); + } + + /// + /// Reset when resuming + /// + void OnApplicationFocus(bool focusState) + { + //Input.ResetInputAxes(); + //ResetAndSendAction( eBackButtonAction.NONE ); + } + + /// + /// Reset when resuming + /// + void OnApplicationPause(bool pauseStatus) + { + if (!pauseStatus) + { + Input.ResetInputAxes(); + } + //ResetAndSendAction( eBackButtonAction.NONE ); + } + + /// + /// Show the confirm quit menu + /// + void ShowConfirmQuitMenu() + { #if UNITY_ANDROID && !UNITY_EDITOR Debug.Log("[PlatformUI-ConfirmQuit] Showing @ " + Time.time); OVRManager.PlatformUIConfirmQuit(); #endif - } - - /// - /// Sample handler for short press which retreats to the previous scene that used OVRPlatformMenu. - /// - private static bool RetreatOneLevel() - { - if (sceneStack.Count > 1) - { - string parentScene = sceneStack.Pop(); - UnityEngine.SceneManagement.SceneManager.LoadSceneAsync (parentScene); - return false; - } - - return true; - } - - /// - /// Tests for long-press and activates global platform menu when detected. - /// as per the Unity integration doc, the back button responds to "mouse 1" button down/up/etc - /// - void Update() - { + } + + /// + /// Sample handler for short press which retreats to the previous scene that used OVRPlatformMenu. + /// + private static bool RetreatOneLevel() + { + if (sceneStack.Count > 1) + { + string parentScene = sceneStack.Pop(); + UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(parentScene); + return false; + } + + return true; + } + + /// + /// Tests for long-press and activates global platform menu when detected. + /// as per the Unity integration doc, the back button responds to "mouse 1" button down/up/etc + /// + void Update() + { #if UNITY_ANDROID eBackButtonAction action = HandleBackButtonState(); if (action == eBackButtonAction.SHORT_PRESS) @@ -215,5 +215,5 @@ void Update() ShowConfirmQuitMenu(); } #endif - } + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRPlugin.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRPlugin.cs index ce9ca10e..b4757de2 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRPlugin.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRPlugin.cs @@ -26,298 +26,300 @@ limitations under the License. internal static class OVRPlugin { - public static readonly System.Version wrapperVersion = OVRP_1_14_0.version; - - private static System.Version _version; - public static System.Version version - { - get { - if (_version == null) - { - try - { - string pluginVersion = OVRP_1_1_0.ovrp_GetVersion(); - - if (pluginVersion != null) - { - // Truncate unsupported trailing version info for System.Version. Original string is returned if not present. - pluginVersion = pluginVersion.Split('-')[0]; - _version = new System.Version(pluginVersion); - } - else - { - _version = _versionZero; - } - } - catch - { - _version = _versionZero; - } - - // Unity 5.1.1f3-p3 have OVRPlugin version "0.5.0", which isn't accurate. - if (_version == OVRP_0_5_0.version) - _version = OVRP_0_1_0.version; - - if (_version > _versionZero && _version < OVRP_1_3_0.version) - throw new PlatformNotSupportedException("Oculus Utilities version " + wrapperVersion + " is too new for OVRPlugin version " + _version.ToString () + ". Update to the latest version of Unity."); - } - - return _version; - } - } - - private static System.Version _nativeSDKVersion; - public static System.Version nativeSDKVersion - { - get { - if (_nativeSDKVersion == null) - { - try - { - string sdkVersion = string.Empty; - - if (version >= OVRP_1_1_0.version) - sdkVersion = OVRP_1_1_0.ovrp_GetNativeSDKVersion(); - else - sdkVersion = _versionZero.ToString(); - - if (sdkVersion != null) - { - // Truncate unsupported trailing version info for System.Version. Original string is returned if not present. - sdkVersion = sdkVersion.Split('-')[0]; - _nativeSDKVersion = new System.Version(sdkVersion); - } - else - { - _nativeSDKVersion = _versionZero; - } - } - catch - { - _nativeSDKVersion = _versionZero; - } - } - - return _nativeSDKVersion; - } - } - - [StructLayout(LayoutKind.Sequential)] - private class GUID - { - public int a; - public short b; - public short c; - public byte d0; - public byte d1; - public byte d2; - public byte d3; - public byte d4; - public byte d5; - public byte d6; - public byte d7; - } - - public enum Bool - { - False = 0, - True - } - - public enum Eye - { - None = -1, - Left = 0, - Right = 1, - Count = 2 - } - - public enum Tracker - { - None = -1, - Zero = 0, - One = 1, - Two = 2, - Three = 3, - Count, - } - - public enum Node - { - None = -1, - EyeLeft = 0, - EyeRight = 1, - EyeCenter = 2, - HandLeft = 3, - HandRight = 4, - TrackerZero = 5, - TrackerOne = 6, - TrackerTwo = 7, - TrackerThree = 8, - Head = 9, - Count, - } - - public enum Controller - { - None = 0, - LTouch = 0x00000001, - RTouch = 0x00000002, - Touch = LTouch | RTouch, - Remote = 0x00000004, - Gamepad = 0x00000010, - Touchpad = 0x08000000, - LTrackedRemote = 0x01000000, - RTrackedRemote = 0x02000000, - Active = unchecked((int)0x80000000), - All = ~None, - } - - public enum TrackingOrigin - { - EyeLevel = 0, - FloorLevel = 1, - Count, - } - - public enum RecenterFlags - { - Default = 0, - Controllers = 0x40000000, - IgnoreAll = unchecked((int)0x80000000), - Count, - } - - public enum BatteryStatus - { - Charging = 0, - Discharging, - Full, - NotCharging, - Unknown, - } - - public enum EyeTextureFormat - { - Default = 0, - R16G16B16A16_FP = 2, - R11G11B10_FP = 3, - } - - public enum PlatformUI - { - None = -1, - GlobalMenu = 0, - ConfirmQuit, + public static readonly System.Version wrapperVersion = OVRP_1_14_0.version; + + private static System.Version _version; + public static System.Version version + { + get + { + if (_version == null) + { + try + { + string pluginVersion = OVRP_1_1_0.ovrp_GetVersion(); + + if (pluginVersion != null) + { + // Truncate unsupported trailing version info for System.Version. Original string is returned if not present. + pluginVersion = pluginVersion.Split('-')[0]; + _version = new System.Version(pluginVersion); + } + else + { + _version = _versionZero; + } + } + catch + { + _version = _versionZero; + } + + // Unity 5.1.1f3-p3 have OVRPlugin version "0.5.0", which isn't accurate. + if (_version == OVRP_0_5_0.version) + _version = OVRP_0_1_0.version; + + if (_version > _versionZero && _version < OVRP_1_3_0.version) + throw new PlatformNotSupportedException("Oculus Utilities version " + wrapperVersion + " is too new for OVRPlugin version " + _version.ToString() + ". Update to the latest version of Unity."); + } + + return _version; + } + } + + private static System.Version _nativeSDKVersion; + public static System.Version nativeSDKVersion + { + get + { + if (_nativeSDKVersion == null) + { + try + { + string sdkVersion = string.Empty; + + if (version >= OVRP_1_1_0.version) + sdkVersion = OVRP_1_1_0.ovrp_GetNativeSDKVersion(); + else + sdkVersion = _versionZero.ToString(); + + if (sdkVersion != null) + { + // Truncate unsupported trailing version info for System.Version. Original string is returned if not present. + sdkVersion = sdkVersion.Split('-')[0]; + _nativeSDKVersion = new System.Version(sdkVersion); + } + else + { + _nativeSDKVersion = _versionZero; + } + } + catch + { + _nativeSDKVersion = _versionZero; + } + } + + return _nativeSDKVersion; + } + } + + [StructLayout(LayoutKind.Sequential)] + private class GUID + { + public int a; + public short b; + public short c; + public byte d0; + public byte d1; + public byte d2; + public byte d3; + public byte d4; + public byte d5; + public byte d6; + public byte d7; + } + + public enum Bool + { + False = 0, + True + } + + public enum Eye + { + None = -1, + Left = 0, + Right = 1, + Count = 2 + } + + public enum Tracker + { + None = -1, + Zero = 0, + One = 1, + Two = 2, + Three = 3, + Count, + } + + public enum Node + { + None = -1, + EyeLeft = 0, + EyeRight = 1, + EyeCenter = 2, + HandLeft = 3, + HandRight = 4, + TrackerZero = 5, + TrackerOne = 6, + TrackerTwo = 7, + TrackerThree = 8, + Head = 9, + Count, + } + + public enum Controller + { + None = 0, + LTouch = 0x00000001, + RTouch = 0x00000002, + Touch = LTouch | RTouch, + Remote = 0x00000004, + Gamepad = 0x00000010, + Touchpad = 0x08000000, + LTrackedRemote = 0x01000000, + RTrackedRemote = 0x02000000, + Active = unchecked((int)0x80000000), + All = ~None, + } + + public enum TrackingOrigin + { + EyeLevel = 0, + FloorLevel = 1, + Count, + } + + public enum RecenterFlags + { + Default = 0, + Controllers = 0x40000000, + IgnoreAll = unchecked((int)0x80000000), + Count, + } + + public enum BatteryStatus + { + Charging = 0, + Discharging, + Full, + NotCharging, + Unknown, + } + + public enum EyeTextureFormat + { + Default = 0, + R16G16B16A16_FP = 2, + R11G11B10_FP = 3, + } + + public enum PlatformUI + { + None = -1, + GlobalMenu = 0, + ConfirmQuit, GlobalMenuTutorial, - } - - public enum SystemRegion - { - Unspecified = 0, - Japan, - China, - } - - public enum SystemHeadset - { - None = 0, - GearVR_R320, // Note4 Innovator - GearVR_R321, // S6 Innovator - GearVR_R322, // Commercial 1 - GearVR_R323, // Commercial 2 (USB Type C) - - Rift_DK1 = 0x1000, - Rift_DK2, - Rift_CV1, - } - - public enum OverlayShape - { - Quad = 0, - Cylinder = 1, - Cubemap = 2, - OffcenterCubemap = 4, - } - - public enum Step - { - Render = -1, - Physics = 0, - } - - private const int OverlayShapeFlagShift = 4; - private enum OverlayFlag - { - None = unchecked((int)0x00000000), - OnTop = unchecked((int)0x00000001), - HeadLocked = unchecked((int)0x00000002), - - // Using the 5-8 bits for shapes, total 16 potential shapes can be supported 0x000000[0]0 -> 0x000000[F]0 - ShapeFlag_Quad = unchecked((int)OverlayShape.Quad << OverlayShapeFlagShift), - ShapeFlag_Cylinder = unchecked((int)OverlayShape.Cylinder << OverlayShapeFlagShift), - ShapeFlag_Cubemap = unchecked((int)OverlayShape.Cubemap << OverlayShapeFlagShift), - ShapeFlag_OffcenterCubemap = unchecked((int)OverlayShape.OffcenterCubemap << OverlayShapeFlagShift), - ShapeFlagRangeMask = unchecked((int)0xF << OverlayShapeFlagShift), - } - - [StructLayout(LayoutKind.Sequential)] - public struct Vector2f - { - public float x; - public float y; - } - - [StructLayout(LayoutKind.Sequential)] - public struct Vector3f - { - public float x; - public float y; - public float z; - } - - [StructLayout(LayoutKind.Sequential)] - public struct Quatf - { - public float x; - public float y; - public float z; - public float w; - } - - [StructLayout(LayoutKind.Sequential)] - public struct Posef - { - public Quatf Orientation; - public Vector3f Position; - } - - [StructLayout(LayoutKind.Sequential)] - public struct PoseStatef - { - public Posef Pose; - public Vector3f Velocity; - public Vector3f Acceleration; - public Vector3f AngularVelocity; - public Vector3f AngularAcceleration; - double Time; - } - - [StructLayout(LayoutKind.Sequential)] - public struct ControllerState2 - { - public uint ConnectedControllers; - public uint Buttons; - public uint Touches; - public uint NearTouches; - public float LIndexTrigger; - public float RIndexTrigger; - public float LHandTrigger; - public float RHandTrigger; - public Vector2f LThumbstick; - public Vector2f RThumbstick; - public Vector2f LTouchpad; - public Vector2f RTouchpad; + } + + public enum SystemRegion + { + Unspecified = 0, + Japan, + China, + } + + public enum SystemHeadset + { + None = 0, + GearVR_R320, // Note4 Innovator + GearVR_R321, // S6 Innovator + GearVR_R322, // Commercial 1 + GearVR_R323, // Commercial 2 (USB Type C) + + Rift_DK1 = 0x1000, + Rift_DK2, + Rift_CV1, + } + + public enum OverlayShape + { + Quad = 0, + Cylinder = 1, + Cubemap = 2, + OffcenterCubemap = 4, + } + + public enum Step + { + Render = -1, + Physics = 0, + } + + private const int OverlayShapeFlagShift = 4; + private enum OverlayFlag + { + None = unchecked((int)0x00000000), + OnTop = unchecked((int)0x00000001), + HeadLocked = unchecked((int)0x00000002), + + // Using the 5-8 bits for shapes, total 16 potential shapes can be supported 0x000000[0]0 -> 0x000000[F]0 + ShapeFlag_Quad = unchecked((int)OverlayShape.Quad << OverlayShapeFlagShift), + ShapeFlag_Cylinder = unchecked((int)OverlayShape.Cylinder << OverlayShapeFlagShift), + ShapeFlag_Cubemap = unchecked((int)OverlayShape.Cubemap << OverlayShapeFlagShift), + ShapeFlag_OffcenterCubemap = unchecked((int)OverlayShape.OffcenterCubemap << OverlayShapeFlagShift), + ShapeFlagRangeMask = unchecked((int)0xF << OverlayShapeFlagShift), + } + + [StructLayout(LayoutKind.Sequential)] + public struct Vector2f + { + public float x; + public float y; + } + + [StructLayout(LayoutKind.Sequential)] + public struct Vector3f + { + public float x; + public float y; + public float z; + } + + [StructLayout(LayoutKind.Sequential)] + public struct Quatf + { + public float x; + public float y; + public float z; + public float w; + } + + [StructLayout(LayoutKind.Sequential)] + public struct Posef + { + public Quatf Orientation; + public Vector3f Position; + } + + [StructLayout(LayoutKind.Sequential)] + public struct PoseStatef + { + public Posef Pose; + public Vector3f Velocity; + public Vector3f Acceleration; + public Vector3f AngularVelocity; + public Vector3f AngularAcceleration; + double Time; + } + + [StructLayout(LayoutKind.Sequential)] + public struct ControllerState2 + { + public uint ConnectedControllers; + public uint Buttons; + public uint Touches; + public uint NearTouches; + public float LIndexTrigger; + public float RIndexTrigger; + public float LHandTrigger; + public float RHandTrigger; + public Vector2f LThumbstick; + public Vector2f RThumbstick; + public Vector2f LTouchpad; + public Vector2f RTouchpad; public ControllerState2(ControllerState cs) { @@ -336,507 +338,514 @@ public ControllerState2(ControllerState cs) } } - [StructLayout(LayoutKind.Sequential)] - public struct ControllerState - { - public uint ConnectedControllers; - public uint Buttons; - public uint Touches; - public uint NearTouches; - public float LIndexTrigger; - public float RIndexTrigger; - public float LHandTrigger; - public float RHandTrigger; - public Vector2f LThumbstick; - public Vector2f RThumbstick; - } - - [StructLayout(LayoutKind.Sequential)] - public struct HapticsBuffer - { - public IntPtr Samples; - public int SamplesCount; - } - - [StructLayout(LayoutKind.Sequential)] - public struct HapticsState - { - public int SamplesAvailable; - public int SamplesQueued; - } - - [StructLayout(LayoutKind.Sequential)] - public struct HapticsDesc - { - public int SampleRateHz; - public int SampleSizeInBytes; - public int MinimumSafeSamplesQueued; - public int MinimumBufferSamplesCount; - public int OptimalBufferSamplesCount; - public int MaximumBufferSamplesCount; - } - - [StructLayout(LayoutKind.Sequential)] - public struct AppPerfFrameStats - { - public int HmdVsyncIndex; - public int AppFrameIndex; - public int AppDroppedFrameCount; - public float AppMotionToPhotonLatency; - public float AppQueueAheadTime; - public float AppCpuElapsedTime; - public float AppGpuElapsedTime; - public int CompositorFrameIndex; - public int CompositorDroppedFrameCount; - public float CompositorLatency; - public float CompositorCpuElapsedTime; - public float CompositorGpuElapsedTime; - public float CompositorCpuStartToGpuEndElapsedTime; - public float CompositorGpuEndToVsyncElapsedTime; - } - - public const int AppPerfFrameStatsMaxCount = 5; - - [StructLayout(LayoutKind.Sequential)] - public struct AppPerfStats - { - [MarshalAs(UnmanagedType.ByValArray, SizeConst=AppPerfFrameStatsMaxCount)] - public AppPerfFrameStats[] FrameStats; - public int FrameStatsCount; - public Bool AnyFrameStatsDropped; - public float AdaptiveGpuPerformanceScale; - } - - [StructLayout(LayoutKind.Sequential)] - public struct Sizei - { - public int w; - public int h; - } - - [StructLayout(LayoutKind.Sequential)] - public struct Frustumf - { - public float zNear; - public float zFar; - public float fovX; - public float fovY; - } - - public enum BoundaryType - { - OuterBoundary = 0x0001, - PlayArea = 0x0100, - } - - [StructLayout(LayoutKind.Sequential)] - public struct BoundaryTestResult - { - public Bool IsTriggering; - public float ClosestDistance; - public Vector3f ClosestPoint; - public Vector3f ClosestPointNormal; - } - - [StructLayout(LayoutKind.Sequential)] - public struct BoundaryLookAndFeel - { - public Colorf Color; - } - - [StructLayout(LayoutKind.Sequential)] - public struct BoundaryGeometry - { - public BoundaryType BoundaryType; - [MarshalAs(UnmanagedType.ByValArray, SizeConst=256)] - public Vector3f[] Points; - public int PointsCount; - } - - [StructLayout(LayoutKind.Sequential)] - public struct Colorf - { - public float r; - public float g; - public float b; - public float a; - } - - public static bool initialized - { - get { - return OVRP_1_1_0.ovrp_GetInitialized() == OVRPlugin.Bool.True; - } - } - - public static bool chromatic - { - get { - if (version >= OVRP_1_7_0.version) - return OVRP_1_7_0.ovrp_GetAppChromaticCorrection() == OVRPlugin.Bool.True; - + [StructLayout(LayoutKind.Sequential)] + public struct ControllerState + { + public uint ConnectedControllers; + public uint Buttons; + public uint Touches; + public uint NearTouches; + public float LIndexTrigger; + public float RIndexTrigger; + public float LHandTrigger; + public float RHandTrigger; + public Vector2f LThumbstick; + public Vector2f RThumbstick; + } + + [StructLayout(LayoutKind.Sequential)] + public struct HapticsBuffer + { + public IntPtr Samples; + public int SamplesCount; + } + + [StructLayout(LayoutKind.Sequential)] + public struct HapticsState + { + public int SamplesAvailable; + public int SamplesQueued; + } + + [StructLayout(LayoutKind.Sequential)] + public struct HapticsDesc + { + public int SampleRateHz; + public int SampleSizeInBytes; + public int MinimumSafeSamplesQueued; + public int MinimumBufferSamplesCount; + public int OptimalBufferSamplesCount; + public int MaximumBufferSamplesCount; + } + + [StructLayout(LayoutKind.Sequential)] + public struct AppPerfFrameStats + { + public int HmdVsyncIndex; + public int AppFrameIndex; + public int AppDroppedFrameCount; + public float AppMotionToPhotonLatency; + public float AppQueueAheadTime; + public float AppCpuElapsedTime; + public float AppGpuElapsedTime; + public int CompositorFrameIndex; + public int CompositorDroppedFrameCount; + public float CompositorLatency; + public float CompositorCpuElapsedTime; + public float CompositorGpuElapsedTime; + public float CompositorCpuStartToGpuEndElapsedTime; + public float CompositorGpuEndToVsyncElapsedTime; + } + + public const int AppPerfFrameStatsMaxCount = 5; + + [StructLayout(LayoutKind.Sequential)] + public struct AppPerfStats + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = AppPerfFrameStatsMaxCount)] + public AppPerfFrameStats[] FrameStats; + public int FrameStatsCount; + public Bool AnyFrameStatsDropped; + public float AdaptiveGpuPerformanceScale; + } + + [StructLayout(LayoutKind.Sequential)] + public struct Sizei + { + public int w; + public int h; + } + + [StructLayout(LayoutKind.Sequential)] + public struct Frustumf + { + public float zNear; + public float zFar; + public float fovX; + public float fovY; + } + + public enum BoundaryType + { + OuterBoundary = 0x0001, + PlayArea = 0x0100, + } + + [StructLayout(LayoutKind.Sequential)] + public struct BoundaryTestResult + { + public Bool IsTriggering; + public float ClosestDistance; + public Vector3f ClosestPoint; + public Vector3f ClosestPointNormal; + } + + [StructLayout(LayoutKind.Sequential)] + public struct BoundaryLookAndFeel + { + public Colorf Color; + } + + [StructLayout(LayoutKind.Sequential)] + public struct BoundaryGeometry + { + public BoundaryType BoundaryType; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)] + public Vector3f[] Points; + public int PointsCount; + } + + [StructLayout(LayoutKind.Sequential)] + public struct Colorf + { + public float r; + public float g; + public float b; + public float a; + } + + public static bool initialized + { + get + { + return OVRP_1_1_0.ovrp_GetInitialized() == OVRPlugin.Bool.True; + } + } + + public static bool chromatic + { + get + { + if (version >= OVRP_1_7_0.version) + return OVRP_1_7_0.ovrp_GetAppChromaticCorrection() == OVRPlugin.Bool.True; + #if UNITY_ANDROID && !UNITY_EDITOR return false; #else - return true; + return true; #endif - } - - set { - if (version >= OVRP_1_7_0.version) - OVRP_1_7_0.ovrp_SetAppChromaticCorrection(ToBool(value)); - } - } - - public static bool monoscopic - { - get { return OVRP_1_1_0.ovrp_GetAppMonoscopic() == OVRPlugin.Bool.True; } - set { OVRP_1_1_0.ovrp_SetAppMonoscopic(ToBool(value)); } - } - - public static bool rotation - { - get { return OVRP_1_1_0.ovrp_GetTrackingOrientationEnabled() == Bool.True; } - set { OVRP_1_1_0.ovrp_SetTrackingOrientationEnabled(ToBool(value)); } - } - - public static bool position - { - get { return OVRP_1_1_0.ovrp_GetTrackingPositionEnabled() == Bool.True; } - set { OVRP_1_1_0.ovrp_SetTrackingPositionEnabled(ToBool(value)); } - } - - public static bool useIPDInPositionTracking - { - get { - if (version >= OVRP_1_6_0.version) - return OVRP_1_6_0.ovrp_GetTrackingIPDEnabled() == OVRPlugin.Bool.True; - - return true; - } - - set { - if (version >= OVRP_1_6_0.version) - OVRP_1_6_0.ovrp_SetTrackingIPDEnabled(ToBool(value)); - } - } - - public static bool positionSupported { get { return OVRP_1_1_0.ovrp_GetTrackingPositionSupported() == Bool.True; } } - - public static bool positionTracked { get { return OVRP_1_1_0.ovrp_GetNodePositionTracked(Node.EyeCenter) == Bool.True; } } - - public static bool powerSaving { get { return OVRP_1_1_0.ovrp_GetSystemPowerSavingMode() == Bool.True; } } - - public static bool hmdPresent { get { return OVRP_1_1_0.ovrp_GetNodePresent(Node.EyeCenter) == Bool.True; } } - - public static bool userPresent { get { return OVRP_1_1_0.ovrp_GetUserPresent() == Bool.True; } } - - public static bool headphonesPresent { get { return OVRP_1_3_0.ovrp_GetSystemHeadphonesPresent() == OVRPlugin.Bool.True; } } - - public static int recommendedMSAALevel - { - get { - if (version >= OVRP_1_6_0.version) - return OVRP_1_6_0.ovrp_GetSystemRecommendedMSAALevel (); - else - return 2; - } - } - - public static SystemRegion systemRegion - { - get { - if (version >= OVRP_1_5_0.version) - return OVRP_1_5_0.ovrp_GetSystemRegion(); - else - return SystemRegion.Unspecified; - } - } - - private static GUID _nativeAudioOutGuid = new OVRPlugin.GUID(); - private static Guid _cachedAudioOutGuid; - private static string _cachedAudioOutString; - public static string audioOutId - { - get - { - try - { - if (_nativeAudioOutGuid == null) - _nativeAudioOutGuid = new OVRPlugin.GUID(); - - IntPtr ptr = OVRP_1_1_0.ovrp_GetAudioOutId(); - if (ptr != IntPtr.Zero) - { - Marshal.PtrToStructure(ptr, _nativeAudioOutGuid); - Guid managedGuid = new Guid( - _nativeAudioOutGuid.a, - _nativeAudioOutGuid.b, - _nativeAudioOutGuid.c, - _nativeAudioOutGuid.d0, - _nativeAudioOutGuid.d1, - _nativeAudioOutGuid.d2, - _nativeAudioOutGuid.d3, - _nativeAudioOutGuid.d4, - _nativeAudioOutGuid.d5, - _nativeAudioOutGuid.d6, - _nativeAudioOutGuid.d7); - - if (managedGuid != _cachedAudioOutGuid) - { - _cachedAudioOutGuid = managedGuid; - _cachedAudioOutString = _cachedAudioOutGuid.ToString(); - } - - return _cachedAudioOutString; - } - } - catch {} - - return string.Empty; - } - } - - private static GUID _nativeAudioInGuid = new OVRPlugin.GUID(); - private static Guid _cachedAudioInGuid; - private static string _cachedAudioInString; - public static string audioInId - { - get - { - try - { - if (_nativeAudioInGuid == null) - _nativeAudioInGuid = new OVRPlugin.GUID(); - - IntPtr ptr = OVRP_1_1_0.ovrp_GetAudioInId(); - if (ptr != IntPtr.Zero) - { - Marshal.PtrToStructure(ptr, _nativeAudioInGuid); - Guid managedGuid = new Guid( - _nativeAudioInGuid.a, - _nativeAudioInGuid.b, - _nativeAudioInGuid.c, - _nativeAudioInGuid.d0, - _nativeAudioInGuid.d1, - _nativeAudioInGuid.d2, - _nativeAudioInGuid.d3, - _nativeAudioInGuid.d4, - _nativeAudioInGuid.d5, - _nativeAudioInGuid.d6, - _nativeAudioInGuid.d7); - - if (managedGuid != _cachedAudioInGuid) - { - _cachedAudioInGuid = managedGuid; - _cachedAudioInString = _cachedAudioInGuid.ToString(); - } - - return _cachedAudioInString; - } - } - catch {} - - return string.Empty; - } - } - - public static bool hasVrFocus { get { return OVRP_1_1_0.ovrp_GetAppHasVrFocus() == Bool.True; } } - - public static bool shouldQuit { get { return OVRP_1_1_0.ovrp_GetAppShouldQuit() == Bool.True; } } - - public static bool shouldRecenter { get { return OVRP_1_1_0.ovrp_GetAppShouldRecenter() == Bool.True; } } - - public static string productName { get { return OVRP_1_1_0.ovrp_GetSystemProductName(); } } - - public static string latency { get { return OVRP_1_1_0.ovrp_GetAppLatencyTimings(); } } - - public static float eyeDepth - { - get { return OVRP_1_1_0.ovrp_GetUserEyeDepth(); } - set { OVRP_1_1_0.ovrp_SetUserEyeDepth(value); } - } - - public static float eyeHeight - { - get { return OVRP_1_1_0.ovrp_GetUserEyeHeight(); } - set { OVRP_1_1_0.ovrp_SetUserEyeHeight(value); } - } - - public static float batteryLevel - { - get { return OVRP_1_1_0.ovrp_GetSystemBatteryLevel(); } - } - - public static float batteryTemperature - { - get { return OVRP_1_1_0.ovrp_GetSystemBatteryTemperature(); } - } - - public static int cpuLevel - { - get { return OVRP_1_1_0.ovrp_GetSystemCpuLevel(); } - set { OVRP_1_1_0.ovrp_SetSystemCpuLevel(value); } - } - - public static int gpuLevel - { - get { return OVRP_1_1_0.ovrp_GetSystemGpuLevel(); } - set { OVRP_1_1_0.ovrp_SetSystemGpuLevel(value); } - } - - public static int vsyncCount - { - get { return OVRP_1_1_0.ovrp_GetSystemVSyncCount(); } - set { OVRP_1_2_0.ovrp_SetSystemVSyncCount(value); } - } - - public static float systemVolume - { - get { return OVRP_1_1_0.ovrp_GetSystemVolume(); } - } - - public static float ipd - { - get { return OVRP_1_1_0.ovrp_GetUserIPD(); } - set { OVRP_1_1_0.ovrp_SetUserIPD(value); } - } - - public static bool occlusionMesh - { - get { return OVRP_1_3_0.ovrp_GetEyeOcclusionMeshEnabled() == Bool.True; } - set { OVRP_1_3_0.ovrp_SetEyeOcclusionMeshEnabled(ToBool(value)); } - } - - public static BatteryStatus batteryStatus - { - get { return OVRP_1_1_0.ovrp_GetSystemBatteryStatus(); } - } - - public static Frustumf GetEyeFrustum(Eye eyeId) { return OVRP_1_1_0.ovrp_GetNodeFrustum((Node)eyeId); } - public static Sizei GetEyeTextureSize(Eye eyeId) { return OVRP_0_1_0.ovrp_GetEyeTextureSize(eyeId); } - public static Posef GetTrackerPose(Tracker trackerId) { return GetNodePose((Node)((int)trackerId + (int)Node.TrackerZero), Step.Render); } - public static Frustumf GetTrackerFrustum(Tracker trackerId) { return OVRP_1_1_0.ovrp_GetNodeFrustum((Node)((int)trackerId + (int)Node.TrackerZero)); } - public static bool ShowUI(PlatformUI ui) { return OVRP_1_1_0.ovrp_ShowSystemUI(ui) == Bool.True; } - public static bool SetOverlayQuad(bool onTop, bool headLocked, IntPtr leftTexture, IntPtr rightTexture, IntPtr device, Posef pose, Vector3f scale, int layerIndex=0, OverlayShape shape=OverlayShape.Quad) - { - if (version >= OVRP_1_6_0.version) - { - uint flags = (uint)OverlayFlag.None; - if (onTop) - flags |= (uint)OverlayFlag.OnTop; - if (headLocked) - flags |= (uint)OverlayFlag.HeadLocked; - - if (shape == OverlayShape.Cylinder || shape == OverlayShape.Cubemap) - { + } + + set + { + if (version >= OVRP_1_7_0.version) + OVRP_1_7_0.ovrp_SetAppChromaticCorrection(ToBool(value)); + } + } + + public static bool monoscopic + { + get { return OVRP_1_1_0.ovrp_GetAppMonoscopic() == OVRPlugin.Bool.True; } + set { OVRP_1_1_0.ovrp_SetAppMonoscopic(ToBool(value)); } + } + + public static bool rotation + { + get { return OVRP_1_1_0.ovrp_GetTrackingOrientationEnabled() == Bool.True; } + set { OVRP_1_1_0.ovrp_SetTrackingOrientationEnabled(ToBool(value)); } + } + + public static bool position + { + get { return OVRP_1_1_0.ovrp_GetTrackingPositionEnabled() == Bool.True; } + set { OVRP_1_1_0.ovrp_SetTrackingPositionEnabled(ToBool(value)); } + } + + public static bool useIPDInPositionTracking + { + get + { + if (version >= OVRP_1_6_0.version) + return OVRP_1_6_0.ovrp_GetTrackingIPDEnabled() == OVRPlugin.Bool.True; + + return true; + } + + set + { + if (version >= OVRP_1_6_0.version) + OVRP_1_6_0.ovrp_SetTrackingIPDEnabled(ToBool(value)); + } + } + + public static bool positionSupported { get { return OVRP_1_1_0.ovrp_GetTrackingPositionSupported() == Bool.True; } } + + public static bool positionTracked { get { return OVRP_1_1_0.ovrp_GetNodePositionTracked(Node.EyeCenter) == Bool.True; } } + + public static bool powerSaving { get { return OVRP_1_1_0.ovrp_GetSystemPowerSavingMode() == Bool.True; } } + + public static bool hmdPresent { get { return OVRP_1_1_0.ovrp_GetNodePresent(Node.EyeCenter) == Bool.True; } } + + public static bool userPresent { get { return OVRP_1_1_0.ovrp_GetUserPresent() == Bool.True; } } + + public static bool headphonesPresent { get { return OVRP_1_3_0.ovrp_GetSystemHeadphonesPresent() == OVRPlugin.Bool.True; } } + + public static int recommendedMSAALevel + { + get + { + if (version >= OVRP_1_6_0.version) + return OVRP_1_6_0.ovrp_GetSystemRecommendedMSAALevel(); + else + return 2; + } + } + + public static SystemRegion systemRegion + { + get + { + if (version >= OVRP_1_5_0.version) + return OVRP_1_5_0.ovrp_GetSystemRegion(); + else + return SystemRegion.Unspecified; + } + } + + private static GUID _nativeAudioOutGuid = new OVRPlugin.GUID(); + private static Guid _cachedAudioOutGuid; + private static string _cachedAudioOutString; + public static string audioOutId + { + get + { + try + { + if (_nativeAudioOutGuid == null) + _nativeAudioOutGuid = new OVRPlugin.GUID(); + + IntPtr ptr = OVRP_1_1_0.ovrp_GetAudioOutId(); + if (ptr != IntPtr.Zero) + { + Marshal.PtrToStructure(ptr, _nativeAudioOutGuid); + Guid managedGuid = new Guid( + _nativeAudioOutGuid.a, + _nativeAudioOutGuid.b, + _nativeAudioOutGuid.c, + _nativeAudioOutGuid.d0, + _nativeAudioOutGuid.d1, + _nativeAudioOutGuid.d2, + _nativeAudioOutGuid.d3, + _nativeAudioOutGuid.d4, + _nativeAudioOutGuid.d5, + _nativeAudioOutGuid.d6, + _nativeAudioOutGuid.d7); + + if (managedGuid != _cachedAudioOutGuid) + { + _cachedAudioOutGuid = managedGuid; + _cachedAudioOutString = _cachedAudioOutGuid.ToString(); + } + + return _cachedAudioOutString; + } + } + catch { } + + return string.Empty; + } + } + + private static GUID _nativeAudioInGuid = new OVRPlugin.GUID(); + private static Guid _cachedAudioInGuid; + private static string _cachedAudioInString; + public static string audioInId + { + get + { + try + { + if (_nativeAudioInGuid == null) + _nativeAudioInGuid = new OVRPlugin.GUID(); + + IntPtr ptr = OVRP_1_1_0.ovrp_GetAudioInId(); + if (ptr != IntPtr.Zero) + { + Marshal.PtrToStructure(ptr, _nativeAudioInGuid); + Guid managedGuid = new Guid( + _nativeAudioInGuid.a, + _nativeAudioInGuid.b, + _nativeAudioInGuid.c, + _nativeAudioInGuid.d0, + _nativeAudioInGuid.d1, + _nativeAudioInGuid.d2, + _nativeAudioInGuid.d3, + _nativeAudioInGuid.d4, + _nativeAudioInGuid.d5, + _nativeAudioInGuid.d6, + _nativeAudioInGuid.d7); + + if (managedGuid != _cachedAudioInGuid) + { + _cachedAudioInGuid = managedGuid; + _cachedAudioInString = _cachedAudioInGuid.ToString(); + } + + return _cachedAudioInString; + } + } + catch { } + + return string.Empty; + } + } + + public static bool hasVrFocus { get { return OVRP_1_1_0.ovrp_GetAppHasVrFocus() == Bool.True; } } + + public static bool shouldQuit { get { return OVRP_1_1_0.ovrp_GetAppShouldQuit() == Bool.True; } } + + public static bool shouldRecenter { get { return OVRP_1_1_0.ovrp_GetAppShouldRecenter() == Bool.True; } } + + public static string productName { get { return OVRP_1_1_0.ovrp_GetSystemProductName(); } } + + public static string latency { get { return OVRP_1_1_0.ovrp_GetAppLatencyTimings(); } } + + public static float eyeDepth + { + get { return OVRP_1_1_0.ovrp_GetUserEyeDepth(); } + set { OVRP_1_1_0.ovrp_SetUserEyeDepth(value); } + } + + public static float eyeHeight + { + get { return OVRP_1_1_0.ovrp_GetUserEyeHeight(); } + set { OVRP_1_1_0.ovrp_SetUserEyeHeight(value); } + } + + public static float batteryLevel + { + get { return OVRP_1_1_0.ovrp_GetSystemBatteryLevel(); } + } + + public static float batteryTemperature + { + get { return OVRP_1_1_0.ovrp_GetSystemBatteryTemperature(); } + } + + public static int cpuLevel + { + get { return OVRP_1_1_0.ovrp_GetSystemCpuLevel(); } + set { OVRP_1_1_0.ovrp_SetSystemCpuLevel(value); } + } + + public static int gpuLevel + { + get { return OVRP_1_1_0.ovrp_GetSystemGpuLevel(); } + set { OVRP_1_1_0.ovrp_SetSystemGpuLevel(value); } + } + + public static int vsyncCount + { + get { return OVRP_1_1_0.ovrp_GetSystemVSyncCount(); } + set { OVRP_1_2_0.ovrp_SetSystemVSyncCount(value); } + } + + public static float systemVolume + { + get { return OVRP_1_1_0.ovrp_GetSystemVolume(); } + } + + public static float ipd + { + get { return OVRP_1_1_0.ovrp_GetUserIPD(); } + set { OVRP_1_1_0.ovrp_SetUserIPD(value); } + } + + public static bool occlusionMesh + { + get { return OVRP_1_3_0.ovrp_GetEyeOcclusionMeshEnabled() == Bool.True; } + set { OVRP_1_3_0.ovrp_SetEyeOcclusionMeshEnabled(ToBool(value)); } + } + + public static BatteryStatus batteryStatus + { + get { return OVRP_1_1_0.ovrp_GetSystemBatteryStatus(); } + } + + public static Frustumf GetEyeFrustum(Eye eyeId) { return OVRP_1_1_0.ovrp_GetNodeFrustum((Node)eyeId); } + public static Sizei GetEyeTextureSize(Eye eyeId) { return OVRP_0_1_0.ovrp_GetEyeTextureSize(eyeId); } + public static Posef GetTrackerPose(Tracker trackerId) { return GetNodePose((Node)((int)trackerId + (int)Node.TrackerZero), Step.Render); } + public static Frustumf GetTrackerFrustum(Tracker trackerId) { return OVRP_1_1_0.ovrp_GetNodeFrustum((Node)((int)trackerId + (int)Node.TrackerZero)); } + public static bool ShowUI(PlatformUI ui) { return OVRP_1_1_0.ovrp_ShowSystemUI(ui) == Bool.True; } + public static bool SetOverlayQuad(bool onTop, bool headLocked, IntPtr leftTexture, IntPtr rightTexture, IntPtr device, Posef pose, Vector3f scale, int layerIndex = 0, OverlayShape shape = OverlayShape.Quad) + { + if (version >= OVRP_1_6_0.version) + { + uint flags = (uint)OverlayFlag.None; + if (onTop) + flags |= (uint)OverlayFlag.OnTop; + if (headLocked) + flags |= (uint)OverlayFlag.HeadLocked; + + if (shape == OverlayShape.Cylinder || shape == OverlayShape.Cubemap) + { #if UNITY_ANDROID if (version >= OVRP_1_7_0.version) flags |= (uint)(shape) << OverlayShapeFlagShift; else #else - if (shape == OverlayShape.Cubemap && version >= OVRP_1_10_0.version) - flags |= (uint)(shape) << OverlayShapeFlagShift; - else + if (shape == OverlayShape.Cubemap && version >= OVRP_1_10_0.version) + flags |= (uint)(shape) << OverlayShapeFlagShift; + else #endif - return false; - } + return false; + } - if (shape == OverlayShape.OffcenterCubemap) - { + if (shape == OverlayShape.OffcenterCubemap) + { #if UNITY_ANDROID if (version >= OVRP_1_11_0.version) flags |= (uint)(shape) << OverlayShapeFlagShift; else #endif - return false; - } + return false; + } + + return OVRP_1_6_0.ovrp_SetOverlayQuad3(flags, leftTexture, rightTexture, device, pose, scale, layerIndex) == Bool.True; + } - return OVRP_1_6_0.ovrp_SetOverlayQuad3(flags, leftTexture, rightTexture, device, pose, scale, layerIndex) == Bool.True; - } + if (layerIndex != 0) + return false; - if (layerIndex != 0) - return false; - - return OVRP_0_1_1.ovrp_SetOverlayQuad2(ToBool(onTop), ToBool(headLocked), leftTexture, device, pose, scale) == Bool.True; - } - - public static bool UpdateNodePhysicsPoses(int frameIndex, double predictionSeconds) - { - if (version >= OVRP_1_8_0.version) - return OVRP_1_8_0.ovrp_Update2((int)Step.Physics, frameIndex, predictionSeconds) == Bool.True; - - return false; - } - - public static Posef GetNodePose(Node nodeId, Step stepId) - { - if (version >= OVRP_1_12_0.version) - return OVRP_1_12_0.ovrp_GetNodePoseState (stepId, nodeId).Pose; - - if (version >= OVRP_1_8_0.version && stepId == Step.Physics) - return OVRP_1_8_0.ovrp_GetNodePose2(0, nodeId); - - return OVRP_0_1_2.ovrp_GetNodePose(nodeId); - } - - public static Vector3f GetNodeVelocity(Node nodeId, Step stepId) - { - if (version >= OVRP_1_12_0.version) - return OVRP_1_12_0.ovrp_GetNodePoseState (stepId, nodeId).Velocity; - - if (version >= OVRP_1_8_0.version && stepId == Step.Physics) - return OVRP_1_8_0.ovrp_GetNodeVelocity2(0, nodeId).Position; - - return OVRP_0_1_3.ovrp_GetNodeVelocity(nodeId).Position; - } - - public static Vector3f GetNodeAngularVelocity(Node nodeId, Step stepId) - { - if (version >= OVRP_1_12_0.version) - return OVRP_1_12_0.ovrp_GetNodePoseState(stepId, nodeId).AngularVelocity; - - return new Vector3f(); //TODO: Convert legacy quat to vec3? - } - - public static Vector3f GetNodeAcceleration(Node nodeId, Step stepId) - { - if (version >= OVRP_1_12_0.version) - return OVRP_1_12_0.ovrp_GetNodePoseState (stepId, nodeId).Acceleration; - - if (version >= OVRP_1_8_0.version && stepId == Step.Physics) - return OVRP_1_8_0.ovrp_GetNodeAcceleration2(0, nodeId).Position; - - return OVRP_0_1_3.ovrp_GetNodeAcceleration(nodeId).Position; - } - - public static Vector3f GetNodeAngularAcceleration(Node nodeId, Step stepId) - { - if (version >= OVRP_1_12_0.version) - return OVRP_1_12_0.ovrp_GetNodePoseState(stepId, nodeId).AngularAcceleration; - - return new Vector3f(); //TODO: Convert legacy quat to vec3? - } - - public static bool GetNodePresent(Node nodeId) - { - return OVRP_1_1_0.ovrp_GetNodePresent(nodeId) == Bool.True; - } - - public static bool GetNodeOrientationTracked(Node nodeId) - { - return OVRP_1_1_0.ovrp_GetNodeOrientationTracked(nodeId) == Bool.True; - } - - public static bool GetNodePositionTracked(Node nodeId) - { - return OVRP_1_1_0.ovrp_GetNodePositionTracked(nodeId) == Bool.True; - } + return OVRP_0_1_1.ovrp_SetOverlayQuad2(ToBool(onTop), ToBool(headLocked), leftTexture, device, pose, scale) == Bool.True; + } + + public static bool UpdateNodePhysicsPoses(int frameIndex, double predictionSeconds) + { + if (version >= OVRP_1_8_0.version) + return OVRP_1_8_0.ovrp_Update2((int)Step.Physics, frameIndex, predictionSeconds) == Bool.True; + + return false; + } + + public static Posef GetNodePose(Node nodeId, Step stepId) + { + if (version >= OVRP_1_12_0.version) + return OVRP_1_12_0.ovrp_GetNodePoseState(stepId, nodeId).Pose; + + if (version >= OVRP_1_8_0.version && stepId == Step.Physics) + return OVRP_1_8_0.ovrp_GetNodePose2(0, nodeId); + + return OVRP_0_1_2.ovrp_GetNodePose(nodeId); + } + + public static Vector3f GetNodeVelocity(Node nodeId, Step stepId) + { + if (version >= OVRP_1_12_0.version) + return OVRP_1_12_0.ovrp_GetNodePoseState(stepId, nodeId).Velocity; + + if (version >= OVRP_1_8_0.version && stepId == Step.Physics) + return OVRP_1_8_0.ovrp_GetNodeVelocity2(0, nodeId).Position; + + return OVRP_0_1_3.ovrp_GetNodeVelocity(nodeId).Position; + } + + public static Vector3f GetNodeAngularVelocity(Node nodeId, Step stepId) + { + if (version >= OVRP_1_12_0.version) + return OVRP_1_12_0.ovrp_GetNodePoseState(stepId, nodeId).AngularVelocity; + + return new Vector3f(); //TODO: Convert legacy quat to vec3? + } + + public static Vector3f GetNodeAcceleration(Node nodeId, Step stepId) + { + if (version >= OVRP_1_12_0.version) + return OVRP_1_12_0.ovrp_GetNodePoseState(stepId, nodeId).Acceleration; + + if (version >= OVRP_1_8_0.version && stepId == Step.Physics) + return OVRP_1_8_0.ovrp_GetNodeAcceleration2(0, nodeId).Position; + + return OVRP_0_1_3.ovrp_GetNodeAcceleration(nodeId).Position; + } + + public static Vector3f GetNodeAngularAcceleration(Node nodeId, Step stepId) + { + if (version >= OVRP_1_12_0.version) + return OVRP_1_12_0.ovrp_GetNodePoseState(stepId, nodeId).AngularAcceleration; + + return new Vector3f(); //TODO: Convert legacy quat to vec3? + } + + public static bool GetNodePresent(Node nodeId) + { + return OVRP_1_1_0.ovrp_GetNodePresent(nodeId) == Bool.True; + } + + public static bool GetNodeOrientationTracked(Node nodeId) + { + return OVRP_1_1_0.ovrp_GetNodeOrientationTracked(nodeId) == Bool.True; + } + + public static bool GetNodePositionTracked(Node nodeId) + { + return OVRP_1_1_0.ovrp_GetNodePositionTracked(nodeId) == Bool.True; + } public static ControllerState GetControllerState(uint controllerMask) { return OVRP_1_1_0.ovrp_GetControllerState(controllerMask); - } + } public static ControllerState2 GetControllerState2(uint controllerMask) { @@ -846,699 +855,699 @@ public static ControllerState2 GetControllerState2(uint controllerMask) } return new ControllerState2(OVRP_1_1_0.ovrp_GetControllerState(controllerMask)); - } - - public static bool SetControllerVibration(uint controllerMask, float frequency, float amplitude) - { - return OVRP_0_1_2.ovrp_SetControllerVibration(controllerMask, frequency, amplitude) == Bool.True; - } - - public static HapticsDesc GetControllerHapticsDesc(uint controllerMask) - { - if (version >= OVRP_1_6_0.version) - { - return OVRP_1_6_0.ovrp_GetControllerHapticsDesc(controllerMask); - } - else - { - return new HapticsDesc(); - } - } - - public static HapticsState GetControllerHapticsState(uint controllerMask) - { - if (version >= OVRP_1_6_0.version) - { - return OVRP_1_6_0.ovrp_GetControllerHapticsState(controllerMask); - } - else - { - return new HapticsState(); - } - } - - public static bool SetControllerHaptics(uint controllerMask, HapticsBuffer hapticsBuffer) - { - if (version >= OVRP_1_6_0.version) - { - return OVRP_1_6_0.ovrp_SetControllerHaptics(controllerMask, hapticsBuffer) == Bool.True; - } - else - { - return false; - } - } - - public static float GetEyeRecommendedResolutionScale() - { - if (version >= OVRP_1_6_0.version) - { - return OVRP_1_6_0.ovrp_GetEyeRecommendedResolutionScale(); - } - else - { - return 1.0f; - } - } - - public static float GetAppCpuStartToGpuEndTime() - { - if (version >= OVRP_1_6_0.version) - { - return OVRP_1_6_0.ovrp_GetAppCpuStartToGpuEndTime(); - } - else - { - return 0.0f; - } - } - - public static bool GetBoundaryConfigured() - { - if (version >= OVRP_1_8_0.version) - { - return OVRP_1_8_0.ovrp_GetBoundaryConfigured() == OVRPlugin.Bool.True; - } - else - { - return false; - } - } - - public static BoundaryTestResult TestBoundaryNode(Node nodeId, BoundaryType boundaryType) - { - if (version >= OVRP_1_8_0.version) - { - return OVRP_1_8_0.ovrp_TestBoundaryNode(nodeId, boundaryType); - } - else - { - return new BoundaryTestResult(); - } - } - - public static BoundaryTestResult TestBoundaryPoint(Vector3f point, BoundaryType boundaryType) - { - if (version >= OVRP_1_8_0.version) - { - return OVRP_1_8_0.ovrp_TestBoundaryPoint(point, boundaryType); - } - else - { - return new BoundaryTestResult(); - } - } - - public static bool SetBoundaryLookAndFeel(BoundaryLookAndFeel lookAndFeel) - { - if (version >= OVRP_1_8_0.version) - { - return OVRP_1_8_0.ovrp_SetBoundaryLookAndFeel(lookAndFeel) == OVRPlugin.Bool.True; - } - else - { - return false; - } - } - - public static bool ResetBoundaryLookAndFeel() - { - if (version >= OVRP_1_8_0.version) - { - return OVRP_1_8_0.ovrp_ResetBoundaryLookAndFeel() == OVRPlugin.Bool.True; - } - else - { - return false; - } - } - - public static BoundaryGeometry GetBoundaryGeometry(BoundaryType boundaryType) - { - if (version >= OVRP_1_8_0.version) - { - return OVRP_1_8_0.ovrp_GetBoundaryGeometry(boundaryType); - } - else - { - return new BoundaryGeometry(); - } - } - - public static bool GetBoundaryGeometry2(BoundaryType boundaryType, IntPtr points, ref int pointsCount) - { - if (version >= OVRP_1_9_0.version) - { - return OVRP_1_9_0.ovrp_GetBoundaryGeometry2(boundaryType, points, ref pointsCount) == OVRPlugin.Bool.True; - } - else - { - pointsCount = 0; + } - return false; - } - } - - public static AppPerfStats GetAppPerfStats() - { - if (version >= OVRP_1_9_0.version) - { - return OVRP_1_9_0.ovrp_GetAppPerfStats(); - } - else - { - return new AppPerfStats(); - } - } - - public static bool ResetAppPerfStats() - { - if (version >= OVRP_1_9_0.version) - { - return OVRP_1_9_0.ovrp_ResetAppPerfStats() == OVRPlugin.Bool.True; - } - else - { - return false; - } - } - - public static float GetAppFramerate() - { - if (version >= OVRP_1_12_0.version) - { - return OVRP_1_12_0.ovrp_GetAppFramerate(); - } - else - { - return 0.0f; - } - } - - public static EyeTextureFormat GetDesiredEyeTextureFormat() - { - if (version >= OVRP_1_11_0.version ) - { - uint eyeTextureFormatValue = (uint) OVRP_1_11_0.ovrp_GetDesiredEyeTextureFormat(); - - // convert both R8G8B8A8 and R8G8B8A8_SRGB to R8G8B8A8 here for avoid confusing developers - if (eyeTextureFormatValue == 1) - eyeTextureFormatValue = 0; - - return (EyeTextureFormat)eyeTextureFormatValue; - } - else - { - return EyeTextureFormat.Default; - } - } - - public static bool SetDesiredEyeTextureFormat(EyeTextureFormat value) - { - if (version >= OVRP_1_11_0.version) - { - return OVRP_1_11_0.ovrp_SetDesiredEyeTextureFormat(value) == OVRPlugin.Bool.True; - } - else - { - return false; - } - } - - public static Vector3f GetBoundaryDimensions(BoundaryType boundaryType) - { - if (version >= OVRP_1_8_0.version) - { - return OVRP_1_8_0.ovrp_GetBoundaryDimensions(boundaryType); - } - else - { - return new Vector3f(); - } - } - - public static bool GetBoundaryVisible() - { - if (version >= OVRP_1_8_0.version) - { - return OVRP_1_8_0.ovrp_GetBoundaryVisible() == OVRPlugin.Bool.True; - } - else - { - return false; - } - } - - public static bool SetBoundaryVisible(bool value) - { - if (version >= OVRP_1_8_0.version) - { - return OVRP_1_8_0.ovrp_SetBoundaryVisible(ToBool(value)) == OVRPlugin.Bool.True; - } - else - { - return false; - } - } - - public static SystemHeadset GetSystemHeadsetType() - { - if (version >= OVRP_1_9_0.version) - return OVRP_1_9_0.ovrp_GetSystemHeadsetType(); - - return SystemHeadset.None; - } - - public static Controller GetActiveController() - { - if (version >= OVRP_1_9_0.version) - return OVRP_1_9_0.ovrp_GetActiveController(); - - return Controller.None; - } - - public static Controller GetConnectedControllers() - { - if (version >= OVRP_1_9_0.version) - return OVRP_1_9_0.ovrp_GetConnectedControllers(); - - return Controller.None; - } - - private static Bool ToBool(bool b) - { - return (b) ? OVRPlugin.Bool.True : OVRPlugin.Bool.False; - } - - public static TrackingOrigin GetTrackingOriginType() - { - return OVRP_1_0_0.ovrp_GetTrackingOriginType(); - } - - public static bool SetTrackingOriginType(TrackingOrigin originType) - { - return OVRP_1_0_0.ovrp_SetTrackingOriginType(originType) == Bool.True; - } - - public static Posef GetTrackingCalibratedOrigin() - { - return OVRP_1_0_0.ovrp_GetTrackingCalibratedOrigin(); - } - - public static bool SetTrackingCalibratedOrigin() - { - return OVRP_1_2_0.ovrpi_SetTrackingCalibratedOrigin() == Bool.True; - } - - public static bool RecenterTrackingOrigin(RecenterFlags flags) - { - return OVRP_1_0_0.ovrp_RecenterTrackingOrigin((uint)flags) == Bool.True; - } - - private const string pluginName = "OVRPlugin"; - private static Version _versionZero = new System.Version(0, 0, 0); - - private static class OVRP_0_1_0 - { - public static readonly System.Version version = new System.Version(0, 1, 0); - - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Sizei ovrp_GetEyeTextureSize(Eye eyeId); - } + public static bool SetControllerVibration(uint controllerMask, float frequency, float amplitude) + { + return OVRP_0_1_2.ovrp_SetControllerVibration(controllerMask, frequency, amplitude) == Bool.True; + } + + public static HapticsDesc GetControllerHapticsDesc(uint controllerMask) + { + if (version >= OVRP_1_6_0.version) + { + return OVRP_1_6_0.ovrp_GetControllerHapticsDesc(controllerMask); + } + else + { + return new HapticsDesc(); + } + } + + public static HapticsState GetControllerHapticsState(uint controllerMask) + { + if (version >= OVRP_1_6_0.version) + { + return OVRP_1_6_0.ovrp_GetControllerHapticsState(controllerMask); + } + else + { + return new HapticsState(); + } + } + + public static bool SetControllerHaptics(uint controllerMask, HapticsBuffer hapticsBuffer) + { + if (version >= OVRP_1_6_0.version) + { + return OVRP_1_6_0.ovrp_SetControllerHaptics(controllerMask, hapticsBuffer) == Bool.True; + } + else + { + return false; + } + } + + public static float GetEyeRecommendedResolutionScale() + { + if (version >= OVRP_1_6_0.version) + { + return OVRP_1_6_0.ovrp_GetEyeRecommendedResolutionScale(); + } + else + { + return 1.0f; + } + } + + public static float GetAppCpuStartToGpuEndTime() + { + if (version >= OVRP_1_6_0.version) + { + return OVRP_1_6_0.ovrp_GetAppCpuStartToGpuEndTime(); + } + else + { + return 0.0f; + } + } + + public static bool GetBoundaryConfigured() + { + if (version >= OVRP_1_8_0.version) + { + return OVRP_1_8_0.ovrp_GetBoundaryConfigured() == OVRPlugin.Bool.True; + } + else + { + return false; + } + } + + public static BoundaryTestResult TestBoundaryNode(Node nodeId, BoundaryType boundaryType) + { + if (version >= OVRP_1_8_0.version) + { + return OVRP_1_8_0.ovrp_TestBoundaryNode(nodeId, boundaryType); + } + else + { + return new BoundaryTestResult(); + } + } + + public static BoundaryTestResult TestBoundaryPoint(Vector3f point, BoundaryType boundaryType) + { + if (version >= OVRP_1_8_0.version) + { + return OVRP_1_8_0.ovrp_TestBoundaryPoint(point, boundaryType); + } + else + { + return new BoundaryTestResult(); + } + } + + public static bool SetBoundaryLookAndFeel(BoundaryLookAndFeel lookAndFeel) + { + if (version >= OVRP_1_8_0.version) + { + return OVRP_1_8_0.ovrp_SetBoundaryLookAndFeel(lookAndFeel) == OVRPlugin.Bool.True; + } + else + { + return false; + } + } + + public static bool ResetBoundaryLookAndFeel() + { + if (version >= OVRP_1_8_0.version) + { + return OVRP_1_8_0.ovrp_ResetBoundaryLookAndFeel() == OVRPlugin.Bool.True; + } + else + { + return false; + } + } + + public static BoundaryGeometry GetBoundaryGeometry(BoundaryType boundaryType) + { + if (version >= OVRP_1_8_0.version) + { + return OVRP_1_8_0.ovrp_GetBoundaryGeometry(boundaryType); + } + else + { + return new BoundaryGeometry(); + } + } + + public static bool GetBoundaryGeometry2(BoundaryType boundaryType, IntPtr points, ref int pointsCount) + { + if (version >= OVRP_1_9_0.version) + { + return OVRP_1_9_0.ovrp_GetBoundaryGeometry2(boundaryType, points, ref pointsCount) == OVRPlugin.Bool.True; + } + else + { + pointsCount = 0; + + return false; + } + } + + public static AppPerfStats GetAppPerfStats() + { + if (version >= OVRP_1_9_0.version) + { + return OVRP_1_9_0.ovrp_GetAppPerfStats(); + } + else + { + return new AppPerfStats(); + } + } + + public static bool ResetAppPerfStats() + { + if (version >= OVRP_1_9_0.version) + { + return OVRP_1_9_0.ovrp_ResetAppPerfStats() == OVRPlugin.Bool.True; + } + else + { + return false; + } + } + + public static float GetAppFramerate() + { + if (version >= OVRP_1_12_0.version) + { + return OVRP_1_12_0.ovrp_GetAppFramerate(); + } + else + { + return 0.0f; + } + } + + public static EyeTextureFormat GetDesiredEyeTextureFormat() + { + if (version >= OVRP_1_11_0.version) + { + uint eyeTextureFormatValue = (uint)OVRP_1_11_0.ovrp_GetDesiredEyeTextureFormat(); + + // convert both R8G8B8A8 and R8G8B8A8_SRGB to R8G8B8A8 here for avoid confusing developers + if (eyeTextureFormatValue == 1) + eyeTextureFormatValue = 0; + + return (EyeTextureFormat)eyeTextureFormatValue; + } + else + { + return EyeTextureFormat.Default; + } + } + + public static bool SetDesiredEyeTextureFormat(EyeTextureFormat value) + { + if (version >= OVRP_1_11_0.version) + { + return OVRP_1_11_0.ovrp_SetDesiredEyeTextureFormat(value) == OVRPlugin.Bool.True; + } + else + { + return false; + } + } + + public static Vector3f GetBoundaryDimensions(BoundaryType boundaryType) + { + if (version >= OVRP_1_8_0.version) + { + return OVRP_1_8_0.ovrp_GetBoundaryDimensions(boundaryType); + } + else + { + return new Vector3f(); + } + } + + public static bool GetBoundaryVisible() + { + if (version >= OVRP_1_8_0.version) + { + return OVRP_1_8_0.ovrp_GetBoundaryVisible() == OVRPlugin.Bool.True; + } + else + { + return false; + } + } + + public static bool SetBoundaryVisible(bool value) + { + if (version >= OVRP_1_8_0.version) + { + return OVRP_1_8_0.ovrp_SetBoundaryVisible(ToBool(value)) == OVRPlugin.Bool.True; + } + else + { + return false; + } + } + + public static SystemHeadset GetSystemHeadsetType() + { + if (version >= OVRP_1_9_0.version) + return OVRP_1_9_0.ovrp_GetSystemHeadsetType(); + + return SystemHeadset.None; + } + + public static Controller GetActiveController() + { + if (version >= OVRP_1_9_0.version) + return OVRP_1_9_0.ovrp_GetActiveController(); + + return Controller.None; + } + + public static Controller GetConnectedControllers() + { + if (version >= OVRP_1_9_0.version) + return OVRP_1_9_0.ovrp_GetConnectedControllers(); - private static class OVRP_0_1_1 - { - public static readonly System.Version version = new System.Version(0, 1, 1); + return Controller.None; + } - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetOverlayQuad2(Bool onTop, Bool headLocked, IntPtr texture, IntPtr device, Posef pose, Vector3f scale); - } + private static Bool ToBool(bool b) + { + return (b) ? OVRPlugin.Bool.True : OVRPlugin.Bool.False; + } - private static class OVRP_0_1_2 - { - public static readonly System.Version version = new System.Version(0, 1, 2); + public static TrackingOrigin GetTrackingOriginType() + { + return OVRP_1_0_0.ovrp_GetTrackingOriginType(); + } - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Posef ovrp_GetNodePose(Node nodeId); + public static bool SetTrackingOriginType(TrackingOrigin originType) + { + return OVRP_1_0_0.ovrp_SetTrackingOriginType(originType) == Bool.True; + } - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetControllerVibration(uint controllerMask, float frequency, float amplitude); - } + public static Posef GetTrackingCalibratedOrigin() + { + return OVRP_1_0_0.ovrp_GetTrackingCalibratedOrigin(); + } - private static class OVRP_0_1_3 - { - public static readonly System.Version version = new System.Version(0, 1, 3); + public static bool SetTrackingCalibratedOrigin() + { + return OVRP_1_2_0.ovrpi_SetTrackingCalibratedOrigin() == Bool.True; + } - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Posef ovrp_GetNodeVelocity(Node nodeId); + public static bool RecenterTrackingOrigin(RecenterFlags flags) + { + return OVRP_1_0_0.ovrp_RecenterTrackingOrigin((uint)flags) == Bool.True; + } - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Posef ovrp_GetNodeAcceleration(Node nodeId); - } + private const string pluginName = "OVRPlugin"; + private static Version _versionZero = new System.Version(0, 0, 0); - private static class OVRP_0_5_0 - { - public static readonly System.Version version = new System.Version(0, 5, 0); - } + private static class OVRP_0_1_0 + { + public static readonly System.Version version = new System.Version(0, 1, 0); - private static class OVRP_1_0_0 - { - public static readonly System.Version version = new System.Version(1, 0, 0); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Sizei ovrp_GetEyeTextureSize(Eye eyeId); + } - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern TrackingOrigin ovrp_GetTrackingOriginType(); + private static class OVRP_0_1_1 + { + public static readonly System.Version version = new System.Version(0, 1, 1); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetTrackingOriginType(TrackingOrigin originType); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetOverlayQuad2(Bool onTop, Bool headLocked, IntPtr texture, IntPtr device, Posef pose, Vector3f scale); + } - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Posef ovrp_GetTrackingCalibratedOrigin(); + private static class OVRP_0_1_2 + { + public static readonly System.Version version = new System.Version(0, 1, 2); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_RecenterTrackingOrigin(uint flags); - } + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Posef ovrp_GetNodePose(Node nodeId); - private static class OVRP_1_1_0 - { - public static readonly System.Version version = new System.Version(1, 1, 0); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetControllerVibration(uint controllerMask, float frequency, float amplitude); + } - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetInitialized(); + private static class OVRP_0_1_3 + { + public static readonly System.Version version = new System.Version(0, 1, 3); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ovrp_GetVersion")] - private static extern IntPtr _ovrp_GetVersion(); - public static string ovrp_GetVersion() { return Marshal.PtrToStringAnsi(_ovrp_GetVersion()); } + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Posef ovrp_GetNodeVelocity(Node nodeId); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ovrp_GetNativeSDKVersion")] - private static extern IntPtr _ovrp_GetNativeSDKVersion(); - public static string ovrp_GetNativeSDKVersion() { return Marshal.PtrToStringAnsi(_ovrp_GetNativeSDKVersion()); } + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Posef ovrp_GetNodeAcceleration(Node nodeId); + } - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr ovrp_GetAudioOutId(); + private static class OVRP_0_5_0 + { + public static readonly System.Version version = new System.Version(0, 5, 0); + } - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr ovrp_GetAudioInId(); + private static class OVRP_1_0_0 + { + public static readonly System.Version version = new System.Version(1, 0, 0); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern float ovrp_GetEyeTextureScale(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern TrackingOrigin ovrp_GetTrackingOriginType(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetEyeTextureScale(float value); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetTrackingOriginType(TrackingOrigin originType); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetTrackingOrientationSupported(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Posef ovrp_GetTrackingCalibratedOrigin(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetTrackingOrientationEnabled(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_RecenterTrackingOrigin(uint flags); + } - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetTrackingOrientationEnabled(Bool value); + private static class OVRP_1_1_0 + { + public static readonly System.Version version = new System.Version(1, 1, 0); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetTrackingPositionSupported(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetInitialized(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetTrackingPositionEnabled(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ovrp_GetVersion")] + private static extern IntPtr _ovrp_GetVersion(); + public static string ovrp_GetVersion() { return Marshal.PtrToStringAnsi(_ovrp_GetVersion()); } - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetTrackingPositionEnabled(Bool value); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ovrp_GetNativeSDKVersion")] + private static extern IntPtr _ovrp_GetNativeSDKVersion(); + public static string ovrp_GetNativeSDKVersion() { return Marshal.PtrToStringAnsi(_ovrp_GetNativeSDKVersion()); } - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetNodePresent(Node nodeId); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr ovrp_GetAudioOutId(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetNodeOrientationTracked(Node nodeId); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr ovrp_GetAudioInId(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetNodePositionTracked(Node nodeId); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern float ovrp_GetEyeTextureScale(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Frustumf ovrp_GetNodeFrustum(Node nodeId); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetEyeTextureScale(float value); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern ControllerState ovrp_GetControllerState(uint controllerMask); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetTrackingOrientationSupported(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern int ovrp_GetSystemCpuLevel(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetTrackingOrientationEnabled(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetSystemCpuLevel(int value); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetTrackingOrientationEnabled(Bool value); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern int ovrp_GetSystemGpuLevel(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetTrackingPositionSupported(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetSystemGpuLevel(int value); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetTrackingPositionEnabled(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetSystemPowerSavingMode(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetTrackingPositionEnabled(Bool value); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern float ovrp_GetSystemDisplayFrequency(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetNodePresent(Node nodeId); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern int ovrp_GetSystemVSyncCount(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetNodeOrientationTracked(Node nodeId); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern float ovrp_GetSystemVolume(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetNodePositionTracked(Node nodeId); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern BatteryStatus ovrp_GetSystemBatteryStatus(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Frustumf ovrp_GetNodeFrustum(Node nodeId); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern float ovrp_GetSystemBatteryLevel(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern ControllerState ovrp_GetControllerState(uint controllerMask); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern float ovrp_GetSystemBatteryTemperature(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern int ovrp_GetSystemCpuLevel(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ovrp_GetSystemProductName")] - private static extern IntPtr _ovrp_GetSystemProductName(); - public static string ovrp_GetSystemProductName() { return Marshal.PtrToStringAnsi(_ovrp_GetSystemProductName()); } + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetSystemCpuLevel(int value); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_ShowSystemUI(PlatformUI ui); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern int ovrp_GetSystemGpuLevel(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetAppMonoscopic(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetSystemGpuLevel(int value); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetAppMonoscopic(Bool value); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetSystemPowerSavingMode(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetAppHasVrFocus(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern float ovrp_GetSystemDisplayFrequency(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetAppShouldQuit(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern int ovrp_GetSystemVSyncCount(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetAppShouldRecenter(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern float ovrp_GetSystemVolume(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ovrp_GetAppLatencyTimings")] - private static extern IntPtr _ovrp_GetAppLatencyTimings(); - public static string ovrp_GetAppLatencyTimings() { return Marshal.PtrToStringAnsi(_ovrp_GetAppLatencyTimings()); } + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern BatteryStatus ovrp_GetSystemBatteryStatus(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetUserPresent(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern float ovrp_GetSystemBatteryLevel(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern float ovrp_GetUserIPD(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern float ovrp_GetSystemBatteryTemperature(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetUserIPD(float value); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ovrp_GetSystemProductName")] + private static extern IntPtr _ovrp_GetSystemProductName(); + public static string ovrp_GetSystemProductName() { return Marshal.PtrToStringAnsi(_ovrp_GetSystemProductName()); } - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern float ovrp_GetUserEyeDepth(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_ShowSystemUI(PlatformUI ui); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetUserEyeDepth(float value); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetAppMonoscopic(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern float ovrp_GetUserEyeHeight(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetAppMonoscopic(Bool value); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetUserEyeHeight(float value); - } + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetAppHasVrFocus(); - private static class OVRP_1_2_0 - { - public static readonly System.Version version = new System.Version(1, 2, 0); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetAppShouldQuit(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetSystemVSyncCount(int vsyncCount); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetAppShouldRecenter(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrpi_SetTrackingCalibratedOrigin(); - } + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ovrp_GetAppLatencyTimings")] + private static extern IntPtr _ovrp_GetAppLatencyTimings(); + public static string ovrp_GetAppLatencyTimings() { return Marshal.PtrToStringAnsi(_ovrp_GetAppLatencyTimings()); } - private static class OVRP_1_3_0 - { - public static readonly System.Version version = new System.Version(1, 3, 0); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetUserPresent(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetEyeOcclusionMeshEnabled(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern float ovrp_GetUserIPD(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetEyeOcclusionMeshEnabled(Bool value); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetUserIPD(float value); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetSystemHeadphonesPresent(); - } + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern float ovrp_GetUserEyeDepth(); - private static class OVRP_1_5_0 - { - public static readonly System.Version version = new System.Version(1, 5, 0); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetUserEyeDepth(float value); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern SystemRegion ovrp_GetSystemRegion(); - } + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern float ovrp_GetUserEyeHeight(); - private static class OVRP_1_6_0 - { - public static readonly System.Version version = new System.Version(1, 6, 0); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetUserEyeHeight(float value); + } - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetTrackingIPDEnabled(); + private static class OVRP_1_2_0 + { + public static readonly System.Version version = new System.Version(1, 2, 0); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetTrackingIPDEnabled(Bool value); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetSystemVSyncCount(int vsyncCount); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern HapticsDesc ovrp_GetControllerHapticsDesc(uint controllerMask); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrpi_SetTrackingCalibratedOrigin(); + } - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern HapticsState ovrp_GetControllerHapticsState(uint controllerMask); + private static class OVRP_1_3_0 + { + public static readonly System.Version version = new System.Version(1, 3, 0); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetControllerHaptics(uint controllerMask, HapticsBuffer hapticsBuffer); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetEyeOcclusionMeshEnabled(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetOverlayQuad3(uint flags, IntPtr textureLeft, IntPtr textureRight, IntPtr device, Posef pose, Vector3f scale, int layerIndex); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetEyeOcclusionMeshEnabled(Bool value); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern float ovrp_GetEyeRecommendedResolutionScale(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetSystemHeadphonesPresent(); + } - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern float ovrp_GetAppCpuStartToGpuEndTime(); + private static class OVRP_1_5_0 + { + public static readonly System.Version version = new System.Version(1, 5, 0); + + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern SystemRegion ovrp_GetSystemRegion(); + } + + private static class OVRP_1_6_0 + { + public static readonly System.Version version = new System.Version(1, 6, 0); + + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetTrackingIPDEnabled(); + + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetTrackingIPDEnabled(Bool value); + + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern HapticsDesc ovrp_GetControllerHapticsDesc(uint controllerMask); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern int ovrp_GetSystemRecommendedMSAALevel(); - } + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern HapticsState ovrp_GetControllerHapticsState(uint controllerMask); - private static class OVRP_1_7_0 - { - public static readonly System.Version version = new System.Version(1, 7, 0); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetControllerHaptics(uint controllerMask, HapticsBuffer hapticsBuffer); + + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetOverlayQuad3(uint flags, IntPtr textureLeft, IntPtr textureRight, IntPtr device, Posef pose, Vector3f scale, int layerIndex); + + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern float ovrp_GetEyeRecommendedResolutionScale(); + + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern float ovrp_GetAppCpuStartToGpuEndTime(); + + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern int ovrp_GetSystemRecommendedMSAALevel(); + } + + private static class OVRP_1_7_0 + { + public static readonly System.Version version = new System.Version(1, 7, 0); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetAppChromaticCorrection(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetAppChromaticCorrection(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetAppChromaticCorrection(Bool value); - } + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetAppChromaticCorrection(Bool value); + } - private static class OVRP_1_8_0 - { - public static readonly System.Version version = new System.Version(1, 8, 0); + private static class OVRP_1_8_0 + { + public static readonly System.Version version = new System.Version(1, 8, 0); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetBoundaryConfigured(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetBoundaryConfigured(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern BoundaryTestResult ovrp_TestBoundaryNode(Node nodeId, BoundaryType boundaryType); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern BoundaryTestResult ovrp_TestBoundaryNode(Node nodeId, BoundaryType boundaryType); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern BoundaryTestResult ovrp_TestBoundaryPoint(Vector3f point, BoundaryType boundaryType); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern BoundaryTestResult ovrp_TestBoundaryPoint(Vector3f point, BoundaryType boundaryType); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetBoundaryLookAndFeel(BoundaryLookAndFeel lookAndFeel); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetBoundaryLookAndFeel(BoundaryLookAndFeel lookAndFeel); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_ResetBoundaryLookAndFeel(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_ResetBoundaryLookAndFeel(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern BoundaryGeometry ovrp_GetBoundaryGeometry(BoundaryType boundaryType); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern BoundaryGeometry ovrp_GetBoundaryGeometry(BoundaryType boundaryType); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Vector3f ovrp_GetBoundaryDimensions(BoundaryType boundaryType); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Vector3f ovrp_GetBoundaryDimensions(BoundaryType boundaryType); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetBoundaryVisible(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetBoundaryVisible(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetBoundaryVisible(Bool value); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetBoundaryVisible(Bool value); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_Update2(int stateId, int frameIndex, double predictionSeconds); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_Update2(int stateId, int frameIndex, double predictionSeconds); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Posef ovrp_GetNodePose2(int stateId, Node nodeId); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Posef ovrp_GetNodePose2(int stateId, Node nodeId); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Posef ovrp_GetNodeVelocity2(int stateId, Node nodeId); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Posef ovrp_GetNodeVelocity2(int stateId, Node nodeId); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Posef ovrp_GetNodeAcceleration2(int stateId, Node nodeId); - } + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Posef ovrp_GetNodeAcceleration2(int stateId, Node nodeId); + } - private static class OVRP_1_9_0 - { - public static readonly System.Version version = new System.Version(1, 9, 0); + private static class OVRP_1_9_0 + { + public static readonly System.Version version = new System.Version(1, 9, 0); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern SystemHeadset ovrp_GetSystemHeadsetType(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern SystemHeadset ovrp_GetSystemHeadsetType(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Controller ovrp_GetActiveController(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Controller ovrp_GetActiveController(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Controller ovrp_GetConnectedControllers(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Controller ovrp_GetConnectedControllers(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_GetBoundaryGeometry2(BoundaryType boundaryType, IntPtr points, ref int pointsCount); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_GetBoundaryGeometry2(BoundaryType boundaryType, IntPtr points, ref int pointsCount); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern AppPerfStats ovrp_GetAppPerfStats(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern AppPerfStats ovrp_GetAppPerfStats(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_ResetAppPerfStats(); - } + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_ResetAppPerfStats(); + } - private static class OVRP_1_10_0 - { - public static readonly System.Version version = new System.Version(1, 10, 0); - } + private static class OVRP_1_10_0 + { + public static readonly System.Version version = new System.Version(1, 10, 0); + } - private static class OVRP_1_11_0 - { - public static readonly System.Version version = new System.Version(1, 11, 0); + private static class OVRP_1_11_0 + { + public static readonly System.Version version = new System.Version(1, 11, 0); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern Bool ovrp_SetDesiredEyeTextureFormat(EyeTextureFormat value); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern Bool ovrp_SetDesiredEyeTextureFormat(EyeTextureFormat value); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern EyeTextureFormat ovrp_GetDesiredEyeTextureFormat(); - } + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern EyeTextureFormat ovrp_GetDesiredEyeTextureFormat(); + } - private static class OVRP_1_12_0 - { - public static readonly System.Version version = new System.Version(1, 12, 0); + private static class OVRP_1_12_0 + { + public static readonly System.Version version = new System.Version(1, 12, 0); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern float ovrp_GetAppFramerate(); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern float ovrp_GetAppFramerate(); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern PoseStatef ovrp_GetNodePoseState(Step stepId, Node nodeId); + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern PoseStatef ovrp_GetNodePoseState(Step stepId, Node nodeId); - [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] - public static extern ControllerState2 ovrp_GetControllerState2(uint controllerMask); - } + [DllImport(pluginName, CallingConvention = CallingConvention.Cdecl)] + public static extern ControllerState2 ovrp_GetControllerState2(uint controllerMask); + } - private static class OVRP_1_13_0 - { - public static readonly System.Version version = new System.Version(1, 13, 0); - } + private static class OVRP_1_13_0 + { + public static readonly System.Version version = new System.Version(1, 13, 0); + } - private static class OVRP_1_14_0 - { - public static readonly System.Version version = new System.Version(1, 14, 0); - } + private static class OVRP_1_14_0 + { + public static readonly System.Version version = new System.Version(1, 14, 0); + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRProfile.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRProfile.cs index 073485a2..9c52b2ea 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRProfile.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRProfile.cs @@ -29,27 +29,27 @@ limitations under the License. /// public class OVRProfile : Object { - [System.Obsolete] - public enum State - { - NOT_TRIGGERED, - LOADING, - READY, - ERROR - }; - - [System.Obsolete] - public string id { get { return "000abc123def"; } } - [System.Obsolete] - public string userName { get { return "Oculus User"; } } - [System.Obsolete] - public string locale { get { return "en_US"; } } - - public float ipd { get { return Vector3.Distance (OVRPlugin.GetNodePose (OVRPlugin.Node.EyeLeft, OVRPlugin.Step.Render).ToOVRPose ().position, OVRPlugin.GetNodePose (OVRPlugin.Node.EyeRight, OVRPlugin.Step.Render).ToOVRPose ().position); } } - public float eyeHeight { get { return OVRPlugin.eyeHeight; } } - public float eyeDepth { get { return OVRPlugin.eyeDepth; } } - public float neckHeight { get { return eyeHeight - 0.075f; } } - - [System.Obsolete] - public State state { get { return State.READY; } } + [System.Obsolete] + public enum State + { + NOT_TRIGGERED, + LOADING, + READY, + ERROR + }; + + [System.Obsolete] + public string id { get { return "000abc123def"; } } + [System.Obsolete] + public string userName { get { return "Oculus User"; } } + [System.Obsolete] + public string locale { get { return "en_US"; } } + + public float ipd { get { return Vector3.Distance(OVRPlugin.GetNodePose(OVRPlugin.Node.EyeLeft, OVRPlugin.Step.Render).ToOVRPose().position, OVRPlugin.GetNodePose(OVRPlugin.Node.EyeRight, OVRPlugin.Step.Render).ToOVRPose().position); } } + public float eyeHeight { get { return OVRPlugin.eyeHeight; } } + public float eyeDepth { get { return OVRPlugin.eyeDepth; } } + public float neckHeight { get { return eyeHeight - 0.075f; } } + + [System.Obsolete] + public State state { get { return State.READY; } } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRTouchpad.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRTouchpad.cs index 1f412588..d66785af 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRTouchpad.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRTouchpad.cs @@ -27,210 +27,210 @@ limitations under the License. /// public static class OVRTouchpad { - /// - /// Touch Type. - /// - public enum TouchEvent - { - SingleTap, - Left, - Right, - Up, - Down, - }; - - /// - /// Details about a touch event. - /// - public class TouchArgs : EventArgs - { - public TouchEvent TouchType; - } - - /// - /// Occurs when touched. - /// - public static event EventHandler TouchHandler; - - /// - /// Native Touch State. - /// - enum TouchState - { - Init, - Down, - Stationary, - Move, - Up - }; - - static TouchState touchState = TouchState.Init; - //static Vector2 moveAmount; - static float minMovMagnitude = 100.0f; // Tune this to gage between click and swipe - - // mouse - static Vector3 moveAmountMouse; - static float minMovMagnitudeMouse = 25.0f; - - // Disable the unused variable warning + /// + /// Touch Type. + /// + public enum TouchEvent + { + SingleTap, + Left, + Right, + Up, + Down, + }; + + /// + /// Details about a touch event. + /// + public class TouchArgs : EventArgs + { + public TouchEvent TouchType; + } + + /// + /// Occurs when touched. + /// + public static event EventHandler TouchHandler; + + /// + /// Native Touch State. + /// + enum TouchState + { + Init, + Down, + Stationary, + Move, + Up + }; + + static TouchState touchState = TouchState.Init; + //static Vector2 moveAmount; + static float minMovMagnitude = 100.0f; // Tune this to gage between click and swipe + + // mouse + static Vector3 moveAmountMouse; + static float minMovMagnitudeMouse = 25.0f; + + // Disable the unused variable warning #pragma warning disable 0414 - // Ensures that the TouchpadHelper will be created automatically upon start of the scene. - static private OVRTouchpadHelper touchpadHelper = - (new GameObject("OVRTouchpadHelper")).AddComponent(); + // Ensures that the TouchpadHelper will be created automatically upon start of the scene. + static private OVRTouchpadHelper touchpadHelper = + (new GameObject("OVRTouchpadHelper")).AddComponent(); #pragma warning restore 0414 - - /// - /// Add the Touchpad game object into the scene. - /// - static public void Create() - { - // Does nothing but call constructor to add game object into scene - } - - static public void Update() - { -/* - // TOUCHPAD INPUT - if (Input.touchCount > 0) - { - switch(Input.GetTouch(0).phase) - { - case(TouchPhase.Began): - touchState = TouchState.Down; - // Get absolute location of touch - moveAmount = Input.GetTouch(0).position; - break; - - case(TouchPhase.Moved): - touchState = TouchState.Move; - break; - - case(TouchPhase.Stationary): - touchState = TouchState.Stationary; - break; - - case(TouchPhase.Ended): - moveAmount -= Input.GetTouch(0).position; - HandleInput(touchState, ref moveAmount); - touchState = TouchState.Init; - break; - - case(TouchPhase.Canceled): - Debug.Log( "CANCELLED\n" ); - touchState = TouchState.Init; - break; - } - } -*/ - // MOUSE INPUT - if (Input.GetMouseButtonDown(0)) - { - moveAmountMouse = Input.mousePosition; - touchState = TouchState.Down; - } - else if (Input.GetMouseButtonUp(0)) - { - moveAmountMouse -= Input.mousePosition; - HandleInputMouse(ref moveAmountMouse); - touchState = TouchState.Init; - } - } - - static public void OnDisable() - { - } - - /// - /// Determines if input was a click or swipe and sends message to all prescribers. - /// - static void HandleInput(TouchState state, ref Vector2 move) - { - if ((move.magnitude < minMovMagnitude) || (touchState == TouchState.Stationary)) - { - //Debug.Log( "CLICK" ); - } - else if (touchState == TouchState.Move) - { - move.Normalize(); - - // Left - if(Mathf.Abs(move.x) > Mathf.Abs (move.y)) - { - if(move.x > 0.0f) - { - //Debug.Log( "SWIPE: LEFT" ); - } - else - { - //Debug.Log( "SWIPE: RIGHT" ); - } - } - // Right - else - { - if(move.y > 0.0f) - { - //Debug.Log( "SWIPE: DOWN" ); - } - else - { - //Debug.Log( "SWIPE: UP" ); - } - } - } - } - - static void HandleInputMouse(ref Vector3 move) - { - if (move.magnitude < minMovMagnitudeMouse) - { - if (TouchHandler != null) - { - TouchHandler(null, new TouchArgs() { TouchType = TouchEvent.SingleTap }); - } - } - else - { - move.Normalize(); - - // Left/Right - if (Mathf.Abs(move.x) > Mathf.Abs(move.y)) - { - if (move.x > 0.0f) - { - if (TouchHandler != null) - { - TouchHandler(null, new TouchArgs () { TouchType = TouchEvent.Left }); - } - } - else - { - if (TouchHandler != null) - { - TouchHandler(null, new TouchArgs () { TouchType = TouchEvent.Right }); - } - } - } - // Up/Down - else - { - if (move.y > 0.0f) - { - if (TouchHandler != null) - { - TouchHandler(null, new TouchArgs () { TouchType = TouchEvent.Down }); - } - } - else - { - if(TouchHandler != null) - { - TouchHandler(null, new TouchArgs () { TouchType = TouchEvent.Up }); - } - } - } - } - } + + /// + /// Add the Touchpad game object into the scene. + /// + static public void Create() + { + // Does nothing but call constructor to add game object into scene + } + + static public void Update() + { + /* + // TOUCHPAD INPUT + if (Input.touchCount > 0) + { + switch(Input.GetTouch(0).phase) + { + case(TouchPhase.Began): + touchState = TouchState.Down; + // Get absolute location of touch + moveAmount = Input.GetTouch(0).position; + break; + + case(TouchPhase.Moved): + touchState = TouchState.Move; + break; + + case(TouchPhase.Stationary): + touchState = TouchState.Stationary; + break; + + case(TouchPhase.Ended): + moveAmount -= Input.GetTouch(0).position; + HandleInput(touchState, ref moveAmount); + touchState = TouchState.Init; + break; + + case(TouchPhase.Canceled): + Debug.Log( "CANCELLED\n" ); + touchState = TouchState.Init; + break; + } + } + */ + // MOUSE INPUT + if (Input.GetMouseButtonDown(0)) + { + moveAmountMouse = Input.mousePosition; + touchState = TouchState.Down; + } + else if (Input.GetMouseButtonUp(0)) + { + moveAmountMouse -= Input.mousePosition; + HandleInputMouse(ref moveAmountMouse); + touchState = TouchState.Init; + } + } + + static public void OnDisable() + { + } + + /// + /// Determines if input was a click or swipe and sends message to all prescribers. + /// + static void HandleInput(TouchState state, ref Vector2 move) + { + if ((move.magnitude < minMovMagnitude) || (touchState == TouchState.Stationary)) + { + //Debug.Log( "CLICK" ); + } + else if (touchState == TouchState.Move) + { + move.Normalize(); + + // Left + if (Mathf.Abs(move.x) > Mathf.Abs(move.y)) + { + if (move.x > 0.0f) + { + //Debug.Log( "SWIPE: LEFT" ); + } + else + { + //Debug.Log( "SWIPE: RIGHT" ); + } + } + // Right + else + { + if (move.y > 0.0f) + { + //Debug.Log( "SWIPE: DOWN" ); + } + else + { + //Debug.Log( "SWIPE: UP" ); + } + } + } + } + + static void HandleInputMouse(ref Vector3 move) + { + if (move.magnitude < minMovMagnitudeMouse) + { + if (TouchHandler != null) + { + TouchHandler(null, new TouchArgs() { TouchType = TouchEvent.SingleTap }); + } + } + else + { + move.Normalize(); + + // Left/Right + if (Mathf.Abs(move.x) > Mathf.Abs(move.y)) + { + if (move.x > 0.0f) + { + if (TouchHandler != null) + { + TouchHandler(null, new TouchArgs() { TouchType = TouchEvent.Left }); + } + } + else + { + if (TouchHandler != null) + { + TouchHandler(null, new TouchArgs() { TouchType = TouchEvent.Right }); + } + } + } + // Up/Down + else + { + if (move.y > 0.0f) + { + if (TouchHandler != null) + { + TouchHandler(null, new TouchArgs() { TouchType = TouchEvent.Down }); + } + } + else + { + if (TouchHandler != null) + { + TouchHandler(null, new TouchArgs() { TouchType = TouchEvent.Up }); + } + } + } + } + } } /// @@ -239,53 +239,53 @@ static void HandleInputMouse(ref Vector3 move) /// public sealed class OVRTouchpadHelper : MonoBehaviour { - void Awake() - { - DontDestroyOnLoad(gameObject); - } - - void Start() - { - // Add a listener to the OVRMessenger for testing - OVRTouchpad.TouchHandler += LocalTouchEventCallback; - } - - void Update() - { - OVRTouchpad.Update(); - } - - public void OnDisable() - { - OVRTouchpad.OnDisable(); - } - - void LocalTouchEventCallback(object sender, EventArgs args) - { - var touchArgs = (OVRTouchpad.TouchArgs)args; - OVRTouchpad.TouchEvent touchEvent = touchArgs.TouchType; - - switch(touchEvent) - { - case OVRTouchpad.TouchEvent.SingleTap: - //Debug.Log("SINGLE CLICK\n"); - break; - - case OVRTouchpad.TouchEvent.Left: - //Debug.Log("LEFT SWIPE\n"); - break; - - case OVRTouchpad.TouchEvent.Right: - //Debug.Log("RIGHT SWIPE\n"); - break; - - case OVRTouchpad.TouchEvent.Up: - //Debug.Log("UP SWIPE\n"); - break; - - case OVRTouchpad.TouchEvent.Down: - //Debug.Log("DOWN SWIPE\n"); - break; - } - } + void Awake() + { + DontDestroyOnLoad(gameObject); + } + + void Start() + { + // Add a listener to the OVRMessenger for testing + OVRTouchpad.TouchHandler += LocalTouchEventCallback; + } + + void Update() + { + OVRTouchpad.Update(); + } + + public void OnDisable() + { + OVRTouchpad.OnDisable(); + } + + void LocalTouchEventCallback(object sender, EventArgs args) + { + var touchArgs = (OVRTouchpad.TouchArgs)args; + OVRTouchpad.TouchEvent touchEvent = touchArgs.TouchType; + + switch (touchEvent) + { + case OVRTouchpad.TouchEvent.SingleTap: + //Debug.Log("SINGLE CLICK\n"); + break; + + case OVRTouchpad.TouchEvent.Left: + //Debug.Log("LEFT SWIPE\n"); + break; + + case OVRTouchpad.TouchEvent.Right: + //Debug.Log("RIGHT SWIPE\n"); + break; + + case OVRTouchpad.TouchEvent.Up: + //Debug.Log("UP SWIPE\n"); + break; + + case OVRTouchpad.TouchEvent.Down: + //Debug.Log("DOWN SWIPE\n"); + break; + } + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRTracker.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRTracker.cs index a033c082..ab79a0e1 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/OVRTracker.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/OVRTracker.cs @@ -29,171 +29,176 @@ limitations under the License. /// public class OVRTracker { - /// - /// The (symmetric) visible area in front of the sensor. - /// - public struct Frustum - { - /// - /// The sensor's minimum supported distance to the HMD. - /// - public float nearZ; - /// - /// The sensor's maximum supported distance to the HMD. - /// - public float farZ; - /// - /// The sensor's horizontal and vertical fields of view in degrees. - /// - public Vector2 fov; - } - - /// - /// If true, a sensor is attached to the system. - /// - public bool isPresent - { - get { - if (!OVRManager.isHmdPresent) - return false; - - return OVRPlugin.positionSupported; - } - } - - /// - /// If true, the sensor is actively tracking the HMD's position. Otherwise the HMD may be temporarily occluded, the system may not support position tracking, etc. - /// - public bool isPositionTracked - { - get { - return OVRPlugin.positionTracked; - } - } - - /// - /// If this is true and a sensor is available, the system will use position tracking when isPositionTracked is also true. - /// - public bool isEnabled - { - get { - if (!OVRManager.isHmdPresent) - return false; - - return OVRPlugin.position; + /// + /// The (symmetric) visible area in front of the sensor. + /// + public struct Frustum + { + /// + /// The sensor's minimum supported distance to the HMD. + /// + public float nearZ; + /// + /// The sensor's maximum supported distance to the HMD. + /// + public float farZ; + /// + /// The sensor's horizontal and vertical fields of view in degrees. + /// + public Vector2 fov; + } + + /// + /// If true, a sensor is attached to the system. + /// + public bool isPresent + { + get + { + if (!OVRManager.isHmdPresent) + return false; + + return OVRPlugin.positionSupported; } + } + + /// + /// If true, the sensor is actively tracking the HMD's position. Otherwise the HMD may be temporarily occluded, the system may not support position tracking, etc. + /// + public bool isPositionTracked + { + get + { + return OVRPlugin.positionTracked; + } + } + + /// + /// If this is true and a sensor is available, the system will use position tracking when isPositionTracked is also true. + /// + public bool isEnabled + { + get + { + if (!OVRManager.isHmdPresent) + return false; + + return OVRPlugin.position; + } + + set + { + if (!OVRManager.isHmdPresent) + return; - set { - if (!OVRManager.isHmdPresent) - return; - - OVRPlugin.position = value; - } - } - - /// - /// Returns the number of sensors currently connected to the system. - /// - public int count - { - get { - int count = 0; - - for (int i = 0; i < (int)OVRPlugin.Tracker.Count; ++i) - { - if (GetPresent(i)) - count++; - } - - return count; - } - } - - /// - /// Gets the sensor's viewing frustum. - /// - public Frustum GetFrustum(int tracker = 0) - { - if (!OVRManager.isHmdPresent) - return new Frustum(); - - return OVRPlugin.GetTrackerFrustum((OVRPlugin.Tracker)tracker).ToFrustum(); - } - - /// - /// Gets the sensor's pose, relative to the head's pose at the time of the last pose recentering. - /// - public OVRPose GetPose(int tracker = 0) - { - if (!OVRManager.isHmdPresent) - return OVRPose.identity; - - OVRPose p; - switch (tracker) - { - case 0: - p = OVRPlugin.GetNodePose(OVRPlugin.Node.TrackerZero, OVRPlugin.Step.Render).ToOVRPose(); - break; - case 1: - p = OVRPlugin.GetNodePose(OVRPlugin.Node.TrackerOne, OVRPlugin.Step.Render).ToOVRPose(); - break; - case 2: - p = OVRPlugin.GetNodePose(OVRPlugin.Node.TrackerTwo, OVRPlugin.Step.Render).ToOVRPose(); - break; - case 3: - p = OVRPlugin.GetNodePose(OVRPlugin.Node.TrackerThree, OVRPlugin.Step.Render).ToOVRPose(); - break; - default: - return OVRPose.identity; - } - - return new OVRPose() - { - position = p.position, - orientation = p.orientation * Quaternion.Euler(0, 180, 0) - }; - } - - /// - /// If true, the pose of the sensor is valid and is ready to be queried. - /// - public bool GetPoseValid(int tracker = 0) - { - if (!OVRManager.isHmdPresent) - return false; - - switch (tracker) - { - case 0: - return OVRPlugin.GetNodePositionTracked(OVRPlugin.Node.TrackerZero); - case 1: - return OVRPlugin.GetNodePositionTracked(OVRPlugin.Node.TrackerOne); - case 2: - return OVRPlugin.GetNodePositionTracked(OVRPlugin.Node.TrackerTwo); - case 3: - return OVRPlugin.GetNodePositionTracked(OVRPlugin.Node.TrackerThree); - default: - return false; - } - } - - public bool GetPresent(int tracker = 0) - { - if (!OVRManager.isHmdPresent) - return false; - - switch (tracker) - { - case 0: - return OVRPlugin.GetNodePresent(OVRPlugin.Node.TrackerZero); - case 1: - return OVRPlugin.GetNodePresent(OVRPlugin.Node.TrackerOne); - case 2: - return OVRPlugin.GetNodePresent(OVRPlugin.Node.TrackerTwo); - case 3: - return OVRPlugin.GetNodePresent(OVRPlugin.Node.TrackerThree); - default: - return false; - } - } + OVRPlugin.position = value; + } + } + + /// + /// Returns the number of sensors currently connected to the system. + /// + public int count + { + get + { + int count = 0; + + for (int i = 0; i < (int)OVRPlugin.Tracker.Count; ++i) + { + if (GetPresent(i)) + count++; + } + + return count; + } + } + + /// + /// Gets the sensor's viewing frustum. + /// + public Frustum GetFrustum(int tracker = 0) + { + if (!OVRManager.isHmdPresent) + return new Frustum(); + + return OVRPlugin.GetTrackerFrustum((OVRPlugin.Tracker)tracker).ToFrustum(); + } + + /// + /// Gets the sensor's pose, relative to the head's pose at the time of the last pose recentering. + /// + public OVRPose GetPose(int tracker = 0) + { + if (!OVRManager.isHmdPresent) + return OVRPose.identity; + + OVRPose p; + switch (tracker) + { + case 0: + p = OVRPlugin.GetNodePose(OVRPlugin.Node.TrackerZero, OVRPlugin.Step.Render).ToOVRPose(); + break; + case 1: + p = OVRPlugin.GetNodePose(OVRPlugin.Node.TrackerOne, OVRPlugin.Step.Render).ToOVRPose(); + break; + case 2: + p = OVRPlugin.GetNodePose(OVRPlugin.Node.TrackerTwo, OVRPlugin.Step.Render).ToOVRPose(); + break; + case 3: + p = OVRPlugin.GetNodePose(OVRPlugin.Node.TrackerThree, OVRPlugin.Step.Render).ToOVRPose(); + break; + default: + return OVRPose.identity; + } + + return new OVRPose() + { + position = p.position, + orientation = p.orientation * Quaternion.Euler(0, 180, 0) + }; + } + + /// + /// If true, the pose of the sensor is valid and is ready to be queried. + /// + public bool GetPoseValid(int tracker = 0) + { + if (!OVRManager.isHmdPresent) + return false; + + switch (tracker) + { + case 0: + return OVRPlugin.GetNodePositionTracked(OVRPlugin.Node.TrackerZero); + case 1: + return OVRPlugin.GetNodePositionTracked(OVRPlugin.Node.TrackerOne); + case 2: + return OVRPlugin.GetNodePositionTracked(OVRPlugin.Node.TrackerTwo); + case 3: + return OVRPlugin.GetNodePositionTracked(OVRPlugin.Node.TrackerThree); + default: + return false; + } + } + + public bool GetPresent(int tracker = 0) + { + if (!OVRManager.isHmdPresent) + return false; + + switch (tracker) + { + case 0: + return OVRPlugin.GetNodePresent(OVRPlugin.Node.TrackerZero); + case 1: + return OVRPlugin.GetNodePresent(OVRPlugin.Node.TrackerOne); + case 2: + return OVRPlugin.GetNodePresent(OVRPlugin.Node.TrackerTwo); + case 3: + return OVRPlugin.GetNodePresent(OVRPlugin.Node.TrackerThree); + default: + return false; + } + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRChromaticAberration.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRChromaticAberration.cs index 3fe013a5..97961cd1 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRChromaticAberration.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRChromaticAberration.cs @@ -26,31 +26,31 @@ limitations under the License. /// public class OVRChromaticAberration : MonoBehaviour { - /// - /// The button that will toggle chromatic aberration correction. - /// - public OVRInput.RawButton toggleButton = OVRInput.RawButton.X; - - private bool chromatic = false; - - void Start () - { - // Enable/Disable Chromatic Aberration Correction. - // NOTE: Enabling Chromatic Aberration for mobile has a large performance cost. - OVRManager.instance.chromatic = chromatic; - } - - void Update() - { - // NOTE: some of the buttons defined in OVRInput.RawButton are not available on the Android game pad controller - if (OVRInput.GetDown(toggleButton)) - { - //************************* - // toggle chromatic aberration correction - //************************* - chromatic = !chromatic; - OVRManager.instance.chromatic = chromatic; - } - } + /// + /// The button that will toggle chromatic aberration correction. + /// + public OVRInput.RawButton toggleButton = OVRInput.RawButton.X; + + private bool chromatic = false; + + void Start() + { + // Enable/Disable Chromatic Aberration Correction. + // NOTE: Enabling Chromatic Aberration for mobile has a large performance cost. + OVRManager.instance.chromatic = chromatic; + } + + void Update() + { + // NOTE: some of the buttons defined in OVRInput.RawButton are not available on the Android game pad controller + if (OVRInput.GetDown(toggleButton)) + { + //************************* + // toggle chromatic aberration correction + //************************* + chromatic = !chromatic; + OVRManager.instance.chromatic = chromatic; + } + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRCubemapCapture.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRCubemapCapture.cs index 680acba5..ba59227f 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRCubemapCapture.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRCubemapCapture.cs @@ -17,236 +17,236 @@ public class OVRCubemapCapture : MonoBehaviour { - /// - /// Enable the automatic screenshot trigger, which will capture a cubemap after autoTriggerDelay (seconds) - /// - public bool autoTriggerAfterLaunch = true; - public float autoTriggerDelay = 1.0f; - private float autoTriggerElapse = 0.0f; - - /// - /// Trigger cubemap screenshot if user pressed key triggeredByKey - /// - public KeyCode triggeredByKey = KeyCode.F8; - - /// - /// The complete file path for saving the cubemap screenshot, including the filename and extension - /// if pathName is blank, screenshots will be saved into %USERPROFILE%\Documents\OVR_ScreenShot360 - /// - public string pathName; - - /// - /// The cube face resolution - /// - public int cubemapSize = 2048; - - // Update is called once per frame - void Update() - { - // Trigger after autoTriggerDelay - if (autoTriggerAfterLaunch) - { - autoTriggerElapse += Time.deltaTime; - if (autoTriggerElapse >= autoTriggerDelay) - { - autoTriggerAfterLaunch = false; - TriggerCubemapCapture(transform.position, cubemapSize, pathName); - } - } - - // Trigger by press triggeredByKey - if ( Input.GetKeyDown( triggeredByKey ) ) - { - TriggerCubemapCapture(transform.position, cubemapSize, pathName); - } - } - - /// - /// Generate unity cubemap at specific location and save into JPG/PNG - /// - /// - /// Default save folder: your app's persistentDataPath - /// Default file name: using current time OVR_hh_mm_ss.png - /// Note1: this will take a few seconds to finish - /// Note2: if you only want to specify path not filename, please end [pathName] with "/" - /// - - public static void TriggerCubemapCapture(Vector3 capturePos, int cubemapSize = 2048, string pathName = null) - { - GameObject ownerObj = new GameObject("CubemapCamera", typeof(Camera)); - ownerObj.hideFlags = HideFlags.HideAndDontSave; - ownerObj.transform.position = capturePos; - ownerObj.transform.rotation = Quaternion.identity; - Camera camComponent = ownerObj.GetComponent(); - camComponent.farClipPlane = 10000.0f; - camComponent.enabled = false; - - Cubemap cubemap = new Cubemap(cubemapSize, TextureFormat.RGB24, false); - RenderIntoCubemap(camComponent, cubemap); - SaveCubemapCapture(cubemap, pathName); - DestroyImmediate(cubemap); - DestroyImmediate(ownerObj); - } - - - public static void RenderIntoCubemap(Camera ownerCamera, Cubemap outCubemap) - { - int width = (int)outCubemap.width; - int height = (int)outCubemap.height; - - CubemapFace[] faces = new CubemapFace[] { CubemapFace.PositiveX, CubemapFace.NegativeX, CubemapFace.PositiveY, CubemapFace.NegativeY, CubemapFace.PositiveZ, CubemapFace.NegativeZ }; - Vector3[] faceAngles = new Vector3[] { new Vector3(0.0f, 90.0f, 0.0f), new Vector3(0.0f, -90.0f, 0.0f), new Vector3(-90.0f, 0.0f, 0.0f), new Vector3(90.0f, 0.0f, 0.0f), new Vector3(0.0f, 0.0f, 0.0f), new Vector3(0.0f, 180.0f, 0.0f) }; - - // Backup states - RenderTexture backupRenderTex = RenderTexture.active; - float backupFieldOfView = ownerCamera.fieldOfView; - float backupAspect = ownerCamera.aspect; - Quaternion backupRot = ownerCamera.transform.rotation; - //RenderTexture backupRT = ownerCamera.targetTexture; - - // Enable 8X MSAA - RenderTexture faceTexture = new RenderTexture(width, height, 24); - faceTexture.antiAliasing = 8; + /// + /// Enable the automatic screenshot trigger, which will capture a cubemap after autoTriggerDelay (seconds) + /// + public bool autoTriggerAfterLaunch = true; + public float autoTriggerDelay = 1.0f; + private float autoTriggerElapse = 0.0f; + + /// + /// Trigger cubemap screenshot if user pressed key triggeredByKey + /// + public KeyCode triggeredByKey = KeyCode.F8; + + /// + /// The complete file path for saving the cubemap screenshot, including the filename and extension + /// if pathName is blank, screenshots will be saved into %USERPROFILE%\Documents\OVR_ScreenShot360 + /// + public string pathName; + + /// + /// The cube face resolution + /// + public int cubemapSize = 2048; + + // Update is called once per frame + void Update() + { + // Trigger after autoTriggerDelay + if (autoTriggerAfterLaunch) + { + autoTriggerElapse += Time.deltaTime; + if (autoTriggerElapse >= autoTriggerDelay) + { + autoTriggerAfterLaunch = false; + TriggerCubemapCapture(transform.position, cubemapSize, pathName); + } + } + + // Trigger by press triggeredByKey + if (Input.GetKeyDown(triggeredByKey)) + { + TriggerCubemapCapture(transform.position, cubemapSize, pathName); + } + } + + /// + /// Generate unity cubemap at specific location and save into JPG/PNG + /// + /// + /// Default save folder: your app's persistentDataPath + /// Default file name: using current time OVR_hh_mm_ss.png + /// Note1: this will take a few seconds to finish + /// Note2: if you only want to specify path not filename, please end [pathName] with "/" + /// + + public static void TriggerCubemapCapture(Vector3 capturePos, int cubemapSize = 2048, string pathName = null) + { + GameObject ownerObj = new GameObject("CubemapCamera", typeof(Camera)); + ownerObj.hideFlags = HideFlags.HideAndDontSave; + ownerObj.transform.position = capturePos; + ownerObj.transform.rotation = Quaternion.identity; + Camera camComponent = ownerObj.GetComponent(); + camComponent.farClipPlane = 10000.0f; + camComponent.enabled = false; + + Cubemap cubemap = new Cubemap(cubemapSize, TextureFormat.RGB24, false); + RenderIntoCubemap(camComponent, cubemap); + SaveCubemapCapture(cubemap, pathName); + DestroyImmediate(cubemap); + DestroyImmediate(ownerObj); + } + + + public static void RenderIntoCubemap(Camera ownerCamera, Cubemap outCubemap) + { + int width = (int)outCubemap.width; + int height = (int)outCubemap.height; + + CubemapFace[] faces = new CubemapFace[] { CubemapFace.PositiveX, CubemapFace.NegativeX, CubemapFace.PositiveY, CubemapFace.NegativeY, CubemapFace.PositiveZ, CubemapFace.NegativeZ }; + Vector3[] faceAngles = new Vector3[] { new Vector3(0.0f, 90.0f, 0.0f), new Vector3(0.0f, -90.0f, 0.0f), new Vector3(-90.0f, 0.0f, 0.0f), new Vector3(90.0f, 0.0f, 0.0f), new Vector3(0.0f, 0.0f, 0.0f), new Vector3(0.0f, 180.0f, 0.0f) }; + + // Backup states + RenderTexture backupRenderTex = RenderTexture.active; + float backupFieldOfView = ownerCamera.fieldOfView; + float backupAspect = ownerCamera.aspect; + Quaternion backupRot = ownerCamera.transform.rotation; + //RenderTexture backupRT = ownerCamera.targetTexture; + + // Enable 8X MSAA + RenderTexture faceTexture = new RenderTexture(width, height, 24); + faceTexture.antiAliasing = 8; #if UNITY_5_4_OR_NEWER faceTexture.dimension = UnityEngine.Rendering.TextureDimension.Tex2D; #endif - faceTexture.hideFlags = HideFlags.HideAndDontSave; - - // For intermediate saving - Texture2D swapTex = new Texture2D(width, height, TextureFormat.RGB24, false); - swapTex.hideFlags = HideFlags.HideAndDontSave; - - // Capture 6 Directions - ownerCamera.targetTexture = faceTexture; - ownerCamera.fieldOfView = 90; - ownerCamera.aspect = 1.0f; - - Color[] mirroredPixels = new Color[swapTex.height * swapTex.width]; - for (int i = 0; i < faces.Length; i++) - { - ownerCamera.transform.eulerAngles = faceAngles[i]; - ownerCamera.Render(); - RenderTexture.active = faceTexture; - swapTex.ReadPixels(new Rect(0, 0, width, height), 0, 0); - - // Mirror vertically to meet the standard of unity cubemap - Color[] OrignalPixels = swapTex.GetPixels(); - for (int y1 = 0; y1 < height; y1++) - { - for (int x1 = 0; x1 < width; x1++) - { - mirroredPixels[y1 * width + x1] = OrignalPixels[((height - 1 - y1) * width) + x1]; - } - }; - outCubemap.SetPixels(mirroredPixels, faces[i]); - } - - outCubemap.SmoothEdges(); - - // Restore states - RenderTexture.active = backupRenderTex; - ownerCamera.fieldOfView = backupFieldOfView; - ownerCamera.aspect = backupAspect; - ownerCamera.transform.rotation = backupRot; - ownerCamera.targetTexture = backupRenderTex; - - DestroyImmediate(swapTex); - DestroyImmediate(faceTexture); - - } - - - /// - /// Save unity cubemap into NPOT 6x1 cubemap/texture atlas in the following format PX NX PY NY PZ NZ - /// - /// - /// Supported format: PNG/JPG - /// Default file name: using current time OVR_hh_mm_ss.png - /// - - public static bool SaveCubemapCapture(Cubemap cubemap, string pathName = null) - { - string fileName; - string dirName; - int width = cubemap.width; - int height = cubemap.height; - int x = 0; - int y = 0; - bool saveToPNG = true; - - if (string.IsNullOrEmpty(pathName)) - { - dirName = Application.persistentDataPath + "/OVR_ScreenShot360/"; - fileName = null; - } - else - { - dirName = Path.GetDirectoryName(pathName); - fileName = Path.GetFileName(pathName); - - if (dirName[dirName.Length - 1] != '/' || dirName[dirName.Length - 1] != '\\') - dirName += "/"; - } - - if (string.IsNullOrEmpty(fileName)) - fileName = "OVR_" + System.DateTime.Now.ToString("hh_mm_ss") + ".png"; - - string extName = Path.GetExtension(fileName); - if (extName == ".png") - { - saveToPNG = true; - } - else if (extName == ".jpg") - { - saveToPNG = false; - } - else - { + faceTexture.hideFlags = HideFlags.HideAndDontSave; + + // For intermediate saving + Texture2D swapTex = new Texture2D(width, height, TextureFormat.RGB24, false); + swapTex.hideFlags = HideFlags.HideAndDontSave; + + // Capture 6 Directions + ownerCamera.targetTexture = faceTexture; + ownerCamera.fieldOfView = 90; + ownerCamera.aspect = 1.0f; + + Color[] mirroredPixels = new Color[swapTex.height * swapTex.width]; + for (int i = 0; i < faces.Length; i++) + { + ownerCamera.transform.eulerAngles = faceAngles[i]; + ownerCamera.Render(); + RenderTexture.active = faceTexture; + swapTex.ReadPixels(new Rect(0, 0, width, height), 0, 0); + + // Mirror vertically to meet the standard of unity cubemap + Color[] OrignalPixels = swapTex.GetPixels(); + for (int y1 = 0; y1 < height; y1++) + { + for (int x1 = 0; x1 < width; x1++) + { + mirroredPixels[y1 * width + x1] = OrignalPixels[((height - 1 - y1) * width) + x1]; + } + }; + outCubemap.SetPixels(mirroredPixels, faces[i]); + } + + outCubemap.SmoothEdges(); + + // Restore states + RenderTexture.active = backupRenderTex; + ownerCamera.fieldOfView = backupFieldOfView; + ownerCamera.aspect = backupAspect; + ownerCamera.transform.rotation = backupRot; + ownerCamera.targetTexture = backupRenderTex; + + DestroyImmediate(swapTex); + DestroyImmediate(faceTexture); + + } + + + /// + /// Save unity cubemap into NPOT 6x1 cubemap/texture atlas in the following format PX NX PY NY PZ NZ + /// + /// + /// Supported format: PNG/JPG + /// Default file name: using current time OVR_hh_mm_ss.png + /// + + public static bool SaveCubemapCapture(Cubemap cubemap, string pathName = null) + { + string fileName; + string dirName; + int width = cubemap.width; + int height = cubemap.height; + int x = 0; + int y = 0; + bool saveToPNG = true; + + if (string.IsNullOrEmpty(pathName)) + { + dirName = Application.persistentDataPath + "/OVR_ScreenShot360/"; + fileName = null; + } + else + { + dirName = Path.GetDirectoryName(pathName); + fileName = Path.GetFileName(pathName); + + if (dirName[dirName.Length - 1] != '/' || dirName[dirName.Length - 1] != '\\') + dirName += "/"; + } + + if (string.IsNullOrEmpty(fileName)) + fileName = "OVR_" + System.DateTime.Now.ToString("hh_mm_ss") + ".png"; + + string extName = Path.GetExtension(fileName); + if (extName == ".png") + { + saveToPNG = true; + } + else if (extName == ".jpg") + { + saveToPNG = false; + } + else + { Debug.LogError("Unsupported file format" + extName); - return false; - } - - // Validate path - try - { - System.IO.Directory.CreateDirectory(dirName); - } - catch (System.Exception e) - { + return false; + } + + // Validate path + try + { + System.IO.Directory.CreateDirectory(dirName); + } + catch (System.Exception e) + { Debug.LogError("Failed to create path " + dirName + " since " + e.ToString()); - return false; - } - - - // Create the new texture - Texture2D tex = new Texture2D(width * 6, height, TextureFormat.RGB24, false); - if (tex == null) - { - Debug.LogError("[OVRScreenshotWizard] Failed creating the texture!"); - return false; - } - - // Merge all the cubemap faces into the texture - // Reference cubemap format: http://docs.unity3d.com/Manual/class-Cubemap.html - CubemapFace[] faces = new CubemapFace[] { CubemapFace.PositiveX, CubemapFace.NegativeX, CubemapFace.PositiveY, CubemapFace.NegativeY, CubemapFace.PositiveZ, CubemapFace.NegativeZ }; - for (int i = 0; i < faces.Length; i++) - { - // get the pixels from the cubemap - Color[] srcPixels = null; - Color[] pixels = cubemap.GetPixels(faces[i]); - // if desired, flip them as they are ordered left to right, bottom to top - srcPixels = new Color[pixels.Length]; - for (int y1 = 0; y1 < height; y1++) - { - for (int x1 = 0; x1 < width; x1++) - { - srcPixels[y1 * width + x1] = pixels[((height - 1 - y1) * width) + x1]; - } - } - // Copy them to the dest texture - tex.SetPixels(x, y, width, height, srcPixels); - x += width; - } + return false; + } + + + // Create the new texture + Texture2D tex = new Texture2D(width * 6, height, TextureFormat.RGB24, false); + if (tex == null) + { + Debug.LogError("[OVRScreenshotWizard] Failed creating the texture!"); + return false; + } + + // Merge all the cubemap faces into the texture + // Reference cubemap format: http://docs.unity3d.com/Manual/class-Cubemap.html + CubemapFace[] faces = new CubemapFace[] { CubemapFace.PositiveX, CubemapFace.NegativeX, CubemapFace.PositiveY, CubemapFace.NegativeY, CubemapFace.PositiveZ, CubemapFace.NegativeZ }; + for (int i = 0; i < faces.Length; i++) + { + // get the pixels from the cubemap + Color[] srcPixels = null; + Color[] pixels = cubemap.GetPixels(faces[i]); + // if desired, flip them as they are ordered left to right, bottom to top + srcPixels = new Color[pixels.Length]; + for (int y1 = 0; y1 < height; y1++) + { + for (int x1 = 0; x1 < width; x1++) + { + srcPixels[y1 * width + x1] = pixels[((height - 1 - y1) * width) + x1]; + } + } + // Copy them to the dest texture + tex.SetPixels(x, y, width, height, srcPixels); + x += width; + } try { @@ -259,12 +259,12 @@ public static bool SaveCubemapCapture(Cubemap cubemap, string pathName = null) catch (System.Exception e) { Debug.LogError("Failed to save cubemap file since " + e.ToString()); - return false; + return false; } - DestroyImmediate(tex); - return true; - } + DestroyImmediate(tex); + return true; + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRDebugInfo.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRDebugInfo.cs index d9bf784b..b30eeb57 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRDebugInfo.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRDebugInfo.cs @@ -33,41 +33,41 @@ public class OVRDebugInfo : MonoBehaviour #region GameObjects for Debug Information UIs GameObject debugUIManager; GameObject debugUIObject; - GameObject riftPresent; - GameObject fps; + GameObject riftPresent; + GameObject fps; GameObject ipd; GameObject fov; GameObject height; - GameObject depth; - GameObject resolutionEyeTexture; + GameObject depth; + GameObject resolutionEyeTexture; GameObject latencies; - GameObject texts; + GameObject texts; #endregion #region Debug strings - string strRiftPresent = null; // "VR DISABLED" - string strFPS = null; // "FPS: 0"; - string strIPD = null; // "IPD: 0.000"; - string strFOV = null; // "FOV: 0.0f"; - string strHeight = null; // "Height: 0.0f"; - string strDepth = null; // "Depth: 0.0f"; - string strResolutionEyeTexture = null; // "Resolution : {0} x {1}" - string strLatencies = null; // "R: {0:F3} TW: {1:F3} PP: {2:F3} RE: {3:F3} TWE: {4:F3}" + string strRiftPresent = null; // "VR DISABLED" + string strFPS = null; // "FPS: 0"; + string strIPD = null; // "IPD: 0.000"; + string strFOV = null; // "FOV: 0.0f"; + string strHeight = null; // "Height: 0.0f"; + string strDepth = null; // "Depth: 0.0f"; + string strResolutionEyeTexture = null; // "Resolution : {0} x {1}" + string strLatencies = null; // "R: {0:F3} TW: {1:F3} PP: {2:F3} RE: {3:F3} TWE: {4:F3}" #endregion /// /// Variables for FPS /// float updateInterval = 0.5f; - float accum = 0.0f; - int frames = 0; - float timeLeft = 0.0f; + float accum = 0.0f; + int frames = 0; + float timeLeft = 0.0f; /// /// Managing for UI initialization /// - bool initUIComponent = false; - bool isInited = false; + bool initUIComponent = false; + bool isInited = false; /// /// UIs Y offset @@ -130,7 +130,7 @@ void Update() { debugUIManager.SetActive(true); UpdateVariable(); - UpdateStrings(); + UpdateStrings(); } else { @@ -186,14 +186,14 @@ void InitUIComponents() { height = VariableObjectManager(height, "Height", posY -= offsetY, strHeight, fontSize); } - - // Print out for Depth - if (!string.IsNullOrEmpty(strDepth)) - { - depth = VariableObjectManager(depth, "Depth", posY -= offsetY, strDepth, fontSize); - } - - // Print out for Resoulution of Eye Texture + + // Print out for Depth + if (!string.IsNullOrEmpty(strDepth)) + { + depth = VariableObjectManager(depth, "Depth", posY -= offsetY, strDepth, fontSize); + } + + // Print out for Resoulution of Eye Texture if (!string.IsNullOrEmpty(strResolutionEyeTexture)) { resolutionEyeTexture = VariableObjectManager(resolutionEyeTexture, "Resolution", posY -= offsetY, strResolutionEyeTexture, fontSize); @@ -215,14 +215,14 @@ void InitUIComponents() /// Update VR Variables /// void UpdateVariable() - { + { UpdateIPD(); UpdateEyeHeightOffset(); - UpdateEyeDepthOffset(); - UpdateFOV(); + UpdateEyeDepthOffset(); + UpdateFOV(); UpdateResolutionEyeTexture(); UpdateLatencyValues(); - UpdateFPS(); + UpdateFPS(); } /// @@ -231,8 +231,8 @@ void UpdateVariable() void UpdateStrings() { if (debugUIObject == null) - return; - + return; + if (!string.IsNullOrEmpty(strFPS)) fps.GetComponentInChildren().text = strFPS; if (!string.IsNullOrEmpty(strIPD)) @@ -242,17 +242,17 @@ void UpdateStrings() if (!string.IsNullOrEmpty(strResolutionEyeTexture)) resolutionEyeTexture.GetComponentInChildren().text = strResolutionEyeTexture; if (!string.IsNullOrEmpty(strLatencies)) - { + { latencies.GetComponentInChildren().text = strLatencies; - latencies.GetComponentInChildren().fontSize = 14; - } + latencies.GetComponentInChildren().fontSize = 14; + } if (!string.IsNullOrEmpty(strHeight)) height.GetComponentInChildren().text = strHeight; - if (!string.IsNullOrEmpty(strDepth)) - depth.GetComponentInChildren().text = strDepth; - } - - /// + if (!string.IsNullOrEmpty(strDepth)) + depth.GetComponentInChildren().text = strDepth; + } + + /// /// It's for rift present GUI /// void RiftPresentGUI(GameObject guiMainOBj) @@ -322,7 +322,7 @@ GameObject ComponentComposition(GameObject GO) texts.AddComponent(); texts.AddComponent(); texts.GetComponent().sizeDelta = new Vector2(350f, 50f); - texts.GetComponent().font = Resources.GetBuiltinResource(typeof(Font), "Arial.ttf") as Font; + texts.GetComponent().font = Resources.GetBuiltinResource(typeof(Font), "Arial.ttf") as Font; texts.GetComponent().alignment = TextAnchor.MiddleCenter; texts.transform.SetParent(GO.transform); @@ -348,24 +348,24 @@ void UpdateEyeHeightOffset() { float eyeHeight = OVRManager.profile.eyeHeight; strHeight = System.String.Format("Eye Height (m): {0:F3}", eyeHeight); - } - - /// - /// Updates the eye depth offset. - /// - void UpdateEyeDepthOffset() - { - float eyeDepth = OVRManager.profile.eyeDepth; - strDepth = System.String.Format("Eye Depth (m): {0:F3}", eyeDepth); - } - - /// - /// Updates the FOV. + } + + /// + /// Updates the eye depth offset. + /// + void UpdateEyeDepthOffset() + { + float eyeDepth = OVRManager.profile.eyeDepth; + strDepth = System.String.Format("Eye Depth (m): {0:F3}", eyeDepth); + } + + /// + /// Updates the FOV. /// void UpdateFOV() { OVRDisplay.EyeRenderDesc eyeDesc = OVRManager.display.GetEyeRenderDesc(UnityEngine.XR.XRNode.LeftEye); - strFOV = System.String.Format("FOV (deg): {0:F3}", eyeDesc.fov.y); + strFOV = System.String.Format("FOV (deg): {0:F3}", eyeDesc.fov.y); } /// @@ -373,10 +373,10 @@ void UpdateFOV() /// void UpdateResolutionEyeTexture() { - OVRDisplay.EyeRenderDesc leftEyeDesc = OVRManager.display.GetEyeRenderDesc(UnityEngine.XR.XRNode.LeftEye); - OVRDisplay.EyeRenderDesc rightEyeDesc = OVRManager.display.GetEyeRenderDesc(UnityEngine.XR.XRNode.RightEye); + OVRDisplay.EyeRenderDesc leftEyeDesc = OVRManager.display.GetEyeRenderDesc(UnityEngine.XR.XRNode.LeftEye); + OVRDisplay.EyeRenderDesc rightEyeDesc = OVRManager.display.GetEyeRenderDesc(UnityEngine.XR.XRNode.RightEye); - float scale = UnityEngine.XR.XRSettings.eyeTextureResolutionScale; + float scale = UnityEngine.XR.XRSettings.eyeTextureResolutionScale; float w = (int)(scale * (float)(leftEyeDesc.resolution.x + rightEyeDesc.resolution.x)); float h = (int)(scale * (float)Mathf.Max(leftEyeDesc.resolution.y, rightEyeDesc.resolution.y)); @@ -389,16 +389,16 @@ void UpdateResolutionEyeTexture() void UpdateLatencyValues() { #if !UNITY_ANDROID || UNITY_EDITOR - OVRDisplay.LatencyData latency = OVRManager.display.latency; - if (latency.render < 0.000001f && latency.timeWarp < 0.000001f && latency.postPresent < 0.000001f) - strLatencies = System.String.Format("Latency values are not available."); - else - strLatencies = System.String.Format("Render: {0:F3} TimeWarp: {1:F3} Post-Present: {2:F3}\nRender Error: {3:F3} TimeWarp Error: {4:F3}", - latency.render, - latency.timeWarp, - latency.postPresent, - latency.renderError, - latency.timeWarpError); + OVRDisplay.LatencyData latency = OVRManager.display.latency; + if (latency.render < 0.000001f && latency.timeWarp < 0.000001f && latency.postPresent < 0.000001f) + strLatencies = System.String.Format("Latency values are not available."); + else + strLatencies = System.String.Format("Render: {0:F3} TimeWarp: {1:F3} Post-Present: {2:F3}\nRender Error: {3:F3} TimeWarp Error: {4:F3}", + latency.render, + latency.timeWarp, + latency.postPresent, + latency.renderError, + latency.timeWarpError); #endif } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGearVrController.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGearVrController.cs index ac27bfd0..c641d995 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGearVrController.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGearVrController.cs @@ -27,14 +27,14 @@ limitations under the License. /// public class OVRGearVrController : MonoBehaviour { - /// - /// The root GameObject that should be conditionally enabled depending on controller connection status. - /// + /// + /// The root GameObject that should be conditionally enabled depending on controller connection status. + /// public GameObject m_model; - /// - /// The controller that determines whether or not to enable rendering of the controller model. - /// + /// + /// The controller that determines whether or not to enable rendering of the controller model. + /// public OVRInput.Controller m_controller; private bool m_prevControllerConnected = false; diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGearVrControllerTest.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGearVrControllerTest.cs index dc292b04..10433ecb 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGearVrControllerTest.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGearVrControllerTest.cs @@ -27,160 +27,160 @@ limitations under the License. public class OVRGearVrControllerTest : MonoBehaviour { - public class BoolMonitor - { - public delegate bool BoolGenerator(); - - private string m_name = ""; - private BoolGenerator m_generator; - private bool m_prevValue = false; - private bool m_currentValue = false; - private bool m_currentValueRecentlyChanged = false; - private float m_displayTimeout = 0.0f; - private float m_displayTimer = 0.0f; - - public BoolMonitor(string name, BoolGenerator generator, float displayTimeout = 0.5f) - { - m_name = name; - m_generator = generator; - m_displayTimeout = displayTimeout; - } - - public void Update() - { - m_prevValue = m_currentValue; - m_currentValue = m_generator(); - - if (m_currentValue != m_prevValue) - { - m_currentValueRecentlyChanged = true; - m_displayTimer = m_displayTimeout; - } - - if (m_displayTimer > 0.0f) - { - m_displayTimer -= Time.deltaTime; - - if (m_displayTimer <= 0.0f) - { - m_currentValueRecentlyChanged = false; - m_displayTimer = 0.0f; - } - } - } - - public void AppendToStringBuilder(ref StringBuilder sb) - { - sb.Append(m_name); - - if (m_currentValue && m_currentValueRecentlyChanged) - sb.Append(": *True*\n"); - else if (m_currentValue) - sb.Append(": True \n"); - else if (!m_currentValue && m_currentValueRecentlyChanged) - sb.Append(": *False*\n"); - else if (!m_currentValue) - sb.Append(": False \n"); - } - } - - public Text uiText; - private List monitors; - private StringBuilder data; - - void Start() - { - if (uiText != null) - { - uiText.supportRichText = false; - } - - data = new StringBuilder(2048); - - monitors = new List() - { + public class BoolMonitor + { + public delegate bool BoolGenerator(); + + private string m_name = ""; + private BoolGenerator m_generator; + private bool m_prevValue = false; + private bool m_currentValue = false; + private bool m_currentValueRecentlyChanged = false; + private float m_displayTimeout = 0.0f; + private float m_displayTimer = 0.0f; + + public BoolMonitor(string name, BoolGenerator generator, float displayTimeout = 0.5f) + { + m_name = name; + m_generator = generator; + m_displayTimeout = displayTimeout; + } + + public void Update() + { + m_prevValue = m_currentValue; + m_currentValue = m_generator(); + + if (m_currentValue != m_prevValue) + { + m_currentValueRecentlyChanged = true; + m_displayTimer = m_displayTimeout; + } + + if (m_displayTimer > 0.0f) + { + m_displayTimer -= Time.deltaTime; + + if (m_displayTimer <= 0.0f) + { + m_currentValueRecentlyChanged = false; + m_displayTimer = 0.0f; + } + } + } + + public void AppendToStringBuilder(ref StringBuilder sb) + { + sb.Append(m_name); + + if (m_currentValue && m_currentValueRecentlyChanged) + sb.Append(": *True*\n"); + else if (m_currentValue) + sb.Append(": True \n"); + else if (!m_currentValue && m_currentValueRecentlyChanged) + sb.Append(": *False*\n"); + else if (!m_currentValue) + sb.Append(": False \n"); + } + } + + public Text uiText; + private List monitors; + private StringBuilder data; + + void Start() + { + if (uiText != null) + { + uiText.supportRichText = false; + } + + data = new StringBuilder(2048); + + monitors = new List() + { // virtual new BoolMonitor("One", () => OVRInput.Get(OVRInput.Button.One)), - new BoolMonitor("OneDown", () => OVRInput.GetDown(OVRInput.Button.One)), - new BoolMonitor("OneUp", () => OVRInput.GetUp(OVRInput.Button.One)), - new BoolMonitor("Two", () => OVRInput.Get(OVRInput.Button.Two)), - new BoolMonitor("TwoDown", () => OVRInput.GetDown(OVRInput.Button.Two)), - new BoolMonitor("TwoUp", () => OVRInput.GetUp(OVRInput.Button.Two)), - new BoolMonitor("PrimaryIndexTrigger", () => OVRInput.Get(OVRInput.Button.PrimaryIndexTrigger)), - new BoolMonitor("PrimaryIndexTriggerDown", () => OVRInput.GetDown(OVRInput.Button.PrimaryIndexTrigger)), - new BoolMonitor("PrimaryIndexTriggerUp", () => OVRInput.GetUp(OVRInput.Button.PrimaryIndexTrigger)), - new BoolMonitor("Up", () => OVRInput.Get(OVRInput.Button.Up)), - new BoolMonitor("Down", () => OVRInput.Get(OVRInput.Button.Down)), - new BoolMonitor("Left", () => OVRInput.Get(OVRInput.Button.Left)), - new BoolMonitor("Right", () => OVRInput.Get(OVRInput.Button.Right)), - new BoolMonitor("Touchpad (Touch)", () => OVRInput.Get(OVRInput.Touch.PrimaryTouchpad)), - new BoolMonitor("TouchpadDown (Touch)", () => OVRInput.GetDown(OVRInput.Touch.PrimaryTouchpad)), - new BoolMonitor("TouchpadUp (Touch)", () => OVRInput.GetUp(OVRInput.Touch.PrimaryTouchpad)), - new BoolMonitor("Touchpad (Click)", () => OVRInput.Get(OVRInput.Button.PrimaryTouchpad)), - new BoolMonitor("TouchpadDown (Click)", () => OVRInput.GetDown(OVRInput.Button.PrimaryTouchpad)), - new BoolMonitor("TouchpadUp (Click)", () => OVRInput.GetUp(OVRInput.Button.PrimaryTouchpad)), + new BoolMonitor("OneDown", () => OVRInput.GetDown(OVRInput.Button.One)), + new BoolMonitor("OneUp", () => OVRInput.GetUp(OVRInput.Button.One)), + new BoolMonitor("Two", () => OVRInput.Get(OVRInput.Button.Two)), + new BoolMonitor("TwoDown", () => OVRInput.GetDown(OVRInput.Button.Two)), + new BoolMonitor("TwoUp", () => OVRInput.GetUp(OVRInput.Button.Two)), + new BoolMonitor("PrimaryIndexTrigger", () => OVRInput.Get(OVRInput.Button.PrimaryIndexTrigger)), + new BoolMonitor("PrimaryIndexTriggerDown", () => OVRInput.GetDown(OVRInput.Button.PrimaryIndexTrigger)), + new BoolMonitor("PrimaryIndexTriggerUp", () => OVRInput.GetUp(OVRInput.Button.PrimaryIndexTrigger)), + new BoolMonitor("Up", () => OVRInput.Get(OVRInput.Button.Up)), + new BoolMonitor("Down", () => OVRInput.Get(OVRInput.Button.Down)), + new BoolMonitor("Left", () => OVRInput.Get(OVRInput.Button.Left)), + new BoolMonitor("Right", () => OVRInput.Get(OVRInput.Button.Right)), + new BoolMonitor("Touchpad (Touch)", () => OVRInput.Get(OVRInput.Touch.PrimaryTouchpad)), + new BoolMonitor("TouchpadDown (Touch)", () => OVRInput.GetDown(OVRInput.Touch.PrimaryTouchpad)), + new BoolMonitor("TouchpadUp (Touch)", () => OVRInput.GetUp(OVRInput.Touch.PrimaryTouchpad)), + new BoolMonitor("Touchpad (Click)", () => OVRInput.Get(OVRInput.Button.PrimaryTouchpad)), + new BoolMonitor("TouchpadDown (Click)", () => OVRInput.GetDown(OVRInput.Button.PrimaryTouchpad)), + new BoolMonitor("TouchpadUp (Click)", () => OVRInput.GetUp(OVRInput.Button.PrimaryTouchpad)), // raw new BoolMonitor("Start", () => OVRInput.Get(OVRInput.RawButton.Start)), - new BoolMonitor("StartDown", () => OVRInput.GetDown(OVRInput.RawButton.Start)), - new BoolMonitor("StartUp", () => OVRInput.GetUp(OVRInput.RawButton.Start)), - new BoolMonitor("Back", () => OVRInput.Get(OVRInput.RawButton.Back)), - new BoolMonitor("BackDown", () => OVRInput.GetDown(OVRInput.RawButton.Back)), - new BoolMonitor("BackUp", () => OVRInput.GetUp(OVRInput.RawButton.Back)), - new BoolMonitor("A", () => OVRInput.Get(OVRInput.RawButton.A)), - new BoolMonitor("ADown", () => OVRInput.GetDown(OVRInput.RawButton.A)), - new BoolMonitor("AUp", () => OVRInput.GetUp(OVRInput.RawButton.A)), - }; - } - - void Update() - { - OVRInput.Controller activeController = OVRInput.GetActiveController(); - - data.Length = 0; - - float framerate = OVRPlugin.GetAppFramerate(); - data.AppendFormat("Framerate: {0:F2}\n", framerate); - - string activeControllerName = activeController.ToString(); - data.AppendFormat("Active: {0}\n", activeControllerName); - - string connectedControllerNames = OVRInput.GetConnectedControllers().ToString(); - data.AppendFormat("Connected: {0}\n", connectedControllerNames); - - Quaternion rot = OVRInput.GetLocalControllerRotation(activeController); - data.AppendFormat("Orientation: ({0:F2}, {1:F2}, {2:F2}, {3:F2})\n", rot.x, rot.y, rot.z, rot.w); - - Vector3 angVel = OVRInput.GetLocalControllerAngularVelocity(activeController); - data.AppendFormat("AngVel: ({0:F2}, {1:F2}, {2:F2})\n", angVel.x, angVel.y, angVel.z); - - Vector3 angAcc = OVRInput.GetLocalControllerAngularAcceleration(activeController); - data.AppendFormat("AngAcc: ({0:F2}, {1:F2}, {2:F2})\n", angAcc.x, angAcc.y, angAcc.z); - - Vector3 pos = OVRInput.GetLocalControllerPosition(activeController); - data.AppendFormat("Position: ({0:F2}, {1:F2}, {2:F2})\n", pos.x, pos.y, pos.z); - - Vector3 vel = OVRInput.GetLocalControllerVelocity(activeController); - data.AppendFormat("Vel: ({0:F2}, {1:F2}, {2:F2})\n", vel.x, vel.y, vel.z); - - Vector3 acc = OVRInput.GetLocalControllerAcceleration(activeController); - data.AppendFormat("Acc: ({0:F2}, {1:F2}, {2:F2})\n", acc.x, acc.y, acc.z); - - Vector2 primaryTouchpad = OVRInput.Get(OVRInput.Axis2D.PrimaryTouchpad); - data.AppendFormat("PrimaryTouchpad: ({0:F2}, {1:F2})\n", primaryTouchpad.x, primaryTouchpad.y); - - Vector2 secondaryTouchpad = OVRInput.Get(OVRInput.Axis2D.SecondaryTouchpad); - data.AppendFormat("SecondaryTouchpad: ({0:F2}, {1:F2})\n", secondaryTouchpad.x, secondaryTouchpad.y); - - for (int i = 0; i < monitors.Count; i++) - { - monitors[i].Update(); - monitors[i].AppendToStringBuilder(ref data); - } - - if (uiText != null) - { - uiText.text = data.ToString(); - } - } + new BoolMonitor("StartDown", () => OVRInput.GetDown(OVRInput.RawButton.Start)), + new BoolMonitor("StartUp", () => OVRInput.GetUp(OVRInput.RawButton.Start)), + new BoolMonitor("Back", () => OVRInput.Get(OVRInput.RawButton.Back)), + new BoolMonitor("BackDown", () => OVRInput.GetDown(OVRInput.RawButton.Back)), + new BoolMonitor("BackUp", () => OVRInput.GetUp(OVRInput.RawButton.Back)), + new BoolMonitor("A", () => OVRInput.Get(OVRInput.RawButton.A)), + new BoolMonitor("ADown", () => OVRInput.GetDown(OVRInput.RawButton.A)), + new BoolMonitor("AUp", () => OVRInput.GetUp(OVRInput.RawButton.A)), + }; + } + + void Update() + { + OVRInput.Controller activeController = OVRInput.GetActiveController(); + + data.Length = 0; + + float framerate = OVRPlugin.GetAppFramerate(); + data.AppendFormat("Framerate: {0:F2}\n", framerate); + + string activeControllerName = activeController.ToString(); + data.AppendFormat("Active: {0}\n", activeControllerName); + + string connectedControllerNames = OVRInput.GetConnectedControllers().ToString(); + data.AppendFormat("Connected: {0}\n", connectedControllerNames); + + Quaternion rot = OVRInput.GetLocalControllerRotation(activeController); + data.AppendFormat("Orientation: ({0:F2}, {1:F2}, {2:F2}, {3:F2})\n", rot.x, rot.y, rot.z, rot.w); + + Vector3 angVel = OVRInput.GetLocalControllerAngularVelocity(activeController); + data.AppendFormat("AngVel: ({0:F2}, {1:F2}, {2:F2})\n", angVel.x, angVel.y, angVel.z); + + Vector3 angAcc = OVRInput.GetLocalControllerAngularAcceleration(activeController); + data.AppendFormat("AngAcc: ({0:F2}, {1:F2}, {2:F2})\n", angAcc.x, angAcc.y, angAcc.z); + + Vector3 pos = OVRInput.GetLocalControllerPosition(activeController); + data.AppendFormat("Position: ({0:F2}, {1:F2}, {2:F2})\n", pos.x, pos.y, pos.z); + + Vector3 vel = OVRInput.GetLocalControllerVelocity(activeController); + data.AppendFormat("Vel: ({0:F2}, {1:F2}, {2:F2})\n", vel.x, vel.y, vel.z); + + Vector3 acc = OVRInput.GetLocalControllerAcceleration(activeController); + data.AppendFormat("Acc: ({0:F2}, {1:F2}, {2:F2})\n", acc.x, acc.y, acc.z); + + Vector2 primaryTouchpad = OVRInput.Get(OVRInput.Axis2D.PrimaryTouchpad); + data.AppendFormat("PrimaryTouchpad: ({0:F2}, {1:F2})\n", primaryTouchpad.x, primaryTouchpad.y); + + Vector2 secondaryTouchpad = OVRInput.Get(OVRInput.Axis2D.SecondaryTouchpad); + data.AppendFormat("SecondaryTouchpad: ({0:F2}, {1:F2})\n", secondaryTouchpad.x, secondaryTouchpad.y); + + for (int i = 0; i < monitors.Count; i++) + { + monitors[i].Update(); + monitors[i].AppendToStringBuilder(ref data); + } + + if (uiText != null) + { + uiText.text = data.ToString(); + } + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGrabbable.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGrabbable.cs index ff5a621e..e5afb1a0 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGrabbable.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGrabbable.cs @@ -42,92 +42,92 @@ public class OVRGrabbable : MonoBehaviour protected Collider m_grabbedCollider = null; protected OVRGrabber m_grabbedBy = null; - /// - /// If true, the object can currently be grabbed. - /// + /// + /// If true, the object can currently be grabbed. + /// public bool allowOffhandGrab { get { return m_allowOffhandGrab; } } - /// - /// If true, the object is currently grabbed. - /// + /// + /// If true, the object is currently grabbed. + /// public bool isGrabbed { get { return m_grabbedBy != null; } } - /// - /// If true, the object's position will snap to match snapOffset when grabbed. - /// + /// + /// If true, the object's position will snap to match snapOffset when grabbed. + /// public bool snapPosition { get { return m_snapPosition; } } - /// - /// If true, the object's orientation will snap to match snapOffset when grabbed. - /// + /// + /// If true, the object's orientation will snap to match snapOffset when grabbed. + /// public bool snapOrientation { get { return m_snapOrientation; } } - /// - /// An offset relative to the OVRGrabber where this object can snap when grabbed. - /// + /// + /// An offset relative to the OVRGrabber where this object can snap when grabbed. + /// public Transform snapOffset { get { return m_snapOffset; } } - /// - /// Returns the OVRGrabber currently grabbing this object. - /// + /// + /// Returns the OVRGrabber currently grabbing this object. + /// public OVRGrabber grabbedBy { get { return m_grabbedBy; } } - /// - /// The transform at which this object was grabbed. - /// + /// + /// The transform at which this object was grabbed. + /// public Transform grabbedTransform { get { return m_grabbedCollider.transform; } } - /// - /// The Rigidbody of the collider that was used to grab this object. - /// + /// + /// The Rigidbody of the collider that was used to grab this object. + /// public Rigidbody grabbedRigidbody { get { return m_grabbedCollider.attachedRigidbody; } } - /// - /// The contact point(s) where the object was grabbed. - /// + /// + /// The contact point(s) where the object was grabbed. + /// public Collider[] grabPoints { get { return m_grabPoints; } } - /// - /// Notifies the object that it has been grabbed. - /// - virtual public void GrabBegin(OVRGrabber hand, Collider grabPoint) + /// + /// Notifies the object that it has been grabbed. + /// + virtual public void GrabBegin(OVRGrabber hand, Collider grabPoint) { m_grabbedBy = hand; m_grabbedCollider = grabPoint; gameObject.GetComponent().isKinematic = true; } - /// - /// Notifies the object that it has been released. - /// - virtual public void GrabEnd(Vector3 linearVelocity, Vector3 angularVelocity) + /// + /// Notifies the object that it has been released. + /// + virtual public void GrabEnd(Vector3 linearVelocity, Vector3 angularVelocity) { Rigidbody rb = gameObject.GetComponent(); rb.isKinematic = m_grabbedKinematic; @@ -145,7 +145,7 @@ void Awake() Collider collider = this.GetComponent(); if (collider == null) { - throw new ArgumentException("Grabbables cannot have zero grab points and no collider -- please add a grab point or collider."); + throw new ArgumentException("Grabbables cannot have zero grab points and no collider -- please add a grab point or collider."); } // Create a default grab point diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGrabber.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGrabber.cs index 203b0013..473ae55e 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGrabber.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGrabber.cs @@ -61,11 +61,11 @@ public class OVRGrabber : MonoBehaviour protected Quaternion m_anchorOffsetRotation; protected Vector3 m_anchorOffsetPosition; protected float m_prevFlex; - protected OVRGrabbable m_grabbedObj = null; + protected OVRGrabbable m_grabbedObj = null; Vector3 m_grabbedObjectPosOff; Quaternion m_grabbedObjectRotOff; - protected Dictionary m_grabCandidates = new Dictionary(); - protected bool operatingWithoutOVRCameraRig = true; + protected Dictionary m_grabCandidates = new Dictionary(); + protected bool operatingWithoutOVRCameraRig = true; /// /// The currently grabbed object. @@ -75,7 +75,7 @@ public OVRGrabbable grabbedObject get { return m_grabbedObj; } } - public void ForceRelease(OVRGrabbable grabbable) + public void ForceRelease(OVRGrabbable grabbable) { bool canRelease = ( (m_grabbedObj != null) && @@ -92,26 +92,26 @@ void Awake() m_anchorOffsetPosition = transform.localPosition; m_anchorOffsetRotation = transform.localRotation; - // If we are being used with an OVRCameraRig, let it drive input updates, which may come from Update or FixedUpdate. - - OVRCameraRig rig = null; - if (transform.parent != null && transform.parent.parent != null) - rig = transform.parent.parent.GetComponent(); - - if (rig != null) - { - rig.UpdatedAnchors += (r) => {OnUpdatedAnchors();}; - operatingWithoutOVRCameraRig = false; - } + // If we are being used with an OVRCameraRig, let it drive input updates, which may come from Update or FixedUpdate. + + OVRCameraRig rig = null; + if (transform.parent != null && transform.parent.parent != null) + rig = transform.parent.parent.GetComponent(); + + if (rig != null) + { + rig.UpdatedAnchors += (r) => { OnUpdatedAnchors(); }; + operatingWithoutOVRCameraRig = false; + } } void Start() { m_lastPos = transform.position; m_lastRot = transform.rotation; - if(m_parentTransform == null) + if (m_parentTransform == null) { - if(gameObject.transform.parent != null) + if (gameObject.transform.parent != null) { m_parentTransform = gameObject.transform.parent.transform; } @@ -124,11 +124,11 @@ void Start() } } - void FixedUpdate() - { - if (operatingWithoutOVRCameraRig) - OnUpdatedAnchors(); - } + void FixedUpdate() + { + if (operatingWithoutOVRCameraRig) + OnUpdatedAnchors(); + } // Hands follow the touch anchors by calling MovePosition each frame to reach the anchor. // This is done instead of parenting to achieve workable physics. If you don't require physics on @@ -149,11 +149,11 @@ void OnUpdatedAnchors() m_lastPos = transform.position; m_lastRot = transform.rotation; - float prevFlex = m_prevFlex; - // Update values from inputs - m_prevFlex = OVRInput.Get(OVRInput.Axis1D.PrimaryHandTrigger, m_controller); + float prevFlex = m_prevFlex; + // Update values from inputs + m_prevFlex = OVRInput.Get(OVRInput.Axis1D.PrimaryHandTrigger, m_controller); - CheckForGrabOrRelease(prevFlex); + CheckForGrabOrRelease(prevFlex); } void OnDestroy() @@ -167,7 +167,7 @@ void OnDestroy() void OnTriggerEnter(Collider otherCollider) { // Get the grab trigger - OVRGrabbable grabbable = otherCollider.GetComponent() ?? otherCollider.GetComponentInParent(); + OVRGrabbable grabbable = otherCollider.GetComponent() ?? otherCollider.GetComponentInParent(); if (grabbable == null) return; // Add the grabbable @@ -178,7 +178,7 @@ void OnTriggerEnter(Collider otherCollider) void OnTriggerExit(Collider otherCollider) { - OVRGrabbable grabbable = otherCollider.GetComponent() ?? otherCollider.GetComponentInParent(); + OVRGrabbable grabbable = otherCollider.GetComponent() ?? otherCollider.GetComponentInParent(); if (grabbable == null) return; // Remove the grabbable @@ -214,11 +214,11 @@ protected void CheckForGrabOrRelease(float prevFlex) protected void GrabBegin() { float closestMagSq = float.MaxValue; - OVRGrabbable closestGrabbable = null; + OVRGrabbable closestGrabbable = null; Collider closestGrabbableCollider = null; // Iterate grab candidates and find the closest grabbable candidate - foreach (OVRGrabbable grabbable in m_grabCandidates.Keys) + foreach (OVRGrabbable grabbable in m_grabCandidates.Keys) { bool canGrab = !(grabbable.isGrabbed && !grabbable.allowOffhandGrab); if (!canGrab) @@ -258,10 +258,10 @@ protected void GrabBegin() m_lastRot = transform.rotation; // Set up offsets for grabbed object desired position relative to hand. - if(m_grabbedObj.snapPosition) + if (m_grabbedObj.snapPosition) { m_grabbedObjectPosOff = m_gripTransform.localPosition; - if(m_grabbedObj.snapOffset) + if (m_grabbedObj.snapOffset) { Vector3 snapOffset = m_grabbedObj.snapOffset.position; if (m_controller == OVRInput.Controller.LTouch) snapOffset.x = -snapOffset.x; @@ -278,7 +278,7 @@ protected void GrabBegin() if (m_grabbedObj.snapOrientation) { m_grabbedObjectRotOff = m_gripTransform.localRotation; - if(m_grabbedObj.snapOffset) + if (m_grabbedObj.snapOffset) { m_grabbedObjectRotOff = m_grabbedObj.snapOffset.rotation * m_grabbedObjectRotOff; } @@ -293,7 +293,7 @@ protected void GrabBegin() // speed and sends them flying. The grabbed object may still teleport inside of other objects, but fixing that // is beyond the scope of this demo. MoveGrabbedObject(m_lastPos, m_lastRot, true); - if(m_parentHeldObject) + if (m_parentHeldObject) { m_grabbedObj.transform.parent = transform; } @@ -327,13 +327,13 @@ protected void GrabEnd() { if (m_grabbedObj != null) { - OVRPose localPose = new OVRPose { position = OVRInput.GetLocalControllerPosition(m_controller), orientation = OVRInput.GetLocalControllerRotation(m_controller) }; + OVRPose localPose = new OVRPose { position = OVRInput.GetLocalControllerPosition(m_controller), orientation = OVRInput.GetLocalControllerRotation(m_controller) }; OVRPose offsetPose = new OVRPose { position = m_anchorOffsetPosition, orientation = m_anchorOffsetRotation }; localPose = localPose * offsetPose; - OVRPose trackingSpace = transform.ToOVRPose() * localPose.Inverse(); - Vector3 linearVelocity = trackingSpace.orientation * OVRInput.GetLocalControllerVelocity(m_controller); - Vector3 angularVelocity = trackingSpace.orientation * OVRInput.GetLocalControllerAngularVelocity(m_controller); + OVRPose trackingSpace = transform.ToOVRPose() * localPose.Inverse(); + Vector3 linearVelocity = trackingSpace.orientation * OVRInput.GetLocalControllerVelocity(m_controller); + Vector3 angularVelocity = trackingSpace.orientation * OVRInput.GetLocalControllerAngularVelocity(m_controller); GrabbableRelease(linearVelocity, angularVelocity); } @@ -345,7 +345,7 @@ protected void GrabEnd() protected void GrabbableRelease(Vector3 linearVelocity, Vector3 angularVelocity) { m_grabbedObj.GrabEnd(linearVelocity, angularVelocity); - if(m_parentHeldObject) m_grabbedObj.transform.parent = null; + if (m_parentHeldObject) m_grabbedObj.transform.parent = null; m_grabbedObj = null; } @@ -369,7 +369,7 @@ protected void GrabVolumeEnable(bool enabled) } } - protected void OffhandGrabbed(OVRGrabbable grabbable) + protected void OffhandGrabbed(OVRGrabbable grabbable) { if (m_grabbedObj == grabbable) { diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGridCube.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGridCube.cs index cee262ff..56907520 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGridCube.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRGridCube.cs @@ -28,168 +28,168 @@ limitations under the License. /// public class OVRGridCube : MonoBehaviour { - /// - /// The key that toggles the grid of cubes. - /// - public KeyCode GridKey = KeyCode.G; - - private GameObject CubeGrid = null; - - private bool CubeGridOn = false; - private bool CubeSwitchColorOld = false; - private bool CubeSwitchColor = false; - - private int gridSizeX = 6; - private int gridSizeY = 4; - private int gridSizeZ = 6; - private float gridScale = 0.3f; - private float cubeScale = 0.03f; - - // Handle to OVRCameraRig - private OVRCameraRig CameraController = null; - - /// - /// Update this instance. - /// - void Update () - { - UpdateCubeGrid(); - } - - /// - /// Sets the OVR camera controller. - /// - /// Camera controller. - public void SetOVRCameraController(ref OVRCameraRig cameraController) - { - CameraController = cameraController; - } - - void UpdateCubeGrid() - { - // Toggle the grid cube display on 'G' - if(Input.GetKeyDown(GridKey)) - { - if(CubeGridOn == false) - { - CubeGridOn = true; - Debug.LogWarning("CubeGrid ON"); - if(CubeGrid != null) - CubeGrid.SetActive(true); - else - CreateCubeGrid(); - } - else - { - CubeGridOn = false; - Debug.LogWarning("CubeGrid OFF"); - - if(CubeGrid != null) - CubeGrid.SetActive(false); - } - } - - if(CubeGrid != null) - { - // Set cube colors to let user know if camera is tracking - CubeSwitchColor = !OVRManager.tracker.isPositionTracked; - - if(CubeSwitchColor != CubeSwitchColorOld) - CubeGridSwitchColor(CubeSwitchColor); - CubeSwitchColorOld = CubeSwitchColor; - } - } - - void CreateCubeGrid() - { - Debug.LogWarning("Create CubeGrid"); - - // Create the visual cube grid - CubeGrid = new GameObject("CubeGrid"); - // Set a layer to target a specific camera - CubeGrid.layer = CameraController.gameObject.layer; - - for (int x = -gridSizeX; x <= gridSizeX; x++) - for (int y = -gridSizeY; y <= gridSizeY; y++) - for (int z = -gridSizeZ; z <= gridSizeZ; z++) - { - // Set the cube type: - // 0 = non-axis cube - // 1 = axis cube - // 2 = center cube - int CubeType = 0; - if ((x == 0 && y == 0) || (x == 0 && z == 0) || (y == 0 && z == 0)) - { - if((x == 0) && (y == 0) && (z == 0)) - CubeType = 2; - else - CubeType = 1; - } - - GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube); - - BoxCollider bc = cube.GetComponent(); - bc.enabled = false; - - cube.layer = CameraController.gameObject.layer; - - // No shadows - Renderer r = cube.GetComponent(); + /// + /// The key that toggles the grid of cubes. + /// + public KeyCode GridKey = KeyCode.G; + + private GameObject CubeGrid = null; + + private bool CubeGridOn = false; + private bool CubeSwitchColorOld = false; + private bool CubeSwitchColor = false; + + private int gridSizeX = 6; + private int gridSizeY = 4; + private int gridSizeZ = 6; + private float gridScale = 0.3f; + private float cubeScale = 0.03f; + + // Handle to OVRCameraRig + private OVRCameraRig CameraController = null; + + /// + /// Update this instance. + /// + void Update() + { + UpdateCubeGrid(); + } + + /// + /// Sets the OVR camera controller. + /// + /// Camera controller. + public void SetOVRCameraController(ref OVRCameraRig cameraController) + { + CameraController = cameraController; + } + + void UpdateCubeGrid() + { + // Toggle the grid cube display on 'G' + if (Input.GetKeyDown(GridKey)) + { + if (CubeGridOn == false) + { + CubeGridOn = true; + Debug.LogWarning("CubeGrid ON"); + if (CubeGrid != null) + CubeGrid.SetActive(true); + else + CreateCubeGrid(); + } + else + { + CubeGridOn = false; + Debug.LogWarning("CubeGrid OFF"); + + if (CubeGrid != null) + CubeGrid.SetActive(false); + } + } + + if (CubeGrid != null) + { + // Set cube colors to let user know if camera is tracking + CubeSwitchColor = !OVRManager.tracker.isPositionTracked; + + if (CubeSwitchColor != CubeSwitchColorOld) + CubeGridSwitchColor(CubeSwitchColor); + CubeSwitchColorOld = CubeSwitchColor; + } + } + + void CreateCubeGrid() + { + Debug.LogWarning("Create CubeGrid"); + + // Create the visual cube grid + CubeGrid = new GameObject("CubeGrid"); + // Set a layer to target a specific camera + CubeGrid.layer = CameraController.gameObject.layer; + + for (int x = -gridSizeX; x <= gridSizeX; x++) + for (int y = -gridSizeY; y <= gridSizeY; y++) + for (int z = -gridSizeZ; z <= gridSizeZ; z++) + { + // Set the cube type: + // 0 = non-axis cube + // 1 = axis cube + // 2 = center cube + int CubeType = 0; + if ((x == 0 && y == 0) || (x == 0 && z == 0) || (y == 0 && z == 0)) + { + if ((x == 0) && (y == 0) && (z == 0)) + CubeType = 2; + else + CubeType = 1; + } + + GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube); + + BoxCollider bc = cube.GetComponent(); + bc.enabled = false; + + cube.layer = CameraController.gameObject.layer; + + // No shadows + Renderer r = cube.GetComponent(); #if UNITY_4_0 || UNITY_4_1 || UNITY_4_2 || UNITY_4_3 || UNITY_4_5 || UNITY_4_6 // Renderer.castShadows was deprecated starting in Unity 5.0 r.castShadows = false; #else - r.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; + r.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; #endif - - r.receiveShadows = false; - - // Cube line is white down the middle - if (CubeType == 0) - r.material.color = Color.red; - else if (CubeType == 1) - r.material.color = Color.white; - else - r.material.color = Color.yellow; - - cube.transform.position = - new Vector3(((float)x * gridScale), - ((float)y * gridScale), - ((float)z * gridScale)); - - float s = 0.7f; - - // Axis cubes are bigger - if(CubeType == 1) - s = 1.0f; - // Center cube is the largest - if(CubeType == 2) - s = 2.0f; - - cube.transform.localScale = - new Vector3(cubeScale * s, cubeScale * s, cubeScale * s); - - cube.transform.parent = CubeGrid.transform; - } - } - - /// - /// Switch the Cube grid color. - /// - /// If set to true cube switch color. - void CubeGridSwitchColor(bool CubeSwitchColor) - { - Color c = Color.red; - if(CubeSwitchColor == true) - c = Color.blue; - - foreach(Transform child in CubeGrid.transform) - { - Material m = child.GetComponent().material; - // Cube line is white down the middle - if(m.color == Color.red || m.color == Color.blue) - m.color = c; - } - } + + r.receiveShadows = false; + + // Cube line is white down the middle + if (CubeType == 0) + r.material.color = Color.red; + else if (CubeType == 1) + r.material.color = Color.white; + else + r.material.color = Color.yellow; + + cube.transform.position = + new Vector3(((float)x * gridScale), + ((float)y * gridScale), + ((float)z * gridScale)); + + float s = 0.7f; + + // Axis cubes are bigger + if (CubeType == 1) + s = 1.0f; + // Center cube is the largest + if (CubeType == 2) + s = 2.0f; + + cube.transform.localScale = + new Vector3(cubeScale * s, cubeScale * s, cubeScale * s); + + cube.transform.parent = CubeGrid.transform; + } + } + + /// + /// Switch the Cube grid color. + /// + /// If set to true cube switch color. + void CubeGridSwitchColor(bool CubeSwitchColor) + { + Color c = Color.red; + if (CubeSwitchColor == true) + c = Color.blue; + + foreach (Transform child in CubeGrid.transform) + { + Material m = child.GetComponent().material; + // Cube line is white down the middle + if (m.color == Color.red || m.color == Color.blue) + m.color = c; + } + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRModeParms.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRModeParms.cs index e92324d9..7bb3c813 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRModeParms.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRModeParms.cs @@ -27,61 +27,61 @@ limitations under the License. /// public class OVRModeParms : MonoBehaviour { -#region Member Variables + #region Member Variables - /// - /// The gamepad button that will switch the application to CPU level 0 and GPU level 1. - /// - public OVRInput.RawButton resetButton = OVRInput.RawButton.X; + /// + /// The gamepad button that will switch the application to CPU level 0 and GPU level 1. + /// + public OVRInput.RawButton resetButton = OVRInput.RawButton.X; -#endregion + #endregion - /// - /// Invoke power state mode test. - /// - void Start() - { - if (!OVRManager.isHmdPresent) - { - enabled = false; - return; - } + /// + /// Invoke power state mode test. + /// + void Start() + { + if (!OVRManager.isHmdPresent) + { + enabled = false; + return; + } - // Call TestPowerLevelState after 10 seconds - // and repeats every 10 seconds. - InvokeRepeating ( "TestPowerStateMode", 10, 10.0f ); - } + // Call TestPowerLevelState after 10 seconds + // and repeats every 10 seconds. + InvokeRepeating("TestPowerStateMode", 10, 10.0f); + } - /// - /// Change default vr mode parms dynamically. - /// - void Update() - { - // NOTE: some of the buttons defined in OVRInput.RawButton are not available on the Android game pad controller - if ( OVRInput.GetDown(resetButton)) - { - //************************* - // Dynamically change VrModeParms cpu and gpu level. - // NOTE: Reset will cause 1 frame of flicker as it leaves - // and re-enters Vr mode. - //************************* - OVRPlugin.cpuLevel = 0; - OVRPlugin.gpuLevel = 1; - } - } + /// + /// Change default vr mode parms dynamically. + /// + void Update() + { + // NOTE: some of the buttons defined in OVRInput.RawButton are not available on the Android game pad controller + if (OVRInput.GetDown(resetButton)) + { + //************************* + // Dynamically change VrModeParms cpu and gpu level. + // NOTE: Reset will cause 1 frame of flicker as it leaves + // and re-enters Vr mode. + //************************* + OVRPlugin.cpuLevel = 0; + OVRPlugin.gpuLevel = 1; + } + } - /// - /// Check current power state mode. - /// - void TestPowerStateMode() - { - //************************* - // Check power-level state mode - //************************* - if (OVRPlugin.powerSaving) - { - // The device has been throttled - Debug.Log("POWER SAVE MODE ACTIVATED"); - } - } + /// + /// Check current power state mode. + /// + void TestPowerStateMode() + { + //************************* + // Check power-level state mode + //************************* + if (OVRPlugin.powerSaving) + { + // The device has been throttled + Debug.Log("POWER SAVE MODE ACTIVATED"); + } + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRMonoscopic.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRMonoscopic.cs index aa9fb801..830872dc 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRMonoscopic.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRMonoscopic.cs @@ -26,28 +26,28 @@ limitations under the License. /// public class OVRMonoscopic : MonoBehaviour { - /// - /// The gamepad button that will toggle monoscopic rendering. - /// - public OVRInput.RawButton toggleButton = OVRInput.RawButton.B; - - private bool monoscopic = false; - - /// - /// Check input and toggle monoscopic rendering mode if necessary - /// See the input mapping setup in the Unity Integration guide - /// - void Update() - { - // NOTE: some of the buttons defined in OVRInput.RawButton are not available on the Android game pad controller - if (OVRInput.GetDown(toggleButton)) - { - //************************* - // toggle monoscopic rendering mode - //************************* - monoscopic = !monoscopic; - OVRManager.instance.monoscopic = monoscopic; - } - } + /// + /// The gamepad button that will toggle monoscopic rendering. + /// + public OVRInput.RawButton toggleButton = OVRInput.RawButton.B; + + private bool monoscopic = false; + + /// + /// Check input and toggle monoscopic rendering mode if necessary + /// See the input mapping setup in the Unity Integration guide + /// + void Update() + { + // NOTE: some of the buttons defined in OVRInput.RawButton are not available on the Android game pad controller + if (OVRInput.GetDown(toggleButton)) + { + //************************* + // toggle monoscopic rendering mode + //************************* + monoscopic = !monoscopic; + OVRManager.instance.monoscopic = monoscopic; + } + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRPlayerController.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRPlayerController.cs index 3d66c5a6..8f81c276 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRPlayerController.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRPlayerController.cs @@ -28,445 +28,445 @@ limitations under the License. [RequireComponent(typeof(CharacterController))] public class OVRPlayerController : MonoBehaviour { - /// - /// The rate acceleration during movement. - /// - public float Acceleration = 0.1f; - - /// - /// The rate of damping on movement. - /// - public float Damping = 0.3f; - - /// - /// The rate of additional damping when moving sideways or backwards. - /// - public float BackAndSideDampen = 0.5f; - - /// - /// The force applied to the character when jumping. - /// - public float JumpForce = 0.3f; - - /// - /// The rate of rotation when using a gamepad. - /// - public float RotationAmount = 1.5f; - - /// - /// The rate of rotation when using the keyboard. - /// - public float RotationRatchet = 45.0f; - - /// - /// If true, reset the initial yaw of the player controller when the Hmd pose is recentered. - /// - public bool HmdResetsY = true; - - /// - /// If true, tracking data from a child OVRCameraRig will update the direction of movement. - /// - public bool HmdRotatesY = true; - - /// - /// Modifies the strength of gravity. - /// - public float GravityModifier = 0.379f; - - /// - /// If true, each OVRPlayerController will use the player's physical height. - /// - public bool useProfileData = true; - - protected CharacterController Controller = null; - protected OVRCameraRig CameraRig = null; - - private float MoveScale = 1.0f; - private Vector3 MoveThrottle = Vector3.zero; - private float FallSpeed = 0.0f; - private OVRPose? InitialPose; - private float InitialYRotation = 0.0f; - private float MoveScaleMultiplier = 1.0f; - private float RotationScaleMultiplier = 1.0f; - private bool SkipMouseRotation = false; - private bool HaltUpdateMovement = false; - private bool prevHatLeft = false; - private bool prevHatRight = false; - private float SimulationRate = 60f; - private float buttonRotation = 0f; - - void Start() - { - // Add eye-depth as a camera offset from the player controller - var p = CameraRig.transform.localPosition; - p.z = OVRManager.profile.eyeDepth; - CameraRig.transform.localPosition = p; - } - - void Awake() - { - Controller = gameObject.GetComponent(); - - if(Controller == null) - Debug.LogWarning("OVRPlayerController: No CharacterController attached."); - - // We use OVRCameraRig to set rotations to cameras, - // and to be influenced by rotation - OVRCameraRig[] CameraRigs = gameObject.GetComponentsInChildren(); - - if(CameraRigs.Length == 0) - Debug.LogWarning("OVRPlayerController: No OVRCameraRig attached."); - else if (CameraRigs.Length > 1) - Debug.LogWarning("OVRPlayerController: More then 1 OVRCameraRig attached."); - else - CameraRig = CameraRigs[0]; - - InitialYRotation = transform.rotation.eulerAngles.y; - } - - void OnEnable() - { - OVRManager.display.RecenteredPose += ResetOrientation; - - if (CameraRig != null) - { - CameraRig.UpdatedAnchors += UpdateTransform; - } - } - - void OnDisable() - { - OVRManager.display.RecenteredPose -= ResetOrientation; - - if (CameraRig != null) - { - CameraRig.UpdatedAnchors -= UpdateTransform; - } - } - - void Update() - { - //Use keys to ratchet rotation - if (Input.GetKeyDown(KeyCode.Q)) - buttonRotation -= RotationRatchet; - - if (Input.GetKeyDown(KeyCode.E)) - buttonRotation += RotationRatchet; - } - - protected virtual void UpdateController() - { - if (useProfileData) - { - if (InitialPose == null) - { - // Save the initial pose so it can be recovered if useProfileData - // is turned off later. - InitialPose = new OVRPose() - { - position = CameraRig.transform.localPosition, - orientation = CameraRig.transform.localRotation - }; - } - - var p = CameraRig.transform.localPosition; - if (OVRManager.instance.trackingOriginType == OVRManager.TrackingOrigin.EyeLevel) - { - p.y = OVRManager.profile.eyeHeight - (0.5f * Controller.height) + Controller.center.y; - } - else if (OVRManager.instance.trackingOriginType == OVRManager.TrackingOrigin.FloorLevel) - { - p.y = - (0.5f * Controller.height) + Controller.center.y; - } - CameraRig.transform.localPosition = p; - } - else if (InitialPose != null) - { - // Return to the initial pose if useProfileData was turned off at runtime - CameraRig.transform.localPosition = InitialPose.Value.position; - CameraRig.transform.localRotation = InitialPose.Value.orientation; - InitialPose = null; - } - - UpdateMovement(); - - Vector3 moveDirection = Vector3.zero; - - float motorDamp = (1.0f + (Damping * SimulationRate * Time.deltaTime)); - - MoveThrottle.x /= motorDamp; - MoveThrottle.y = (MoveThrottle.y > 0.0f) ? (MoveThrottle.y / motorDamp) : MoveThrottle.y; - MoveThrottle.z /= motorDamp; - - moveDirection += MoveThrottle * SimulationRate * Time.deltaTime; - - // Gravity - if (Controller.isGrounded && FallSpeed <= 0) - FallSpeed = ((Physics.gravity.y * (GravityModifier * 0.002f))); - else - FallSpeed += ((Physics.gravity.y * (GravityModifier * 0.002f)) * SimulationRate * Time.deltaTime); - - moveDirection.y += FallSpeed * SimulationRate * Time.deltaTime; - - // Offset correction for uneven ground - float bumpUpOffset = 0.0f; + /// + /// The rate acceleration during movement. + /// + public float Acceleration = 0.1f; + + /// + /// The rate of damping on movement. + /// + public float Damping = 0.3f; + + /// + /// The rate of additional damping when moving sideways or backwards. + /// + public float BackAndSideDampen = 0.5f; + + /// + /// The force applied to the character when jumping. + /// + public float JumpForce = 0.3f; + + /// + /// The rate of rotation when using a gamepad. + /// + public float RotationAmount = 1.5f; + + /// + /// The rate of rotation when using the keyboard. + /// + public float RotationRatchet = 45.0f; + + /// + /// If true, reset the initial yaw of the player controller when the Hmd pose is recentered. + /// + public bool HmdResetsY = true; + + /// + /// If true, tracking data from a child OVRCameraRig will update the direction of movement. + /// + public bool HmdRotatesY = true; + + /// + /// Modifies the strength of gravity. + /// + public float GravityModifier = 0.379f; + + /// + /// If true, each OVRPlayerController will use the player's physical height. + /// + public bool useProfileData = true; + + protected CharacterController Controller = null; + protected OVRCameraRig CameraRig = null; + + private float MoveScale = 1.0f; + private Vector3 MoveThrottle = Vector3.zero; + private float FallSpeed = 0.0f; + private OVRPose? InitialPose; + private float InitialYRotation = 0.0f; + private float MoveScaleMultiplier = 1.0f; + private float RotationScaleMultiplier = 1.0f; + private bool SkipMouseRotation = false; + private bool HaltUpdateMovement = false; + private bool prevHatLeft = false; + private bool prevHatRight = false; + private float SimulationRate = 60f; + private float buttonRotation = 0f; + + void Start() + { + // Add eye-depth as a camera offset from the player controller + var p = CameraRig.transform.localPosition; + p.z = OVRManager.profile.eyeDepth; + CameraRig.transform.localPosition = p; + } + + void Awake() + { + Controller = gameObject.GetComponent(); + + if (Controller == null) + Debug.LogWarning("OVRPlayerController: No CharacterController attached."); + + // We use OVRCameraRig to set rotations to cameras, + // and to be influenced by rotation + OVRCameraRig[] CameraRigs = gameObject.GetComponentsInChildren(); + + if (CameraRigs.Length == 0) + Debug.LogWarning("OVRPlayerController: No OVRCameraRig attached."); + else if (CameraRigs.Length > 1) + Debug.LogWarning("OVRPlayerController: More then 1 OVRCameraRig attached."); + else + CameraRig = CameraRigs[0]; + + InitialYRotation = transform.rotation.eulerAngles.y; + } + + void OnEnable() + { + OVRManager.display.RecenteredPose += ResetOrientation; + + if (CameraRig != null) + { + CameraRig.UpdatedAnchors += UpdateTransform; + } + } + + void OnDisable() + { + OVRManager.display.RecenteredPose -= ResetOrientation; + + if (CameraRig != null) + { + CameraRig.UpdatedAnchors -= UpdateTransform; + } + } + + void Update() + { + //Use keys to ratchet rotation + if (Input.GetKeyDown(KeyCode.Q)) + buttonRotation -= RotationRatchet; + + if (Input.GetKeyDown(KeyCode.E)) + buttonRotation += RotationRatchet; + } + + protected virtual void UpdateController() + { + if (useProfileData) + { + if (InitialPose == null) + { + // Save the initial pose so it can be recovered if useProfileData + // is turned off later. + InitialPose = new OVRPose() + { + position = CameraRig.transform.localPosition, + orientation = CameraRig.transform.localRotation + }; + } + + var p = CameraRig.transform.localPosition; + if (OVRManager.instance.trackingOriginType == OVRManager.TrackingOrigin.EyeLevel) + { + p.y = OVRManager.profile.eyeHeight - (0.5f * Controller.height) + Controller.center.y; + } + else if (OVRManager.instance.trackingOriginType == OVRManager.TrackingOrigin.FloorLevel) + { + p.y = -(0.5f * Controller.height) + Controller.center.y; + } + CameraRig.transform.localPosition = p; + } + else if (InitialPose != null) + { + // Return to the initial pose if useProfileData was turned off at runtime + CameraRig.transform.localPosition = InitialPose.Value.position; + CameraRig.transform.localRotation = InitialPose.Value.orientation; + InitialPose = null; + } + + UpdateMovement(); + + Vector3 moveDirection = Vector3.zero; + + float motorDamp = (1.0f + (Damping * SimulationRate * Time.deltaTime)); + + MoveThrottle.x /= motorDamp; + MoveThrottle.y = (MoveThrottle.y > 0.0f) ? (MoveThrottle.y / motorDamp) : MoveThrottle.y; + MoveThrottle.z /= motorDamp; + + moveDirection += MoveThrottle * SimulationRate * Time.deltaTime; + + // Gravity + if (Controller.isGrounded && FallSpeed <= 0) + FallSpeed = ((Physics.gravity.y * (GravityModifier * 0.002f))); + else + FallSpeed += ((Physics.gravity.y * (GravityModifier * 0.002f)) * SimulationRate * Time.deltaTime); + + moveDirection.y += FallSpeed * SimulationRate * Time.deltaTime; + + // Offset correction for uneven ground + float bumpUpOffset = 0.0f; if (Controller.isGrounded && MoveThrottle.y <= transform.lossyScale.y * 0.001f) - { - bumpUpOffset = Mathf.Max(Controller.stepOffset, new Vector3(moveDirection.x, 0, moveDirection.z).magnitude); - moveDirection -= bumpUpOffset * Vector3.up; - } + { + bumpUpOffset = Mathf.Max(Controller.stepOffset, new Vector3(moveDirection.x, 0, moveDirection.z).magnitude); + moveDirection -= bumpUpOffset * Vector3.up; + } - Vector3 predictedXZ = Vector3.Scale((Controller.transform.localPosition + moveDirection), new Vector3(1, 0, 1)); + Vector3 predictedXZ = Vector3.Scale((Controller.transform.localPosition + moveDirection), new Vector3(1, 0, 1)); - // Move contoller - Controller.Move(moveDirection); + // Move contoller + Controller.Move(moveDirection); - Vector3 actualXZ = Vector3.Scale(Controller.transform.localPosition, new Vector3(1, 0, 1)); + Vector3 actualXZ = Vector3.Scale(Controller.transform.localPosition, new Vector3(1, 0, 1)); - if (predictedXZ != actualXZ) - MoveThrottle += (actualXZ - predictedXZ) / (SimulationRate * Time.deltaTime); - } + if (predictedXZ != actualXZ) + MoveThrottle += (actualXZ - predictedXZ) / (SimulationRate * Time.deltaTime); + } - public virtual void UpdateMovement() - { - if (HaltUpdateMovement) - return; + public virtual void UpdateMovement() + { + if (HaltUpdateMovement) + return; - bool moveForward = Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow); - bool moveLeft = Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow); - bool moveRight = Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow); - bool moveBack = Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow); + bool moveForward = Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow); + bool moveLeft = Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow); + bool moveRight = Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow); + bool moveBack = Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow); - bool dpad_move = false; + bool dpad_move = false; - if (OVRInput.Get(OVRInput.Button.DpadUp)) - { - moveForward = true; - dpad_move = true; + if (OVRInput.Get(OVRInput.Button.DpadUp)) + { + moveForward = true; + dpad_move = true; - } + } - if (OVRInput.Get(OVRInput.Button.DpadDown)) - { - moveBack = true; - dpad_move = true; - } + if (OVRInput.Get(OVRInput.Button.DpadDown)) + { + moveBack = true; + dpad_move = true; + } - MoveScale = 1.0f; + MoveScale = 1.0f; - if ( (moveForward && moveLeft) || (moveForward && moveRight) || - (moveBack && moveLeft) || (moveBack && moveRight) ) - MoveScale = 0.70710678f; + if ((moveForward && moveLeft) || (moveForward && moveRight) || + (moveBack && moveLeft) || (moveBack && moveRight)) + MoveScale = 0.70710678f; - // No positional movement if we are in the air - if (!Controller.isGrounded) - MoveScale = 0.0f; + // No positional movement if we are in the air + if (!Controller.isGrounded) + MoveScale = 0.0f; - MoveScale *= SimulationRate * Time.deltaTime; + MoveScale *= SimulationRate * Time.deltaTime; - // Compute this for key movement - float moveInfluence = Acceleration * 0.1f * MoveScale * MoveScaleMultiplier; + // Compute this for key movement + float moveInfluence = Acceleration * 0.1f * MoveScale * MoveScaleMultiplier; - // Run! - if (dpad_move || Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift)) - moveInfluence *= 2.0f; + // Run! + if (dpad_move || Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift)) + moveInfluence *= 2.0f; - Quaternion ort = transform.rotation; - Vector3 ortEuler = ort.eulerAngles; - ortEuler.z = ortEuler.x = 0f; - ort = Quaternion.Euler(ortEuler); + Quaternion ort = transform.rotation; + Vector3 ortEuler = ort.eulerAngles; + ortEuler.z = ortEuler.x = 0f; + ort = Quaternion.Euler(ortEuler); - if (moveForward) - MoveThrottle += ort * (transform.lossyScale.z * moveInfluence * Vector3.forward); - if (moveBack) - MoveThrottle += ort * (transform.lossyScale.z * moveInfluence * BackAndSideDampen * Vector3.back); - if (moveLeft) - MoveThrottle += ort * (transform.lossyScale.x * moveInfluence * BackAndSideDampen * Vector3.left); - if (moveRight) - MoveThrottle += ort * (transform.lossyScale.x * moveInfluence * BackAndSideDampen * Vector3.right); + if (moveForward) + MoveThrottle += ort * (transform.lossyScale.z * moveInfluence * Vector3.forward); + if (moveBack) + MoveThrottle += ort * (transform.lossyScale.z * moveInfluence * BackAndSideDampen * Vector3.back); + if (moveLeft) + MoveThrottle += ort * (transform.lossyScale.x * moveInfluence * BackAndSideDampen * Vector3.left); + if (moveRight) + MoveThrottle += ort * (transform.lossyScale.x * moveInfluence * BackAndSideDampen * Vector3.right); - Vector3 euler = transform.rotation.eulerAngles; + Vector3 euler = transform.rotation.eulerAngles; - bool curHatLeft = OVRInput.Get(OVRInput.Button.PrimaryShoulder); + bool curHatLeft = OVRInput.Get(OVRInput.Button.PrimaryShoulder); - if (curHatLeft && !prevHatLeft) - euler.y -= RotationRatchet; + if (curHatLeft && !prevHatLeft) + euler.y -= RotationRatchet; - prevHatLeft = curHatLeft; + prevHatLeft = curHatLeft; - bool curHatRight = OVRInput.Get(OVRInput.Button.SecondaryShoulder); + bool curHatRight = OVRInput.Get(OVRInput.Button.SecondaryShoulder); - if(curHatRight && !prevHatRight) - euler.y += RotationRatchet; + if (curHatRight && !prevHatRight) + euler.y += RotationRatchet; - prevHatRight = curHatRight; + prevHatRight = curHatRight; - euler.y += buttonRotation; - buttonRotation = 0f; + euler.y += buttonRotation; + buttonRotation = 0f; - float rotateInfluence = SimulationRate * Time.deltaTime * RotationAmount * RotationScaleMultiplier; + float rotateInfluence = SimulationRate * Time.deltaTime * RotationAmount * RotationScaleMultiplier; #if !UNITY_ANDROID || UNITY_EDITOR - if (!SkipMouseRotation) - euler.y += Input.GetAxis("Mouse X") * rotateInfluence * 3.25f; + if (!SkipMouseRotation) + euler.y += Input.GetAxis("Mouse X") * rotateInfluence * 3.25f; #endif - moveInfluence = Acceleration * 0.1f * MoveScale * MoveScaleMultiplier; + moveInfluence = Acceleration * 0.1f * MoveScale * MoveScaleMultiplier; #if !UNITY_ANDROID // LeftTrigger not avail on Android game pad - moveInfluence *= 1.0f + OVRInput.Get(OVRInput.Axis1D.PrimaryIndexTrigger); + moveInfluence *= 1.0f + OVRInput.Get(OVRInput.Axis1D.PrimaryIndexTrigger); #endif - Vector2 primaryAxis = OVRInput.Get(OVRInput.Axis2D.PrimaryThumbstick); + Vector2 primaryAxis = OVRInput.Get(OVRInput.Axis2D.PrimaryThumbstick); - if(primaryAxis.y > 0.0f) + if (primaryAxis.y > 0.0f) MoveThrottle += ort * (primaryAxis.y * transform.lossyScale.z * moveInfluence * Vector3.forward); - if(primaryAxis.y < 0.0f) + if (primaryAxis.y < 0.0f) MoveThrottle += ort * (Mathf.Abs(primaryAxis.y) * transform.lossyScale.z * moveInfluence * BackAndSideDampen * Vector3.back); - if(primaryAxis.x < 0.0f) + if (primaryAxis.x < 0.0f) MoveThrottle += ort * (Mathf.Abs(primaryAxis.x) * transform.lossyScale.x * moveInfluence * BackAndSideDampen * Vector3.left); - if(primaryAxis.x > 0.0f) + if (primaryAxis.x > 0.0f) MoveThrottle += ort * (primaryAxis.x * transform.lossyScale.x * moveInfluence * BackAndSideDampen * Vector3.right); - Vector2 secondaryAxis = OVRInput.Get(OVRInput.Axis2D.SecondaryThumbstick); + Vector2 secondaryAxis = OVRInput.Get(OVRInput.Axis2D.SecondaryThumbstick); - euler.y += secondaryAxis.x * rotateInfluence; + euler.y += secondaryAxis.x * rotateInfluence; - transform.rotation = Quaternion.Euler(euler); - } + transform.rotation = Quaternion.Euler(euler); + } - /// - /// Invoked by OVRCameraRig's UpdatedAnchors callback. Allows the Hmd rotation to update the facing direction of the player. - /// - public void UpdateTransform(OVRCameraRig rig) - { - Transform root = CameraRig.trackingSpace; - Transform centerEye = CameraRig.centerEyeAnchor; + /// + /// Invoked by OVRCameraRig's UpdatedAnchors callback. Allows the Hmd rotation to update the facing direction of the player. + /// + public void UpdateTransform(OVRCameraRig rig) + { + Transform root = CameraRig.trackingSpace; + Transform centerEye = CameraRig.centerEyeAnchor; - if (HmdRotatesY) - { - Vector3 prevPos = root.position; - Quaternion prevRot = root.rotation; + if (HmdRotatesY) + { + Vector3 prevPos = root.position; + Quaternion prevRot = root.rotation; - transform.rotation = Quaternion.Euler(0.0f, centerEye.rotation.eulerAngles.y, 0.0f); + transform.rotation = Quaternion.Euler(0.0f, centerEye.rotation.eulerAngles.y, 0.0f); - root.position = prevPos; - root.rotation = prevRot; - } + root.position = prevPos; + root.rotation = prevRot; + } - UpdateController(); - } + UpdateController(); + } - /// - /// Jump! Must be enabled manually. - /// - public bool Jump() - { - if (!Controller.isGrounded) - return false; + /// + /// Jump! Must be enabled manually. + /// + public bool Jump() + { + if (!Controller.isGrounded) + return false; MoveThrottle += new Vector3(0, transform.lossyScale.y * JumpForce, 0); - return true; - } - - /// - /// Stop this instance. - /// - public void Stop() - { - Controller.Move(Vector3.zero); - MoveThrottle = Vector3.zero; - FallSpeed = 0.0f; - } - - /// - /// Gets the move scale multiplier. - /// - /// Move scale multiplier. - public void GetMoveScaleMultiplier(ref float moveScaleMultiplier) - { - moveScaleMultiplier = MoveScaleMultiplier; - } - - /// - /// Sets the move scale multiplier. - /// - /// Move scale multiplier. - public void SetMoveScaleMultiplier(float moveScaleMultiplier) - { - MoveScaleMultiplier = moveScaleMultiplier; - } - - /// - /// Gets the rotation scale multiplier. - /// - /// Rotation scale multiplier. - public void GetRotationScaleMultiplier(ref float rotationScaleMultiplier) - { - rotationScaleMultiplier = RotationScaleMultiplier; - } - - /// - /// Sets the rotation scale multiplier. - /// - /// Rotation scale multiplier. - public void SetRotationScaleMultiplier(float rotationScaleMultiplier) - { - RotationScaleMultiplier = rotationScaleMultiplier; - } - - /// - /// Gets the allow mouse rotation. - /// - /// Allow mouse rotation. - public void GetSkipMouseRotation(ref bool skipMouseRotation) - { - skipMouseRotation = SkipMouseRotation; - } - - /// - /// Sets the allow mouse rotation. - /// - /// If set to true allow mouse rotation. - public void SetSkipMouseRotation(bool skipMouseRotation) - { - SkipMouseRotation = skipMouseRotation; - } - - /// - /// Gets the halt update movement. - /// - /// Halt update movement. - public void GetHaltUpdateMovement(ref bool haltUpdateMovement) - { - haltUpdateMovement = HaltUpdateMovement; - } - - /// - /// Sets the halt update movement. - /// - /// If set to true halt update movement. - public void SetHaltUpdateMovement(bool haltUpdateMovement) - { - HaltUpdateMovement = haltUpdateMovement; - } - - /// - /// Resets the player look rotation when the device orientation is reset. - /// - public void ResetOrientation() - { - if (HmdResetsY && !HmdRotatesY) - { - Vector3 euler = transform.rotation.eulerAngles; - euler.y = InitialYRotation; - transform.rotation = Quaternion.Euler(euler); - } - } + return true; + } + + /// + /// Stop this instance. + /// + public void Stop() + { + Controller.Move(Vector3.zero); + MoveThrottle = Vector3.zero; + FallSpeed = 0.0f; + } + + /// + /// Gets the move scale multiplier. + /// + /// Move scale multiplier. + public void GetMoveScaleMultiplier(ref float moveScaleMultiplier) + { + moveScaleMultiplier = MoveScaleMultiplier; + } + + /// + /// Sets the move scale multiplier. + /// + /// Move scale multiplier. + public void SetMoveScaleMultiplier(float moveScaleMultiplier) + { + MoveScaleMultiplier = moveScaleMultiplier; + } + + /// + /// Gets the rotation scale multiplier. + /// + /// Rotation scale multiplier. + public void GetRotationScaleMultiplier(ref float rotationScaleMultiplier) + { + rotationScaleMultiplier = RotationScaleMultiplier; + } + + /// + /// Sets the rotation scale multiplier. + /// + /// Rotation scale multiplier. + public void SetRotationScaleMultiplier(float rotationScaleMultiplier) + { + RotationScaleMultiplier = rotationScaleMultiplier; + } + + /// + /// Gets the allow mouse rotation. + /// + /// Allow mouse rotation. + public void GetSkipMouseRotation(ref bool skipMouseRotation) + { + skipMouseRotation = SkipMouseRotation; + } + + /// + /// Sets the allow mouse rotation. + /// + /// If set to true allow mouse rotation. + public void SetSkipMouseRotation(bool skipMouseRotation) + { + SkipMouseRotation = skipMouseRotation; + } + + /// + /// Gets the halt update movement. + /// + /// Halt update movement. + public void GetHaltUpdateMovement(ref bool haltUpdateMovement) + { + haltUpdateMovement = HaltUpdateMovement; + } + + /// + /// Sets the halt update movement. + /// + /// If set to true halt update movement. + public void SetHaltUpdateMovement(bool haltUpdateMovement) + { + HaltUpdateMovement = haltUpdateMovement; + } + + /// + /// Resets the player look rotation when the device orientation is reset. + /// + public void ResetOrientation() + { + if (HmdResetsY && !HmdRotatesY) + { + Vector3 euler = transform.rotation.eulerAngles; + euler.y = InitialYRotation; + transform.rotation = Quaternion.Euler(euler); + } + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRRTOverlayConnector.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRRTOverlayConnector.cs index c58cd20f..1b0a5c05 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRRTOverlayConnector.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRRTOverlayConnector.cs @@ -37,61 +37,61 @@ limitations under the License. /// public class OVRRTOverlayConnector : MonoBehaviour { - /// - /// OVROverlay texture required alpha = 0 border for avoiding artifacts - /// - public int alphaBorderSizePixels = 3; + /// + /// OVROverlay texture required alpha = 0 border for avoiding artifacts + /// + public int alphaBorderSizePixels = 3; - /// - /// Triple buffer the render target - /// - const int overlayRTChainSize = 3; - private int overlayRTIndex = 0; - private IntPtr[] overlayTexturePtrs = new IntPtr[overlayRTChainSize]; - private RenderTexture[] overlayRTChain = new RenderTexture[overlayRTChainSize]; + /// + /// Triple buffer the render target + /// + const int overlayRTChainSize = 3; + private int overlayRTIndex = 0; + private IntPtr[] overlayTexturePtrs = new IntPtr[overlayRTChainSize]; + private RenderTexture[] overlayRTChain = new RenderTexture[overlayRTChainSize]; - /// - /// Destination OVROverlay target object - /// - public GameObject ovrOverlayObj; - private RenderTexture srcRT; - private Camera ownerCamera; + /// + /// Destination OVROverlay target object + /// + public GameObject ovrOverlayObj; + private RenderTexture srcRT; + private Camera ownerCamera; - /// - /// Reconstruct render texture chain if ownerCamera's targetTexture was changed - /// - public void RefreshRenderTextureChain() - { - srcRT = ownerCamera.targetTexture; - Debug.Assert(srcRT); - ConstructRenderTextureChain(); - } + /// + /// Reconstruct render texture chain if ownerCamera's targetTexture was changed + /// + public void RefreshRenderTextureChain() + { + srcRT = ownerCamera.targetTexture; + Debug.Assert(srcRT); + ConstructRenderTextureChain(); + } -/// -/// Triple buffer the textures applying to overlay -/// - void ConstructRenderTextureChain() - { - for (int i = 0; i < overlayRTChainSize; i++) - { - overlayRTChain[i] = new RenderTexture(srcRT.width, srcRT.height, 1, srcRT.format, RenderTextureReadWrite.sRGB); - overlayRTChain[i].antiAliasing = 1; - overlayRTChain[i].depth = 0; - overlayRTChain[i].wrapMode = TextureWrapMode.Clamp; - overlayRTChain[i].hideFlags = HideFlags.HideAndDontSave; - overlayRTChain[i].Create(); - overlayTexturePtrs[i] = overlayRTChain[i].GetNativeTexturePtr(); - } - } + /// + /// Triple buffer the textures applying to overlay + /// + void ConstructRenderTextureChain() + { + for (int i = 0; i < overlayRTChainSize; i++) + { + overlayRTChain[i] = new RenderTexture(srcRT.width, srcRT.height, 1, srcRT.format, RenderTextureReadWrite.sRGB); + overlayRTChain[i].antiAliasing = 1; + overlayRTChain[i].depth = 0; + overlayRTChain[i].wrapMode = TextureWrapMode.Clamp; + overlayRTChain[i].hideFlags = HideFlags.HideAndDontSave; + overlayRTChain[i].Create(); + overlayTexturePtrs[i] = overlayRTChain[i].GetNativeTexturePtr(); + } + } - void Start () - { - ownerCamera = GetComponent(); - Debug.Assert(ownerCamera); - srcRT = ownerCamera.targetTexture; - Debug.Assert(srcRT); - ConstructRenderTextureChain(); - } + void Start() + { + ownerCamera = GetComponent(); + Debug.Assert(ownerCamera); + srcRT = ownerCamera.targetTexture; + Debug.Assert(srcRT); + ConstructRenderTextureChain(); + } #if UNITY_ANDROID /// @@ -110,19 +110,19 @@ void OnPreRender() } #endif - /// - /// Copy camera's render target to triple buffered texture array and send it to OVROverlay object - /// - void OnPostRender() - { - if (srcRT) - { - Graphics.Blit(srcRT, overlayRTChain[overlayRTIndex]); - OVROverlay ovrOverlay = ovrOverlayObj.GetComponent(); - Debug.Assert(ovrOverlay); - ovrOverlay.OverrideOverlayTextureInfo(overlayRTChain[overlayRTIndex], overlayTexturePtrs[overlayRTIndex], UnityEngine.XR.XRNode.LeftEye); - overlayRTIndex++; - overlayRTIndex = overlayRTIndex % overlayRTChainSize; - } - } + /// + /// Copy camera's render target to triple buffered texture array and send it to OVROverlay object + /// + void OnPostRender() + { + if (srcRT) + { + Graphics.Blit(srcRT, overlayRTChain[overlayRTIndex]); + OVROverlay ovrOverlay = ovrOverlayObj.GetComponent(); + Debug.Assert(ovrOverlay); + ovrOverlay.OverrideOverlayTextureInfo(overlayRTChain[overlayRTIndex], overlayTexturePtrs[overlayRTIndex], UnityEngine.XR.XRNode.LeftEye); + overlayRTIndex++; + overlayRTIndex = overlayRTIndex % overlayRTChainSize; + } + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRResetOrientation.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRResetOrientation.cs index aee62bd9..80afe5d7 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRResetOrientation.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRResetOrientation.cs @@ -26,24 +26,24 @@ limitations under the License. /// public class OVRResetOrientation : MonoBehaviour { - /// - /// The gamepad button that will reset VR input tracking. - /// - public OVRInput.RawButton resetButton = OVRInput.RawButton.Y; - - /// - /// Check input and reset orientation if necessary - /// See the input mapping setup in the Unity Integration guide - /// - void Update() - { - // NOTE: some of the buttons defined in OVRInput.RawButton are not available on the Android game pad controller - if (OVRInput.GetDown(resetButton)) - { - //************************* - // reset orientation - //************************* - OVRManager.display.RecenterPose(); - } - } + /// + /// The gamepad button that will reset VR input tracking. + /// + public OVRInput.RawButton resetButton = OVRInput.RawButton.Y; + + /// + /// Check input and reset orientation if necessary + /// See the input mapping setup in the Unity Integration guide + /// + void Update() + { + // NOTE: some of the buttons defined in OVRInput.RawButton are not available on the Android game pad controller + if (OVRInput.GetDown(resetButton)) + { + //************************* + // reset orientation + //************************* + OVRManager.display.RecenterPose(); + } + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRSceneSampleController.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRSceneSampleController.cs index a0bee2a2..35b66ce0 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRSceneSampleController.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRSceneSampleController.cs @@ -37,7 +37,7 @@ public class OVRSceneSampleController : MonoBehaviour /// An optional texture that appears before the menu fades in. /// public Texture fadeInTexture = null; - + /// /// Controls how quickly the player's speed and rotation change based on input. /// @@ -86,7 +86,7 @@ void Awake() else { cameraController = cameraControllers[0]; - } + } // Find player controller OVRPlayerController[] playerControllers; @@ -114,8 +114,8 @@ void Start() // Make sure to hide cursor if (Application.isEditor == false) { - Cursor.visible = false; - Cursor.lockState = CursorLockMode.Locked; + Cursor.visible = false; + Cursor.lockState = CursorLockMode.Locked; } // CameraController updates @@ -124,7 +124,7 @@ void Start() // Add a GridCube component to this object gridCube = gameObject.AddComponent(); gridCube.SetOVRCameraController(ref cameraController); - } + } } @@ -132,7 +132,7 @@ void Start() /// Update this instance. /// void Update() - { + { // Recenter pose UpdateRecenterPose(); @@ -148,7 +148,7 @@ void Update() Screen.fullScreen = !Screen.fullScreen; if (Input.GetKeyDown(KeyCode.M)) - UnityEngine.XR.XRSettings.showDeviceView = !UnityEngine.XR.XRSettings.showDeviceView; + UnityEngine.XR.XRSettings.showDeviceView = !UnityEngine.XR.XRSettings.showDeviceView; #if !UNITY_ANDROID || UNITY_EDITOR // Escape Application @@ -166,7 +166,7 @@ void UpdateVisionMode() if (Input.GetKeyDown(KeyCode.F2)) { visionMode ^= visionMode; - OVRManager.tracker.isEnabled = visionMode; + OVRManager.tracker.isEnabled = visionMode; } } @@ -202,8 +202,8 @@ void UpdateSpeedAndRotationScaleMultiplier() } playerController.SetRotationScaleMultiplier(rotationScaleMultiplier); - } - + } + /// /// Recenter pose /// diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRScreenFade.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRScreenFade.cs index 3c14628d..662de65b 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRScreenFade.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRScreenFade.cs @@ -27,97 +27,97 @@ limitations under the License. /// public class OVRScreenFade : MonoBehaviour { - /// - /// How long it takes to fade. - /// - public float fadeTime = 2.0f; - - /// - /// The initial screen color. - /// - public Color fadeColor = new Color(0.01f, 0.01f, 0.01f, 1.0f); - - private Material fadeMaterial = null; - private bool isFading = false; - private YieldInstruction fadeInstruction = new WaitForEndOfFrame(); - - /// - /// Initialize. - /// - void Awake() - { - // create the fade material - fadeMaterial = new Material(Shader.Find("Oculus/Unlit Transparent Color")); - } - - /// - /// Starts the fade in - /// - void OnEnable() - { - StartCoroutine(FadeIn()); - } - - /// - /// Starts a fade in when a new level is loaded - /// + /// + /// How long it takes to fade. + /// + public float fadeTime = 2.0f; + + /// + /// The initial screen color. + /// + public Color fadeColor = new Color(0.01f, 0.01f, 0.01f, 1.0f); + + private Material fadeMaterial = null; + private bool isFading = false; + private YieldInstruction fadeInstruction = new WaitForEndOfFrame(); + + /// + /// Initialize. + /// + void Awake() + { + // create the fade material + fadeMaterial = new Material(Shader.Find("Oculus/Unlit Transparent Color")); + } + + /// + /// Starts the fade in + /// + void OnEnable() + { + StartCoroutine(FadeIn()); + } + + /// + /// Starts a fade in when a new level is loaded + /// #if UNITY_5_4_OR_NEWER void OnLevelFinishedLoading(int level) #else - void OnLevelWasLoaded(int level) + void OnLevelWasLoaded(int level) #endif - { - StartCoroutine(FadeIn()); - } - - /// - /// Cleans up the fade material - /// - void OnDestroy() - { - if (fadeMaterial != null) - { - Destroy(fadeMaterial); - } - } - - /// - /// Fades alpha from 1.0 to 0.0 - /// - IEnumerator FadeIn() - { - float elapsedTime = 0.0f; - fadeMaterial.color = fadeColor; - Color color = fadeColor; - isFading = true; - while (elapsedTime < fadeTime) - { - yield return fadeInstruction; - elapsedTime += Time.deltaTime; - color.a = 1.0f - Mathf.Clamp01(elapsedTime / fadeTime); - fadeMaterial.color = color; - } - isFading = false; - } - - /// - /// Renders the fade overlay when attached to a camera object - /// - void OnPostRender() - { - if (isFading) - { - fadeMaterial.SetPass(0); - GL.PushMatrix(); - GL.LoadOrtho(); - GL.Color(fadeMaterial.color); - GL.Begin(GL.QUADS); - GL.Vertex3(0f, 0f, -12f); - GL.Vertex3(0f, 1f, -12f); - GL.Vertex3(1f, 1f, -12f); - GL.Vertex3(1f, 0f, -12f); - GL.End(); - GL.PopMatrix(); - } - } + { + StartCoroutine(FadeIn()); + } + + /// + /// Cleans up the fade material + /// + void OnDestroy() + { + if (fadeMaterial != null) + { + Destroy(fadeMaterial); + } + } + + /// + /// Fades alpha from 1.0 to 0.0 + /// + IEnumerator FadeIn() + { + float elapsedTime = 0.0f; + fadeMaterial.color = fadeColor; + Color color = fadeColor; + isFading = true; + while (elapsedTime < fadeTime) + { + yield return fadeInstruction; + elapsedTime += Time.deltaTime; + color.a = 1.0f - Mathf.Clamp01(elapsedTime / fadeTime); + fadeMaterial.color = color; + } + isFading = false; + } + + /// + /// Renders the fade overlay when attached to a camera object + /// + void OnPostRender() + { + if (isFading) + { + fadeMaterial.SetPass(0); + GL.PushMatrix(); + GL.LoadOrtho(); + GL.Color(fadeMaterial.color); + GL.Begin(GL.QUADS); + GL.Vertex3(0f, 0f, -12f); + GL.Vertex3(0f, 1f, -12f); + GL.Vertex3(1f, 1f, -12f); + GL.Vertex3(1f, 0f, -12f); + GL.End(); + GL.PopMatrix(); + } + } } diff --git a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRWaitCursor.cs b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRWaitCursor.cs index 0e2f3fb8..e6bb9abd 100644 --- a/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRWaitCursor.cs +++ b/Assets/Scripts/ThirdParty/OVR/Scripts/Util/OVRWaitCursor.cs @@ -26,13 +26,13 @@ limitations under the License. /// public class OVRWaitCursor : MonoBehaviour { - public Vector3 rotateSpeeds = new Vector3(0.0f, 0.0f, -60.0f); - - /// - /// Auto rotates the attached cursor. - /// - void Update() - { - transform.Rotate(rotateSpeeds * Time.smoothDeltaTime); - } + public Vector3 rotateSpeeds = new Vector3(0.0f, 0.0f, -60.0f); + + /// + /// Auto rotates the attached cursor. + /// + void Update() + { + transform.Rotate(rotateSpeeds * Time.smoothDeltaTime); + } } diff --git a/Assets/Scripts/alignment/CoordinateSystem.cs b/Assets/Scripts/alignment/CoordinateSystem.cs index f7821212..3dc0cc00 100644 --- a/Assets/Scripts/alignment/CoordinateSystem.cs +++ b/Assets/Scripts/alignment/CoordinateSystem.cs @@ -18,191 +18,205 @@ using com.google.apps.peltzer.client.tools.utils; using UnityEngine; -namespace com.google.apps.peltzer.client.alignment { - /// - /// Defines an arbitrary coordinate system. - /// - /// A coordinate system has an origin and three axes used to determine the position of points or other - /// geometric elements in Euclidean space. Coordinate systems exist in model space so that they move and rotate with - /// scale/world changes. - /// - public class CoordinateSystem { - // Defining thresholds in world space accounts for a user's available precision and their perspective. When a user - // can see less they are more likely to snap or stick but when a user is zoomed in and working in detail they need - // to be more precise to snap or stick because they can visualize what they are working on better. - - /// - /// The worldspace threshold at which a point will stick to an important position (origin, corner, etc). - /// - public const float STICK_THRESHOLD_WORLDSPACE = 0.03f; - - /// - /// The worldspace threshold at which a point will stick to an axis. - /// - protected const float AXIS_STICK_THRESHOLD_WORLDSPACE = 0.04f; - - /// - /// The default properties of a coordinate system. The defaults represent the universal coordinate system. - /// - protected readonly Vector3 DEFAULT_ORIGIN = Vector3.zero; - protected readonly Quaternion DEFAULT_ROTATION = Quaternion.identity; - protected readonly Axes DEFAULT_AXES = new Axes(Vector3.right, Vector3.up, Vector3.forward); - - /// - /// The center or anchor of the coordinate system in model space. In the universal coordinate system this is - /// Vector3.zero. - /// - protected Vector3 origin { get; private set; } - - /// - /// The right, up and forward axes represented as perpendicular unit vectors in model space. In the universal - /// coordinate system these are: Vector3.right, Vector3.up, Vector3.forward. - /// - protected Axes axes { get; private set; } - - /// - /// The rotation of the coordinate system defined as the rotational difference between the axes and the universal - /// coordinate system axes in model space. - /// - protected Quaternion rotation { get; private set; } - - /// - /// The scale of the coordinate system. Set to one for use with Matrix transformations. - /// - protected readonly Vector3 scale = Vector3.one; - - /// - /// The transform matrix for this coordinate system. - /// - private Matrix4x4 transformMatrix; - - protected void Setup(Vector3 origin, Quaternion rotation, Axes axes) { - this.origin = origin; - this.rotation = rotation; - this.axes = axes; - - transformMatrix = Matrix4x4.TRS(origin, rotation, scale); - } - - /// - /// Snaps a position to the coordinate system and then returns the position in its original space. This allows us - /// to snap a model space position to an arbritrary grid. - /// - /// The position to snap. - /// The position snapped to the coordinate system in its original space. - public void SnapToGrid(Vector3 position, out Vector3 snappedPosition) { - // Transform the position into the coordinate system space. - Vector3 positionInCoordinatesSystemSpace = transformMatrix.MultiplyPoint3x4(position); - - // Snap the transformed position to the coordinate system grid. - Vector3 snappedPositionInCoordinateSystemSpace = GridUtils.SnapToGrid(positionInCoordinatesSystemSpace); - - // Transform back to the positions original space. - snappedPosition = transformMatrix.inverse.MultiplyPoint3x4(snappedPositionInCoordinateSystemSpace); - } - - /// - /// Snaps a position to the origin. Returns false if the origin is not within the threshold to stick. - /// - /// The position being snapped. - /// The snapped position, this is either the origin or the original position. - /// Whether the position is close enough to the origin to stick. - public bool SnapToOrigin(Vector3 position, out Vector3 snappedPosition) { - if (Vector3.Distance(position, origin) < STICK_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale) { - snappedPosition = origin; - return true; - } - - snappedPosition = position; - return false; - } - - /// - /// Snaps a position to the nearest axis. Returns false if the position is not within the threshold to stick. - /// - /// Will either snap the position smoothly onto the nearest axis or to the nearest grid unit from the origin if - /// gridMode is on. - /// - /// The position to snap to an axis. - /// Whether the snap should be smooth or in grid increments. - /// The snapped position. - /// Whether the position is close enough to an axis to stick. - public bool SnapToAxes(Vector3 position, bool isGridMode, out Vector3 snappedPosition) { - // Find the position snapped to the up axis. - Vector3 snappedUpPosition; - float upDelta; - bool withinUpThreshold = SnapToAxis(Axes.Axis.UP, position, isGridMode, out snappedUpPosition, out upDelta); - - // Find the position snapped to the forward axis. - Vector3 snappedForwardPosition; - float forwardDelta; - bool withinForwardThreshold = SnapToAxis(Axes.Axis.FORWARD, position, isGridMode, - out snappedForwardPosition, out forwardDelta); - - // Find the position snapped to the right axis. - Vector3 snappedRightPosition; - float rightDelta; - bool withinRightThreshold = SnapToAxis(Axes.Axis.RIGHT, position, isGridMode, out snappedRightPosition, - out rightDelta); - - // Determine which axis is closest. - float minDistance = Mathf.Min(upDelta, forwardDelta, rightDelta); - - if (minDistance == upDelta) { - snappedPosition = snappedUpPosition; - return withinUpThreshold; - } else if (minDistance == forwardDelta) { - snappedPosition = snappedForwardPosition; - return withinForwardThreshold; - } else { - snappedPosition = snappedRightPosition; - return withinRightThreshold; - } - } - +namespace com.google.apps.peltzer.client.alignment +{ /// - /// Snaps a position to a given axis. Returns false if the position is not within the threshold to snap. + /// Defines an arbitrary coordinate system. /// - /// Will either snap the position smoothly onto the nearest axis or to the nearest grid unit from the origin if - /// gridMode is on. + /// A coordinate system has an origin and three axes used to determine the position of points or other + /// geometric elements in Euclidean space. Coordinate systems exist in model space so that they move and rotate with + /// scale/world changes. /// - /// The axis the position is being snapped onto. - /// The position being snapped. - /// Whether the snap should be smooth or in grid increments. - /// The snapped position. - /// The distance the position changes when snapped. - /// Whether the position is close enough to the axis to snap. - public bool SnapToAxis(Axes.Axis axisName, Vector3 position, bool isGridMode, out Vector3 snappedPosition, - out float delta) { - - // Grab the axis that we are snapping to. - Vector3 axis = Vector3.zero; - switch (axisName) { - case Axes.Axis.RIGHT: - axis = axes.right; - break; - case Axes.Axis.UP: - axis = axes.up; - break; - case Axes.Axis.FORWARD: - axis = axes.forward; - break; - } - - // Calculate the current model space stick threshold. We use a world space threshold so that we account for a - // user's available precision and perspective. - float stickThresholdModelSpace = AXIS_STICK_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale; - - // Project the position onto the axis. - snappedPosition = isGridMode ? - GridUtils.ProjectPointOntoLine(position, axis, origin) : - Math3d.ProjectPointOntoLine(position, axis, origin); - - // Calculate the change. - delta = Vector3.Distance(position, snappedPosition); - - // Return whether the change is within the allowed threshold. - return delta < stickThresholdModelSpace; + public class CoordinateSystem + { + // Defining thresholds in world space accounts for a user's available precision and their perspective. When a user + // can see less they are more likely to snap or stick but when a user is zoomed in and working in detail they need + // to be more precise to snap or stick because they can visualize what they are working on better. + + /// + /// The worldspace threshold at which a point will stick to an important position (origin, corner, etc). + /// + public const float STICK_THRESHOLD_WORLDSPACE = 0.03f; + + /// + /// The worldspace threshold at which a point will stick to an axis. + /// + protected const float AXIS_STICK_THRESHOLD_WORLDSPACE = 0.04f; + + /// + /// The default properties of a coordinate system. The defaults represent the universal coordinate system. + /// + protected readonly Vector3 DEFAULT_ORIGIN = Vector3.zero; + protected readonly Quaternion DEFAULT_ROTATION = Quaternion.identity; + protected readonly Axes DEFAULT_AXES = new Axes(Vector3.right, Vector3.up, Vector3.forward); + + /// + /// The center or anchor of the coordinate system in model space. In the universal coordinate system this is + /// Vector3.zero. + /// + protected Vector3 origin { get; private set; } + + /// + /// The right, up and forward axes represented as perpendicular unit vectors in model space. In the universal + /// coordinate system these are: Vector3.right, Vector3.up, Vector3.forward. + /// + protected Axes axes { get; private set; } + + /// + /// The rotation of the coordinate system defined as the rotational difference between the axes and the universal + /// coordinate system axes in model space. + /// + protected Quaternion rotation { get; private set; } + + /// + /// The scale of the coordinate system. Set to one for use with Matrix transformations. + /// + protected readonly Vector3 scale = Vector3.one; + + /// + /// The transform matrix for this coordinate system. + /// + private Matrix4x4 transformMatrix; + + protected void Setup(Vector3 origin, Quaternion rotation, Axes axes) + { + this.origin = origin; + this.rotation = rotation; + this.axes = axes; + + transformMatrix = Matrix4x4.TRS(origin, rotation, scale); + } + + /// + /// Snaps a position to the coordinate system and then returns the position in its original space. This allows us + /// to snap a model space position to an arbritrary grid. + /// + /// The position to snap. + /// The position snapped to the coordinate system in its original space. + public void SnapToGrid(Vector3 position, out Vector3 snappedPosition) + { + // Transform the position into the coordinate system space. + Vector3 positionInCoordinatesSystemSpace = transformMatrix.MultiplyPoint3x4(position); + + // Snap the transformed position to the coordinate system grid. + Vector3 snappedPositionInCoordinateSystemSpace = GridUtils.SnapToGrid(positionInCoordinatesSystemSpace); + + // Transform back to the positions original space. + snappedPosition = transformMatrix.inverse.MultiplyPoint3x4(snappedPositionInCoordinateSystemSpace); + } + + /// + /// Snaps a position to the origin. Returns false if the origin is not within the threshold to stick. + /// + /// The position being snapped. + /// The snapped position, this is either the origin or the original position. + /// Whether the position is close enough to the origin to stick. + public bool SnapToOrigin(Vector3 position, out Vector3 snappedPosition) + { + if (Vector3.Distance(position, origin) < STICK_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale) + { + snappedPosition = origin; + return true; + } + + snappedPosition = position; + return false; + } + + /// + /// Snaps a position to the nearest axis. Returns false if the position is not within the threshold to stick. + /// + /// Will either snap the position smoothly onto the nearest axis or to the nearest grid unit from the origin if + /// gridMode is on. + /// + /// The position to snap to an axis. + /// Whether the snap should be smooth or in grid increments. + /// The snapped position. + /// Whether the position is close enough to an axis to stick. + public bool SnapToAxes(Vector3 position, bool isGridMode, out Vector3 snappedPosition) + { + // Find the position snapped to the up axis. + Vector3 snappedUpPosition; + float upDelta; + bool withinUpThreshold = SnapToAxis(Axes.Axis.UP, position, isGridMode, out snappedUpPosition, out upDelta); + + // Find the position snapped to the forward axis. + Vector3 snappedForwardPosition; + float forwardDelta; + bool withinForwardThreshold = SnapToAxis(Axes.Axis.FORWARD, position, isGridMode, + out snappedForwardPosition, out forwardDelta); + + // Find the position snapped to the right axis. + Vector3 snappedRightPosition; + float rightDelta; + bool withinRightThreshold = SnapToAxis(Axes.Axis.RIGHT, position, isGridMode, out snappedRightPosition, + out rightDelta); + + // Determine which axis is closest. + float minDistance = Mathf.Min(upDelta, forwardDelta, rightDelta); + + if (minDistance == upDelta) + { + snappedPosition = snappedUpPosition; + return withinUpThreshold; + } + else if (minDistance == forwardDelta) + { + snappedPosition = snappedForwardPosition; + return withinForwardThreshold; + } + else + { + snappedPosition = snappedRightPosition; + return withinRightThreshold; + } + } + + /// + /// Snaps a position to a given axis. Returns false if the position is not within the threshold to snap. + /// + /// Will either snap the position smoothly onto the nearest axis or to the nearest grid unit from the origin if + /// gridMode is on. + /// + /// The axis the position is being snapped onto. + /// The position being snapped. + /// Whether the snap should be smooth or in grid increments. + /// The snapped position. + /// The distance the position changes when snapped. + /// Whether the position is close enough to the axis to snap. + public bool SnapToAxis(Axes.Axis axisName, Vector3 position, bool isGridMode, out Vector3 snappedPosition, + out float delta) + { + + // Grab the axis that we are snapping to. + Vector3 axis = Vector3.zero; + switch (axisName) + { + case Axes.Axis.RIGHT: + axis = axes.right; + break; + case Axes.Axis.UP: + axis = axes.up; + break; + case Axes.Axis.FORWARD: + axis = axes.forward; + break; + } + + // Calculate the current model space stick threshold. We use a world space threshold so that we account for a + // user's available precision and perspective. + float stickThresholdModelSpace = AXIS_STICK_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale; + + // Project the position onto the axis. + snappedPosition = isGridMode ? + GridUtils.ProjectPointOntoLine(position, axis, origin) : + Math3d.ProjectPointOntoLine(position, axis, origin); + + // Calculate the change. + delta = Vector3.Distance(position, snappedPosition); + + // Return whether the change is within the allowed threshold. + return delta < stickThresholdModelSpace; + } } - } } diff --git a/Assets/Scripts/alignment/FaceSnapSpace.cs b/Assets/Scripts/alignment/FaceSnapSpace.cs index 8e9b9899..f1316d59 100644 --- a/Assets/Scripts/alignment/FaceSnapSpace.cs +++ b/Assets/Scripts/alignment/FaceSnapSpace.cs @@ -21,455 +21,496 @@ using System.Linq; using UnityEngine; -namespace com.google.apps.peltzer.client.alignment { - /// - /// A FaceSnapSpace is a coordinate system that an MMesh can be orientated in and snapped to. - /// - /// A FaceSnapSpace is a 2D coordinate system overlayed on a face. A FaceSnapSpace is used to 'snap a face from a - /// source mesh onto a face of a target mesh' so its properties are defined by the target face. The axes are: the - /// most representative edge of the face, the face's normal and their cross product. The most representative edge - /// of a face is the one that is perpendicular that greatest number of other edges. This allows the FaceSnapSpace - /// to naturally align with as many edges as possible. Its rotation can then be defined as the rotational - /// difference from the unit axes to its axes and its origin is the center of the face. - /// - /// When an MMesh is snapped to a FaceSnapSpace its rotation is changed so that the source face is parallel with - /// the target face and its moved so that the faces are flush. The source face will automatically stick to - /// important points on the target face. Corners will stick to corners, edges to edges and centers to centers - /// if the distance between them is within a threshold. If GridMode is on the MMesh will also move in grid units - /// along the surface of the face. - /// - public class FaceSnapSpace : SnapSpace { - private const SnapType snapType = SnapType.FACE; - - /// - /// Floating point error threshold for comparing angles. - /// - private static readonly float DEGREE_ANGLE_ERROR_THRESHOLD = 0.01f; +namespace com.google.apps.peltzer.client.alignment +{ /// - /// The point on the targetFace that the reference point on the sourceFace will snap to initially. This is - /// important for displaying the correct UI snap hints. + /// A FaceSnapSpace is a coordinate system that an MMesh can be orientated in and snapped to. + /// + /// A FaceSnapSpace is a 2D coordinate system overlayed on a face. A FaceSnapSpace is used to 'snap a face from a + /// source mesh onto a face of a target mesh' so its properties are defined by the target face. The axes are: the + /// most representative edge of the face, the face's normal and their cross product. The most representative edge + /// of a face is the one that is perpendicular that greatest number of other edges. This allows the FaceSnapSpace + /// to naturally align with as many edges as possible. Its rotation can then be defined as the rotational + /// difference from the unit axes to its axes and its origin is the center of the face. + /// + /// When an MMesh is snapped to a FaceSnapSpace its rotation is changed so that the source face is parallel with + /// the target face and its moved so that the faces are flush. The source face will automatically stick to + /// important points on the target face. Corners will stick to corners, edges to edges and centers to centers + /// if the distance between them is within a threshold. If GridMode is on the MMesh will also move in grid units + /// along the surface of the face. /// - public Vector3 initialSnapPoint; - - public Vector3 snapPoint; - /// - /// The face key of the face being snapped. - /// - public FaceKey sourceFaceKey { get; private set; } - /// - /// The center of the face being snapped. - /// - public Vector3 sourceFaceCenter { get; private set; } - - // We keep a reference to these variables to avoid extraneously re-calculating anything while defining a - // FaceSnapSpace and snapping to the space. + public class FaceSnapSpace : SnapSpace + { + private const SnapType snapType = SnapType.FACE; + + /// + /// Floating point error threshold for comparing angles. + /// + private static readonly float DEGREE_ANGLE_ERROR_THRESHOLD = 0.01f; + /// + /// The point on the targetFace that the reference point on the sourceFace will snap to initially. This is + /// important for displaying the correct UI snap hints. + /// + public Vector3 initialSnapPoint; + + public Vector3 snapPoint; + /// + /// The face key of the face being snapped. + /// + public FaceKey sourceFaceKey { get; private set; } + /// + /// The center of the face being snapped. + /// + public Vector3 sourceFaceCenter { get; private set; } + + // We keep a reference to these variables to avoid extraneously re-calculating anything while defining a + // FaceSnapSpace and snapping to the space. + + /// + /// The face key of the face being snapped to. This face defines the properties of the FaceSnapSpace. + /// + public FaceKey targetFaceKey { get; private set; } + /// + /// The center of the face being snapped to, calculated as the geometric center of the target faces verts. + /// + public Vector3 targetFaceCenter { get; private set; } + /// + /// The mesh whose face is being snapped to this FaceSnapSpace. Any mesh should be able to be snapped to a + /// FaceSnapSpace because its an arbritrary coordinate system but we hold a reference to the specific mesh being + /// snapped to increase performance. We need this reference from the start because the sourceMesh is a preview and + /// its id is not in the spatialIndex. + /// + public MMesh sourceMesh; + /// + /// The vertices of the face being snapped to this FaceSnapSpace. Any face should be able to be snapped to a + /// FaceSnapSpace because its an arbritrary coordinate system but we hold a reference to the specific face being + /// snapped to increase performance. + /// + private ReadOnlyCollection sourceFaceVertexIds; + /// + /// The vertices of the face being snapped to. We hold a reference to these vertices to increase performance when + /// sticking to edges. + /// + private List targetFaceVertices; + /// + /// The edge from the target face used to define the rotation of this FaceSnapSpace. We keep a reference to this + /// edge to increase performance while determining the rotation of a mesh being snapped to this FaceSnapSpace. + /// + private EdgeInfo mostRepresentativeEdge; + /// + /// The effect for sticking to an edge. + /// + private ContinuousEdgeStickEffect continuousEdgeStickEffect; + /// + /// Whether we are currently edge sticking. + /// + private bool isEdgeSticking; + /// + /// The effect for sticking to the origin or a vertex; + /// + private ContinuousPointStickEffect continuousPointStickEffect; + /// + /// Whether we are currently sticking to the origin. + /// + private bool isPointSticking; + + public Vector3 sourceMeshOffset; + public Quaternion sourceMeshRotation; + + public FaceSnapSpace(MMesh sourceMesh, FaceKey sourceFaceKey, FaceKey targetFaceKey, Vector3 sourceFaceCenter, + Vector3 targetFaceCenter, Vector3 initialSnapPoint) + { + this.sourceMesh = sourceMesh; + this.sourceFaceKey = sourceFaceKey; + this.targetFaceKey = targetFaceKey; + this.sourceFaceCenter = sourceFaceCenter; + this.targetFaceCenter = targetFaceCenter; + this.initialSnapPoint = initialSnapPoint; + this.snapPoint = initialSnapPoint; + continuousEdgeStickEffect = new ContinuousEdgeStickEffect(); + continuousPointStickEffect = new ContinuousPointStickEffect(); + } - /// - /// The face key of the face being snapped to. This face defines the properties of the FaceSnapSpace. - /// - public FaceKey targetFaceKey { get; private set; } - /// - /// The center of the face being snapped to, calculated as the geometric center of the target faces verts. - /// - public Vector3 targetFaceCenter { get; private set; } - /// - /// The mesh whose face is being snapped to this FaceSnapSpace. Any mesh should be able to be snapped to a - /// FaceSnapSpace because its an arbritrary coordinate system but we hold a reference to the specific mesh being - /// snapped to increase performance. We need this reference from the start because the sourceMesh is a preview and - /// its id is not in the spatialIndex. - /// - public MMesh sourceMesh; - /// - /// The vertices of the face being snapped to this FaceSnapSpace. Any face should be able to be snapped to a - /// FaceSnapSpace because its an arbritrary coordinate system but we hold a reference to the specific face being - /// snapped to increase performance. - /// - private ReadOnlyCollection sourceFaceVertexIds; - /// - /// The vertices of the face being snapped to. We hold a reference to these vertices to increase performance when - /// sticking to edges. - /// - private List targetFaceVertices; - /// - /// The edge from the target face used to define the rotation of this FaceSnapSpace. We keep a reference to this - /// edge to increase performance while determining the rotation of a mesh being snapped to this FaceSnapSpace. - /// - private EdgeInfo mostRepresentativeEdge; - /// - /// The effect for sticking to an edge. - /// - private ContinuousEdgeStickEffect continuousEdgeStickEffect; - /// - /// Whether we are currently edge sticking. - /// - private bool isEdgeSticking; - /// - /// The effect for sticking to the origin or a vertex; - /// - private ContinuousPointStickEffect continuousPointStickEffect; - /// - /// Whether we are currently sticking to the origin. - /// - private bool isPointSticking; - - public Vector3 sourceMeshOffset; - public Quaternion sourceMeshRotation; - - public FaceSnapSpace(MMesh sourceMesh, FaceKey sourceFaceKey, FaceKey targetFaceKey, Vector3 sourceFaceCenter, - Vector3 targetFaceCenter, Vector3 initialSnapPoint) { - this.sourceMesh = sourceMesh; - this.sourceFaceKey = sourceFaceKey; - this.targetFaceKey = targetFaceKey; - this.sourceFaceCenter = sourceFaceCenter; - this.targetFaceCenter = targetFaceCenter; - this.initialSnapPoint = initialSnapPoint; - this.snapPoint = initialSnapPoint; - continuousEdgeStickEffect = new ContinuousEdgeStickEffect(); - continuousPointStickEffect = new ContinuousPointStickEffect(); - } + /// + /// Calculates the origin, rotation and axes of the FaceSnapSpace and any other information we can calculate once + /// and hold onto. + /// + public override void Execute() + { + MMesh targetMesh = PeltzerMain.Instance.model.GetMesh(targetFaceKey.meshId); + Face targetFace = targetMesh.GetFace(targetFaceKey.faceId); + + targetFaceVertices = new List(targetFace.vertexIds.Count); + for (int i = 0; i < targetFace.vertexIds.Count; i++) + { + targetFaceVertices.Add(targetMesh.VertexPositionInModelCoords(targetFace.vertexIds[i])); + } - /// - /// Calculates the origin, rotation and axes of the FaceSnapSpace and any other information we can calculate once - /// and hold onto. - /// - public override void Execute() { - MMesh targetMesh = PeltzerMain.Instance.model.GetMesh(targetFaceKey.meshId); - Face targetFace = targetMesh.GetFace(targetFaceKey.faceId); + mostRepresentativeEdge = MeshMath.FindMostRepresentativeEdge(targetFaceVertices); + Axes axes = Axes.FindAxesForAFace(targetFaceVertices); + Setup(targetFaceCenter, Axes.FromToRotation(Axes.identity, axes), axes); - targetFaceVertices = new List(targetFace.vertexIds.Count); - for (int i = 0; i < targetFace.vertexIds.Count; i++) { - targetFaceVertices.Add(targetMesh.VertexPositionInModelCoords(targetFace.vertexIds[i])); - } + Face sourceFace = sourceMesh.GetFace(sourceFaceKey.faceId); + sourceFaceVertexIds = sourceFace.vertexIds; + } - mostRepresentativeEdge = MeshMath.FindMostRepresentativeEdge(targetFaceVertices); - Axes axes = Axes.FindAxesForAFace(targetFaceVertices); - Setup(targetFaceCenter, Axes.FromToRotation(Axes.identity, axes), axes); + /// + /// Checks if the targetMeshId still exists in the model. If it does the snap is still valid. + /// + /// Whether the targetMeshId exists still. + public override bool IsValid() + { + return PeltzerMain.Instance.model.HasMesh(targetFaceKey.meshId); + } - Face sourceFace = sourceMesh.GetFace(sourceFaceKey.faceId); - sourceFaceVertexIds = sourceFace.vertexIds; - } + /// + /// Translates a transform into the SnapSpace. + /// + /// When snapping a position of a mesh to a FaceSnapSpace we don't actually want the position to be snapped to the + /// FaceSnapSpace but a reference position on a face of the mesh. So we determine where the reference point is + /// before the snap starts, find where the reference point should be snapped to and then move the position by the + /// reference delta. + /// + /// The reference point is snapped to a FaceSnapSpace by: + /// 1) Projecting the reference onto the target face. + /// 2) Sticking the reference to the origin if its close enough. + /// 3) Otherwise, snapping to a grid defined by the coordinate system if grid mode is on. + /// + /// After we've found the snapped reference point and if we didn't snap to the origin we check to see if any + /// edge/corner on the source face is close enough to an edge/corner on the target face to stick them together. + /// If they are we determine the delta to move the mesh so the edge/corner stick and update the reference point. + /// + /// A rotation is snapped to a FaceSnapSpace by finding the rotation needed to align the source face with + /// the FaceSnapSpace and then applying that delta to the rotation. When we edge/corner stick the source face + /// might need to be rotated to align correctly in which case that delta will be added to the rotation as well. + /// + /// The position of the mesh being snapped. + /// The rotation of the mesh being snapped. + /// The snapped position and rotation. + public override SnapTransform Snap(Vector3 position, Quaternion rotation) + { + sourceMeshOffset = position; + sourceMeshRotation = rotation; + List sourceFaceVerticesBeforeSnap = + MeshMath.CalculateVertexPositions(sourceFaceVertexIds, sourceMesh, position, rotation); + + // The reference point on the sourceFace. + Vector3 reference = MeshMath.CalculateGeometricCenter(sourceFaceVerticesBeforeSnap); + + // In face snapping we always snap to the surface of the face. So start snapping off by projecting the reference + // point onto a plane along the sourceFace. We can define this plane easily by using axes.foward which is the + // face's normal and the origin which is a point on the plane. + Vector3 snappedReference = Math3d.ProjectPointOnPlane(axes.forward, origin, reference); + + // Determine what the rotation of the mesh needs to be if we just wanted to align the sourceFace with the + // targetFace plane. This will be the final rotation if we don't edge/corner stick. Rotation is applied around + // the offset of the mesh which is position. + Quaternion rotationAfterAlignmentWithTargetFacePlane = + FindSourceMeshRotationForFaceSnap(sourceFaceVerticesBeforeSnap, rotation); + + // Rotations are applied to meshes first. So apply our tempRotation to the reference point to see where it would + // be when we rotate the mesh to align with the projected reference. + Vector3 meshSpaceReference = Quaternion.Inverse(rotation) * (reference - position); + Vector3 referenceRotatedToAlignWithTargetFace = + (rotationAfterAlignmentWithTargetFacePlane * meshSpaceReference) + position; + + // Check if we are close enough to the origin (the center of the target face in FaceSnapSpaces) to snap the + // reference there. If we are we won't do anything else. + if (SnapToOrigin(snappedReference, out snappedReference)) + { + // We don't do any further rotational snapping so we can just use the tempRotation we already found. + Quaternion snappedRotation = rotationAfterAlignmentWithTargetFacePlane; + // Find the reference delta which is the the vector from the reference point once its rotated for the snap. To + // the position it should be snapped to. Then apply this delta to the position. + Vector3 referenceDelta = (snappedReference - referenceRotatedToAlignWithTargetFace); + Vector3 snappedPosition = position + referenceDelta; + isEdgeSticking = false; + UXEffectManager.GetEffectManager().EndEffect(continuousEdgeStickEffect); + + if (!isPointSticking) + { + isPointSticking = true; + UXEffectManager.GetEffectManager().StartEffect(continuousPointStickEffect); + } + continuousPointStickEffect.UpdateFromPoint(origin); + + return new SnapTransform(snappedPosition, snappedRotation); + } + else + { + // If we are in grid mode snap the reference to the nearest grid point for this coordinate system. This is + // guaranteed to be on the surface of the face. + if (PeltzerMain.Instance.peltzerController.isBlockMode) + { + SnapToGrid(snappedReference, out snappedReference); + } + + // Determine where the position would be at this point for where the reference point is. We want to + // mathematically perform the snap and then see if we should stick any edges/corners together. + + // At this point we've projected the reference onto the target face plane and potentially snapped the + // projected reference onto the nearest grid point. Now we want to check if after the projection and grid snap + // if any edges or corners on the source face are close enough to any edges/corners on the target face to + // stick them together. So we need to find the reference delta at this point and then update position to where + // it temporarily should be after the previous translations. + Vector3 referenceDelta = (snappedReference - referenceRotatedToAlignWithTargetFace); + Vector3 positionAfterProjectionRotationMaybeGridSnap = position + referenceDelta; + + // Now that we have the temporary position and rotation, at this point in the snap, we can calculate + // where the positions of the vertices of the sourceFace should be and then check if we should edge/corner + // stick. + List sourceFaceVerticesAfterProjectionRotationMaybeGridSnap = + MeshMath.CalculateVertexPositions(sourceFaceVertexIds, sourceMesh, + positionAfterProjectionRotationMaybeGridSnap, rotationAfterAlignmentWithTargetFacePlane); + + // Find the snappedPosition and snappedRotation if we were to edge/corner stick. If we don't stick our current + // temp position and rotation will be returned as the final snaps. + Vector3 snappedPosition; + Quaternion snappedRotation; + bool edgeSnapped; + EdgeInfo targetEdge; + bool vertexSnapped; + Vector3 targetVertex; + if (StickToEdgeOrCorner(positionAfterProjectionRotationMaybeGridSnap, + rotationAfterAlignmentWithTargetFacePlane, sourceFaceVerticesAfterProjectionRotationMaybeGridSnap, + snappedReference, out snappedPosition, out snappedRotation, out edgeSnapped, out targetEdge, out vertexSnapped, + out targetVertex)) + { + if (edgeSnapped) + { + if (!isEdgeSticking) + { + isEdgeSticking = true; + UXEffectManager.GetEffectManager().StartEffect(continuousEdgeStickEffect); + } + continuousEdgeStickEffect.UpdateFromEdge(targetEdge); + } + else if (isEdgeSticking) + { + isEdgeSticking = false; + UXEffectManager.GetEffectManager().EndEffect(continuousEdgeStickEffect); + } + + if (vertexSnapped) + { + if (!isPointSticking) + { + isPointSticking = true; + UXEffectManager.GetEffectManager().StartEffect(continuousPointStickEffect); + } + continuousPointStickEffect.UpdateFromPoint(targetVertex); + } + else if (isPointSticking) + { + isPointSticking = false; + UXEffectManager.GetEffectManager().EndEffect(continuousPointStickEffect); + } + + return new SnapTransform(snappedPosition, snappedRotation); + } + else + { + isEdgeSticking = false; + UXEffectManager.GetEffectManager().EndEffect(continuousEdgeStickEffect); + isPointSticking = false; + UXEffectManager.GetEffectManager().EndEffect(continuousPointStickEffect); + + // We didn't corner or edge snap so we just use the rotation used to align with the target face plane and + // the position needed to move the mesh so that the reference is on the target face plane. + return new SnapTransform( + positionAfterProjectionRotationMaybeGridSnap, + rotationAfterAlignmentWithTargetFacePlane); + } + } + } - /// - /// Checks if the targetMeshId still exists in the model. If it does the snap is still valid. - /// - /// Whether the targetMeshId exists still. - public override bool IsValid() { - return PeltzerMain.Instance.model.HasMesh(targetFaceKey.meshId); - } + /// + /// Handles stopping snap logic maintained by the FaceSnapSpace such as hints for edge and vertex sticking. + /// + public override void StopSnap() + { + if (isEdgeSticking) + { + UXEffectManager.GetEffectManager().EndEffect(continuousEdgeStickEffect); + } - /// - /// Translates a transform into the SnapSpace. - /// - /// When snapping a position of a mesh to a FaceSnapSpace we don't actually want the position to be snapped to the - /// FaceSnapSpace but a reference position on a face of the mesh. So we determine where the reference point is - /// before the snap starts, find where the reference point should be snapped to and then move the position by the - /// reference delta. - /// - /// The reference point is snapped to a FaceSnapSpace by: - /// 1) Projecting the reference onto the target face. - /// 2) Sticking the reference to the origin if its close enough. - /// 3) Otherwise, snapping to a grid defined by the coordinate system if grid mode is on. - /// - /// After we've found the snapped reference point and if we didn't snap to the origin we check to see if any - /// edge/corner on the source face is close enough to an edge/corner on the target face to stick them together. - /// If they are we determine the delta to move the mesh so the edge/corner stick and update the reference point. - /// - /// A rotation is snapped to a FaceSnapSpace by finding the rotation needed to align the source face with - /// the FaceSnapSpace and then applying that delta to the rotation. When we edge/corner stick the source face - /// might need to be rotated to align correctly in which case that delta will be added to the rotation as well. - /// - /// The position of the mesh being snapped. - /// The rotation of the mesh being snapped. - /// The snapped position and rotation. - public override SnapTransform Snap(Vector3 position, Quaternion rotation) { - sourceMeshOffset = position; - sourceMeshRotation = rotation; - List sourceFaceVerticesBeforeSnap = - MeshMath.CalculateVertexPositions(sourceFaceVertexIds, sourceMesh, position, rotation); - - // The reference point on the sourceFace. - Vector3 reference = MeshMath.CalculateGeometricCenter(sourceFaceVerticesBeforeSnap); - - // In face snapping we always snap to the surface of the face. So start snapping off by projecting the reference - // point onto a plane along the sourceFace. We can define this plane easily by using axes.foward which is the - // face's normal and the origin which is a point on the plane. - Vector3 snappedReference = Math3d.ProjectPointOnPlane(axes.forward, origin, reference); - - // Determine what the rotation of the mesh needs to be if we just wanted to align the sourceFace with the - // targetFace plane. This will be the final rotation if we don't edge/corner stick. Rotation is applied around - // the offset of the mesh which is position. - Quaternion rotationAfterAlignmentWithTargetFacePlane = - FindSourceMeshRotationForFaceSnap(sourceFaceVerticesBeforeSnap, rotation); - - // Rotations are applied to meshes first. So apply our tempRotation to the reference point to see where it would - // be when we rotate the mesh to align with the projected reference. - Vector3 meshSpaceReference = Quaternion.Inverse(rotation) * (reference - position); - Vector3 referenceRotatedToAlignWithTargetFace = - (rotationAfterAlignmentWithTargetFacePlane * meshSpaceReference) + position; - - // Check if we are close enough to the origin (the center of the target face in FaceSnapSpaces) to snap the - // reference there. If we are we won't do anything else. - if (SnapToOrigin(snappedReference, out snappedReference)) { - // We don't do any further rotational snapping so we can just use the tempRotation we already found. - Quaternion snappedRotation = rotationAfterAlignmentWithTargetFacePlane; - // Find the reference delta which is the the vector from the reference point once its rotated for the snap. To - // the position it should be snapped to. Then apply this delta to the position. - Vector3 referenceDelta = (snappedReference - referenceRotatedToAlignWithTargetFace); - Vector3 snappedPosition = position + referenceDelta; - isEdgeSticking = false; - UXEffectManager.GetEffectManager().EndEffect(continuousEdgeStickEffect); - - if (!isPointSticking) { - isPointSticking = true; - UXEffectManager.GetEffectManager().StartEffect(continuousPointStickEffect); - } - continuousPointStickEffect.UpdateFromPoint(origin); - - return new SnapTransform(snappedPosition, snappedRotation); - } else { - // If we are in grid mode snap the reference to the nearest grid point for this coordinate system. This is - // guaranteed to be on the surface of the face. - if (PeltzerMain.Instance.peltzerController.isBlockMode) { - SnapToGrid(snappedReference, out snappedReference); + if (isPointSticking) + { + UXEffectManager.GetEffectManager().EndEffect(continuousPointStickEffect); + } } - // Determine where the position would be at this point for where the reference point is. We want to - // mathematically perform the snap and then see if we should stick any edges/corners together. - - // At this point we've projected the reference onto the target face plane and potentially snapped the - // projected reference onto the nearest grid point. Now we want to check if after the projection and grid snap - // if any edges or corners on the source face are close enough to any edges/corners on the target face to - // stick them together. So we need to find the reference delta at this point and then update position to where - // it temporarily should be after the previous translations. - Vector3 referenceDelta = (snappedReference - referenceRotatedToAlignWithTargetFace); - Vector3 positionAfterProjectionRotationMaybeGridSnap = position + referenceDelta; - - // Now that we have the temporary position and rotation, at this point in the snap, we can calculate - // where the positions of the vertices of the sourceFace should be and then check if we should edge/corner - // stick. - List sourceFaceVerticesAfterProjectionRotationMaybeGridSnap = - MeshMath.CalculateVertexPositions(sourceFaceVertexIds, sourceMesh, - positionAfterProjectionRotationMaybeGridSnap, rotationAfterAlignmentWithTargetFacePlane); - - // Find the snappedPosition and snappedRotation if we were to edge/corner stick. If we don't stick our current - // temp position and rotation will be returned as the final snaps. - Vector3 snappedPosition; - Quaternion snappedRotation; - bool edgeSnapped; - EdgeInfo targetEdge; - bool vertexSnapped; - Vector3 targetVertex; - if (StickToEdgeOrCorner(positionAfterProjectionRotationMaybeGridSnap, - rotationAfterAlignmentWithTargetFacePlane, sourceFaceVerticesAfterProjectionRotationMaybeGridSnap, - snappedReference, out snappedPosition, out snappedRotation, out edgeSnapped, out targetEdge, out vertexSnapped, - out targetVertex)) { - if (edgeSnapped) { - if (!isEdgeSticking) { - isEdgeSticking = true; - UXEffectManager.GetEffectManager().StartEffect(continuousEdgeStickEffect); + /// + /// Sticks an edge/corner on the source face to an edge/corner on the target face if they are close enough. + /// + /// The position being snapped. + /// The rotation being snapped. + /// + /// The vertices of the face being snapped at the given position and rotation. + /// + /// The position of the reference point at the given position and rotation. + /// The updated position that sticks a corner/edge. + /// The udpated rotation that sticks a corner/edge. + /// Whether a corner/edge pair were close enough to stick. + private bool StickToEdgeOrCorner(Vector3 position, Quaternion rotation, IEnumerable sourceFaceVertices, + Vector3 reference, out Vector3 snappedPosition, out Quaternion snappedRotation, out bool edgeSnapped, + out EdgeInfo targetEdge, out bool vertexSnapped, out Vector3 targetVertex) + { + EdgePair closestEdgePair; + bool shouldEdgeSnap = + MeshMath.MaybeFindClosestEdgePair(sourceFaceVertices, targetFaceVertices, out closestEdgePair); + + targetVertex = new Vector3(); + vertexSnapped = false; + + if (!shouldEdgeSnap || + closestEdgePair.separation > STICK_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale) + { + snappedPosition = position; + snappedRotation = rotation; + targetEdge = new EdgeInfo(); + edgeSnapped = false; + return false; } - continuousEdgeStickEffect.UpdateFromEdge(targetEdge); - } else if (isEdgeSticking) { - isEdgeSticking = false; - UXEffectManager.GetEffectManager().EndEffect(continuousEdgeStickEffect); - } - - if (vertexSnapped) { - if (!isPointSticking) { - isPointSticking = true; - UXEffectManager.GetEffectManager().StartEffect(continuousPointStickEffect); + + EdgeInfo sourceEdge = closestEdgePair.fromEdge; + targetEdge = closestEdgePair.toEdge; + edgeSnapped = true; + + // Switch the direction of snapEdge to minimize the angle between sourceFaceEdge and targetFaceEdge. + // The angle between snapEdge and -snapEdge is 180 degrees. So if the angle between sourceEdge and snapEdge is + // greater than 90 the angle between sourceEdge and -snapEdge will be less than 90 and therefore the minimized + // angle. + if (90.0f - Vector3.Angle(sourceEdge.edgeVector, targetEdge.edgeVector) < DEGREE_ANGLE_ERROR_THRESHOLD) + { + targetEdge.edgeStart = targetEdge.edgeStart + targetEdge.edgeVector; + targetEdge.edgeVector = -targetEdge.edgeVector; } - continuousPointStickEffect.UpdateFromPoint(targetVertex); - } else if (isPointSticking) { - isPointSticking = false; - UXEffectManager.GetEffectManager().EndEffect(continuousPointStickEffect); - } - - return new SnapTransform(snappedPosition, snappedRotation); - } else { - isEdgeSticking = false; - UXEffectManager.GetEffectManager().EndEffect(continuousEdgeStickEffect); - isPointSticking = false; - UXEffectManager.GetEffectManager().EndEffect(continuousPointStickEffect); - - // We didn't corner or edge snap so we just use the rotation used to align with the target face plane and - // the position needed to move the mesh so that the reference is on the target face plane. - return new SnapTransform( - positionAfterProjectionRotationMaybeGridSnap, - rotationAfterAlignmentWithTargetFacePlane); - } - } - } - /// - /// Handles stopping snap logic maintained by the FaceSnapSpace such as hints for edge and vertex sticking. - /// - public override void StopSnap() { - if (isEdgeSticking) { - UXEffectManager.GetEffectManager().EndEffect(continuousEdgeStickEffect); - } - - if (isPointSticking) { - UXEffectManager.GetEffectManager().EndEffect(continuousPointStickEffect); - } - } + // Find the rotational difference between the two edges. + Quaternion edgeRotDelta = Quaternion.FromToRotation(sourceEdge.edgeVector, targetEdge.edgeVector); + Vector3 rotatedSourceEdgeStart = (edgeRotDelta * (sourceEdge.edgeStart - position)) + position; + + // Find the position of an edge point if snapped onto the line. + Vector3 snappedSourceEdgeStart = Math3d.ProjectPointOntoLine(rotatedSourceEdgeStart, targetEdge.edgeVector, + targetEdge.edgeStart); + + // Find the difference and apply it to positionToSnap. + Vector3 rotatedReference = (edgeRotDelta * (reference - position)) + position; + Vector3 snappedReference = rotatedReference + (snappedSourceEdgeStart - rotatedSourceEdgeStart); + + // Check to see how far apart the vertices of the edges are from each other. If any set of vertices are close + // enough we'll snap them together to corner snap. + Vector3 targetEdgeStart = targetEdge.edgeStart; + Vector3 targetEdgeEnd = targetEdgeStart + targetEdge.edgeVector; + + // Find the source edge points after they have been snapped onto the target edge. + Vector3 sourceEdgeStart = snappedSourceEdgeStart; + Vector3 sourceEdgeEnd = sourceEdgeStart + (edgeRotDelta * sourceEdge.edgeVector); + + float startToStartDelta = Vector3.Distance(sourceEdgeStart, targetEdgeStart); + float endToEndDelta = Vector3.Distance(sourceEdgeEnd, targetEdgeEnd); + float sourceStartToTargetEndDelta = Vector3.Distance(sourceEdgeStart, targetEdgeEnd); + float sourceEndToTargetStartDelta = Vector3.Distance(sourceEdgeEnd, targetEdgeStart); + + float minDelta = + Mathf.Min(startToStartDelta, endToEndDelta, sourceStartToTargetEndDelta, sourceEndToTargetStartDelta); + + // Slide the position over so it snaps to the corner. + if (minDelta < STICK_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale) + { + if (minDelta == startToStartDelta) + { + snappedReference += (targetEdgeStart - sourceEdgeStart); + targetVertex = targetEdgeStart; + } + else if (minDelta == endToEndDelta) + { + snappedReference += (targetEdgeEnd - sourceEdgeEnd); + targetVertex = targetEdgeEnd; + } + else if (minDelta == sourceStartToTargetEndDelta) + { + snappedReference += (targetEdgeEnd - sourceEdgeStart); + targetVertex = targetEdgeEnd; + } + else + { + snappedReference += (targetEdgeStart - sourceEdgeEnd); + targetVertex = targetEdgeStart; + } + + vertexSnapped = true; + edgeSnapped = false; + } - /// - /// Sticks an edge/corner on the source face to an edge/corner on the target face if they are close enough. - /// - /// The position being snapped. - /// The rotation being snapped. - /// - /// The vertices of the face being snapped at the given position and rotation. - /// - /// The position of the reference point at the given position and rotation. - /// The updated position that sticks a corner/edge. - /// The udpated rotation that sticks a corner/edge. - /// Whether a corner/edge pair were close enough to stick. - private bool StickToEdgeOrCorner(Vector3 position, Quaternion rotation, IEnumerable sourceFaceVertices, - Vector3 reference, out Vector3 snappedPosition, out Quaternion snappedRotation, out bool edgeSnapped, - out EdgeInfo targetEdge, out bool vertexSnapped, out Vector3 targetVertex) { - EdgePair closestEdgePair; - bool shouldEdgeSnap = - MeshMath.MaybeFindClosestEdgePair(sourceFaceVertices, targetFaceVertices, out closestEdgePair); - - targetVertex = new Vector3(); - vertexSnapped = false; - - if (!shouldEdgeSnap || - closestEdgePair.separation > STICK_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale) { - snappedPosition = position; - snappedRotation = rotation; - targetEdge = new EdgeInfo(); - edgeSnapped = false; - return false; - } - - EdgeInfo sourceEdge = closestEdgePair.fromEdge; - targetEdge = closestEdgePair.toEdge; - edgeSnapped = true; - - // Switch the direction of snapEdge to minimize the angle between sourceFaceEdge and targetFaceEdge. - // The angle between snapEdge and -snapEdge is 180 degrees. So if the angle between sourceEdge and snapEdge is - // greater than 90 the angle between sourceEdge and -snapEdge will be less than 90 and therefore the minimized - // angle. - if (90.0f - Vector3.Angle(sourceEdge.edgeVector, targetEdge.edgeVector) < DEGREE_ANGLE_ERROR_THRESHOLD) { - targetEdge.edgeStart = targetEdge.edgeStart + targetEdge.edgeVector; - targetEdge.edgeVector = -targetEdge.edgeVector; - } - - // Find the rotational difference between the two edges. - Quaternion edgeRotDelta = Quaternion.FromToRotation(sourceEdge.edgeVector, targetEdge.edgeVector); - Vector3 rotatedSourceEdgeStart = (edgeRotDelta * (sourceEdge.edgeStart - position)) + position; - - // Find the position of an edge point if snapped onto the line. - Vector3 snappedSourceEdgeStart = Math3d.ProjectPointOntoLine(rotatedSourceEdgeStart, targetEdge.edgeVector, - targetEdge.edgeStart); - - // Find the difference and apply it to positionToSnap. - Vector3 rotatedReference = (edgeRotDelta * (reference - position)) + position; - Vector3 snappedReference = rotatedReference + (snappedSourceEdgeStart - rotatedSourceEdgeStart); - - // Check to see how far apart the vertices of the edges are from each other. If any set of vertices are close - // enough we'll snap them together to corner snap. - Vector3 targetEdgeStart = targetEdge.edgeStart; - Vector3 targetEdgeEnd = targetEdgeStart + targetEdge.edgeVector; - - // Find the source edge points after they have been snapped onto the target edge. - Vector3 sourceEdgeStart = snappedSourceEdgeStart; - Vector3 sourceEdgeEnd = sourceEdgeStart + (edgeRotDelta * sourceEdge.edgeVector); - - float startToStartDelta = Vector3.Distance(sourceEdgeStart, targetEdgeStart); - float endToEndDelta = Vector3.Distance(sourceEdgeEnd, targetEdgeEnd); - float sourceStartToTargetEndDelta = Vector3.Distance(sourceEdgeStart, targetEdgeEnd); - float sourceEndToTargetStartDelta = Vector3.Distance(sourceEdgeEnd, targetEdgeStart); - - float minDelta = - Mathf.Min(startToStartDelta, endToEndDelta, sourceStartToTargetEndDelta, sourceEndToTargetStartDelta); - - // Slide the position over so it snaps to the corner. - if (minDelta < STICK_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale) { - if (minDelta == startToStartDelta) { - snappedReference += (targetEdgeStart - sourceEdgeStart); - targetVertex = targetEdgeStart; - } else if (minDelta == endToEndDelta) { - snappedReference += (targetEdgeEnd - sourceEdgeEnd); - targetVertex = targetEdgeEnd; - } else if (minDelta == sourceStartToTargetEndDelta) { - snappedReference += (targetEdgeEnd - sourceEdgeStart); - targetVertex = targetEdgeEnd; - } else { - snappedReference += (targetEdgeStart - sourceEdgeEnd); - targetVertex = targetEdgeStart; + snappedPosition = position + (snappedReference - rotatedReference); + snappedRotation = Math3d.Normalize(edgeRotDelta * rotation); + return true; } - vertexSnapped = true; - edgeSnapped = false; - } + /// + /// Finds the rotation of the sourceMesh given the snapGrid properties. It does this by finding the Axes + /// representing the sourceFace, then the rotation from the sourceFace to the snapFace and applying the + /// rotational delta to the rotation of the sourceMesh. + /// + /// + /// The vertices representing the sourceFace which is being rotated to be flush with the snapFace.451 + /// + /// The rotation of the sourceMesh the sourceFace belongs to. + /// The new rotation for the sourceMesh such that the sourceFace and snapFace are flush. + public Quaternion FindSourceMeshRotationForFaceSnap(List coplanarSourceFaceVertices, + Quaternion sourceMeshRotation) + { + // To face snap we need to rotate the sourceMesh so that the sourceFace is flush with the targetFace. + // To start we find the rotational difference between the faces and then we apply that difference to the + // rotation of the sourceMesh. To find the rotational difference between the faces we can find the rotational + // difference between the axes of the sourceFace at the start and what the final sourceFace.axes need to be to + // be flush with the targetFace. The finalSourceFaceAxes is the axes of the targetFace but inverted so that the + // forward (targerFace normal) points in the opposite direction because faces that are flush/aligned have + // inverted normals. + Axes finalSourceFaceAxes = new Axes(-axes.right, -axes.up, -axes.forward); + + // Find the axes that represent the sourceFace at the start. + Vector3 startingSourceForward = Axes.FindForwardAxis(coplanarSourceFaceVertices); + + // Choose the axis.Right that is closest to the axis.Right of the snapFace to minimize the amount of the + // rotation we apply to make the faces align. We will find the edge that is closest to the edge we used to + // define snapAxes.right. + Vector3 startingSourceRight = + MeshMath.ClosestEdgeToEdge(coplanarSourceFaceVertices, mostRepresentativeEdge).normalized; + Vector3 startingSourceUp = Axes.FindUpAxis(startingSourceRight, startingSourceForward); + Axes startingSourceFaceAxes = new Axes(startingSourceRight, startingSourceUp, startingSourceForward); + + Quaternion sourceRotDelta = Axes.FromToRotation(startingSourceFaceAxes, finalSourceFaceAxes); + sourceRotDelta *= sourceMeshRotation; + + return sourceRotDelta; + } - snappedPosition = position + (snappedReference - rotatedReference); - snappedRotation = Math3d.Normalize(edgeRotDelta * rotation); - return true; - } + /// + /// Checks if another SnapSpace is equivalent to this space. FaceSnapSpaces are equivalent if they have the same + /// sourceFaceKey and TargetFaceKey. + /// + /// The other SnapSpace. + /// Whether they are equal. + public override bool Equals(SnapSpace otherSpace) + { + if (otherSpace == null || otherSpace.SnapType != snapType) + { + return false; + } - /// - /// Finds the rotation of the sourceMesh given the snapGrid properties. It does this by finding the Axes - /// representing the sourceFace, then the rotation from the sourceFace to the snapFace and applying the - /// rotational delta to the rotation of the sourceMesh. - /// - /// - /// The vertices representing the sourceFace which is being rotated to be flush with the snapFace.451 - /// - /// The rotation of the sourceMesh the sourceFace belongs to. - /// The new rotation for the sourceMesh such that the sourceFace and snapFace are flush. - public Quaternion FindSourceMeshRotationForFaceSnap(List coplanarSourceFaceVertices, - Quaternion sourceMeshRotation) { - // To face snap we need to rotate the sourceMesh so that the sourceFace is flush with the targetFace. - // To start we find the rotational difference between the faces and then we apply that difference to the - // rotation of the sourceMesh. To find the rotational difference between the faces we can find the rotational - // difference between the axes of the sourceFace at the start and what the final sourceFace.axes need to be to - // be flush with the targetFace. The finalSourceFaceAxes is the axes of the targetFace but inverted so that the - // forward (targerFace normal) points in the opposite direction because faces that are flush/aligned have - // inverted normals. - Axes finalSourceFaceAxes = new Axes(-axes.right, -axes.up, -axes.forward); - - // Find the axes that represent the sourceFace at the start. - Vector3 startingSourceForward = Axes.FindForwardAxis(coplanarSourceFaceVertices); - - // Choose the axis.Right that is closest to the axis.Right of the snapFace to minimize the amount of the - // rotation we apply to make the faces align. We will find the edge that is closest to the edge we used to - // define snapAxes.right. - Vector3 startingSourceRight = - MeshMath.ClosestEdgeToEdge(coplanarSourceFaceVertices, mostRepresentativeEdge).normalized; - Vector3 startingSourceUp = Axes.FindUpAxis(startingSourceRight, startingSourceForward); - Axes startingSourceFaceAxes = new Axes(startingSourceRight, startingSourceUp, startingSourceForward); - - Quaternion sourceRotDelta = Axes.FromToRotation(startingSourceFaceAxes, finalSourceFaceAxes); - sourceRotDelta *= sourceMeshRotation; - - return sourceRotDelta; - } + FaceSnapSpace otherFaceSnapSpace = (FaceSnapSpace)otherSpace; + if (sourceFaceKey == otherFaceSnapSpace.sourceFaceKey + && targetFaceKey == otherFaceSnapSpace.targetFaceKey) + { + return true; + } - /// - /// Checks if another SnapSpace is equivalent to this space. FaceSnapSpaces are equivalent if they have the same - /// sourceFaceKey and TargetFaceKey. - /// - /// The other SnapSpace. - /// Whether they are equal. - public override bool Equals(SnapSpace otherSpace) { - if (otherSpace == null || otherSpace.SnapType != snapType) { - return false; - } - - FaceSnapSpace otherFaceSnapSpace = (FaceSnapSpace)otherSpace; - if (sourceFaceKey == otherFaceSnapSpace.sourceFaceKey - && targetFaceKey == otherFaceSnapSpace.targetFaceKey) { - return true; - } - - return false; - } + return false; + } - public override SnapType SnapType { get { return snapType; } } - } + public override SnapType SnapType { get { return snapType; } } + } } diff --git a/Assets/Scripts/alignment/MeshSnapSpace.cs b/Assets/Scripts/alignment/MeshSnapSpace.cs index 4360b81b..5e00f928 100644 --- a/Assets/Scripts/alignment/MeshSnapSpace.cs +++ b/Assets/Scripts/alignment/MeshSnapSpace.cs @@ -19,145 +19,167 @@ using UnityEngine; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.alignment { - /// - /// A MeshSnapSpace is a coordinate system that an MMesh can be orientated in and snapped to. - /// - /// A MeshSnapSpace is conceptually the same as the universal coordinate system but with an arbitrary rotation and - /// origin. A MeshSnapSpace is used to 'snap to a target mesh' so its properties are defined by the target mesh. - /// Its rotation is the mesh's rotation, its origin is the mesh's offset and its axes are the mesh's local right, - /// up and forward vectors. - /// - /// When an MMesh is snapped to a MeshSnapSpace its rotation is snapped to the nearest 90 degrees of the space's - /// rotation and if GridMode is on the MMesh is moved so its bounding box lines up with the MeshSnapGrid. The - /// center of the mesh being snapped will also automatically stick to the origin and axes. - /// - public class MeshSnapSpace : SnapSpace { - private SnapType snapType = SnapType.MESH; - public Vector3 sourceMeshCenter; - public Vector3 snappedPosition; - public Vector3 unsnappedPosition; - - // The id of the mesh that defines this MeshSnapSpace. - public int targetMeshId { get; private set; } - private ContinuousAxisStickEffect continuousAxisStickEffect; - private ContinuousPointStickEffect continuousSourcePointStickEffect; - private ContinuousPointStickEffect continuousTargetPointStickEffect; - private bool isAxisSticking; - - public MeshSnapSpace(int targetMeshId) { - this.targetMeshId = targetMeshId; - continuousAxisStickEffect = new ContinuousAxisStickEffect(); - continuousSourcePointStickEffect = new ContinuousPointStickEffect(); - continuousTargetPointStickEffect = new ContinuousPointStickEffect(); - } - - /// - /// Calculates the origin, rotation and axes of the MeshSnapSpace. - /// - public override void Execute() { - MMesh targetMesh = PeltzerMain.Instance.model.GetMesh(targetMeshId); - - Axes axes = new Axes( - targetMesh.rotation * Vector3.right, - targetMesh.rotation * Vector3.up, - targetMesh.rotation * Vector3.forward); - - Setup(targetMesh.bounds.center, targetMesh.rotation, axes); - - // We always show the offsets for all mesh stick effects. - UXEffectManager.GetEffectManager().StartEffect(continuousSourcePointStickEffect); - UXEffectManager.GetEffectManager().StartEffect(continuousTargetPointStickEffect); - // The target mesh offset doesn't change throughout a mesh snap. - continuousTargetPointStickEffect.UpdateFromPoint(origin); - } - - /// - /// Checks if the targetMeshId still exists in the model. If it does the snap is still valid. - /// - /// Whether the targetMeshId exists still. - public override bool IsValid() { - return PeltzerMain.Instance.model.HasMesh(targetMeshId); - } - +namespace com.google.apps.peltzer.client.alignment +{ /// - /// Translates a transform into the SnapSpace. - /// - /// A position is snapped to a MeshSnapSpace by: - /// 1) Trying to stick to the origin (all three axes at once). - /// 2) Trying to stick the position to a nearby axis. - /// 3) Snapping the position to a grid defined by the coordinate system if grid mode is on. - /// If neither of those conditions are met the position doesn't change. - /// - /// A rotation is snapped to a MeshSnapSpace by snapping the rotation to the nearest 90 degrees of the - /// MeshSnapSpace rotation. + /// A MeshSnapSpace is a coordinate system that an MMesh can be orientated in and snapped to. + /// + /// A MeshSnapSpace is conceptually the same as the universal coordinate system but with an arbitrary rotation and + /// origin. A MeshSnapSpace is used to 'snap to a target mesh' so its properties are defined by the target mesh. + /// Its rotation is the mesh's rotation, its origin is the mesh's offset and its axes are the mesh's local right, + /// up and forward vectors. + /// + /// When an MMesh is snapped to a MeshSnapSpace its rotation is snapped to the nearest 90 degrees of the space's + /// rotation and if GridMode is on the MMesh is moved so its bounding box lines up with the MeshSnapGrid. The + /// center of the mesh being snapped will also automatically stick to the origin and axes. /// - /// The position of the mesh being snapped. - /// The rotation of the mesh being snapped. - /// The snapped position and rotation as a SnapTransform. - public override SnapTransform Snap(Vector3 position, Quaternion rotation) { - unsnappedPosition = position; - // Try to snap to the origin. - if (SnapToOrigin(position, out snappedPosition)) { - if (isAxisSticking) { - isAxisSticking = false; - UXEffectManager.GetEffectManager().EndEffect(continuousAxisStickEffect); + public class MeshSnapSpace : SnapSpace + { + private SnapType snapType = SnapType.MESH; + public Vector3 sourceMeshCenter; + public Vector3 snappedPosition; + public Vector3 unsnappedPosition; + + // The id of the mesh that defines this MeshSnapSpace. + public int targetMeshId { get; private set; } + private ContinuousAxisStickEffect continuousAxisStickEffect; + private ContinuousPointStickEffect continuousSourcePointStickEffect; + private ContinuousPointStickEffect continuousTargetPointStickEffect; + private bool isAxisSticking; + + public MeshSnapSpace(int targetMeshId) + { + this.targetMeshId = targetMeshId; + continuousAxisStickEffect = new ContinuousAxisStickEffect(); + continuousSourcePointStickEffect = new ContinuousPointStickEffect(); + continuousTargetPointStickEffect = new ContinuousPointStickEffect(); } - } else { - if (SnapToAxes(position, PeltzerMain.Instance.peltzerController.isBlockMode, out snappedPosition)) { - if (!isAxisSticking) { - isAxisSticking = true; - UXEffectManager.GetEffectManager().StartEffect(continuousAxisStickEffect); - } - continuousAxisStickEffect.UpdateFromAxis(origin, snappedPosition); - } else { - if (isAxisSticking) { - isAxisSticking = false; - UXEffectManager.GetEffectManager().EndEffect(continuousAxisStickEffect); - } - // If we didn't snap to an axes, snap to the grid if grid mode is on or don't do anything. - if (PeltzerMain.Instance.peltzerController.isBlockMode) { - SnapToGrid(position, out snappedPosition); - } else { - snappedPosition = position; - } + + /// + /// Calculates the origin, rotation and axes of the MeshSnapSpace. + /// + public override void Execute() + { + MMesh targetMesh = PeltzerMain.Instance.model.GetMesh(targetMeshId); + + Axes axes = new Axes( + targetMesh.rotation * Vector3.right, + targetMesh.rotation * Vector3.up, + targetMesh.rotation * Vector3.forward); + + Setup(targetMesh.bounds.center, targetMesh.rotation, axes); + + // We always show the offsets for all mesh stick effects. + UXEffectManager.GetEffectManager().StartEffect(continuousSourcePointStickEffect); + UXEffectManager.GetEffectManager().StartEffect(continuousTargetPointStickEffect); + // The target mesh offset doesn't change throughout a mesh snap. + continuousTargetPointStickEffect.UpdateFromPoint(origin); } - continuousSourcePointStickEffect.UpdateFromPoint(snappedPosition); - } + /// + /// Checks if the targetMeshId still exists in the model. If it does the snap is still valid. + /// + /// Whether the targetMeshId exists still. + public override bool IsValid() + { + return PeltzerMain.Instance.model.HasMesh(targetMeshId); + } - // Snap the rotation to the nearest 90 degrees of the MeshSnapSpace's rotation. - Quaternion snappedRotation = GridUtils.SnapToNearest(rotation, this.rotation, 90f); - return new SnapTransform(snappedPosition, snappedRotation); - } + /// + /// Translates a transform into the SnapSpace. + /// + /// A position is snapped to a MeshSnapSpace by: + /// 1) Trying to stick to the origin (all three axes at once). + /// 2) Trying to stick the position to a nearby axis. + /// 3) Snapping the position to a grid defined by the coordinate system if grid mode is on. + /// If neither of those conditions are met the position doesn't change. + /// + /// A rotation is snapped to a MeshSnapSpace by snapping the rotation to the nearest 90 degrees of the + /// MeshSnapSpace rotation. + /// + /// The position of the mesh being snapped. + /// The rotation of the mesh being snapped. + /// The snapped position and rotation as a SnapTransform. + public override SnapTransform Snap(Vector3 position, Quaternion rotation) + { + unsnappedPosition = position; + // Try to snap to the origin. + if (SnapToOrigin(position, out snappedPosition)) + { + if (isAxisSticking) + { + isAxisSticking = false; + UXEffectManager.GetEffectManager().EndEffect(continuousAxisStickEffect); + } + } + else + { + if (SnapToAxes(position, PeltzerMain.Instance.peltzerController.isBlockMode, out snappedPosition)) + { + if (!isAxisSticking) + { + isAxisSticking = true; + UXEffectManager.GetEffectManager().StartEffect(continuousAxisStickEffect); + } + continuousAxisStickEffect.UpdateFromAxis(origin, snappedPosition); + } + else + { + if (isAxisSticking) + { + isAxisSticking = false; + UXEffectManager.GetEffectManager().EndEffect(continuousAxisStickEffect); + } + // If we didn't snap to an axes, snap to the grid if grid mode is on or don't do anything. + if (PeltzerMain.Instance.peltzerController.isBlockMode) + { + SnapToGrid(position, out snappedPosition); + } + else + { + snappedPosition = position; + } + } + + continuousSourcePointStickEffect.UpdateFromPoint(snappedPosition); + } + + // Snap the rotation to the nearest 90 degrees of the MeshSnapSpace's rotation. + Quaternion snappedRotation = GridUtils.SnapToNearest(rotation, this.rotation, 90f); + return new SnapTransform(snappedPosition, snappedRotation); + } - /// - /// Handles stopping snap logic maintained by the MeshSnapSpace. - /// - public override void StopSnap() { - if (isAxisSticking) { - UXEffectManager.GetEffectManager().EndEffect(continuousAxisStickEffect); - } + /// + /// Handles stopping snap logic maintained by the MeshSnapSpace. + /// + public override void StopSnap() + { + if (isAxisSticking) + { + UXEffectManager.GetEffectManager().EndEffect(continuousAxisStickEffect); + } + + UXEffectManager.GetEffectManager().EndEffect(continuousSourcePointStickEffect); + UXEffectManager.GetEffectManager().EndEffect(continuousTargetPointStickEffect); + } - UXEffectManager.GetEffectManager().EndEffect(continuousSourcePointStickEffect); - UXEffectManager.GetEffectManager().EndEffect(continuousTargetPointStickEffect); - } + /// + /// Checks if another SnapSpace is equivalent to this space. MeshSnapSpaces are equivalent if they have the same + /// targetMeshId. + /// + /// The other SnapSpace. + /// Whether they are equal. + public override bool Equals(SnapSpace otherSpace) + { + if (otherSpace == null || otherSpace.SnapType != snapType) + { + return false; + } + + MeshSnapSpace otherFaceSnapSpace = (MeshSnapSpace)otherSpace; + return targetMeshId == otherFaceSnapSpace.targetMeshId; + } - /// - /// Checks if another SnapSpace is equivalent to this space. MeshSnapSpaces are equivalent if they have the same - /// targetMeshId. - /// - /// The other SnapSpace. - /// Whether they are equal. - public override bool Equals(SnapSpace otherSpace) { - if (otherSpace == null || otherSpace.SnapType != snapType) { - return false; - } - - MeshSnapSpace otherFaceSnapSpace = (MeshSnapSpace)otherSpace; - return targetMeshId == otherFaceSnapSpace.targetMeshId; + public override SnapType SnapType { get { return snapType; } } } - - public override SnapType SnapType { get { return snapType; } } - } } diff --git a/Assets/Scripts/alignment/SnapSpace.cs b/Assets/Scripts/alignment/SnapSpace.cs index 1c3f3bbd..a52ee7b5 100644 --- a/Assets/Scripts/alignment/SnapSpace.cs +++ b/Assets/Scripts/alignment/SnapSpace.cs @@ -16,59 +16,62 @@ using com.google.apps.peltzer.client.tools.utils; using UnityEngine; -namespace com.google.apps.peltzer.client.alignment { - /// - /// Defines a SnapSpace: a special kind of coordinate system that an MMesh can be orientated in and snapped to. - /// - /// Each method in ISnapSpace represents a step in the snapping flow. When we 'DETECT' what snap should occur we - /// construct the space. This is a partial setup, every class that inherits from this class should set required - /// metadata and maintain any data that was already calculated while detecting the snap. This helps us avoid any - /// duplicate work when the snap is executed, and extraneous calculations in every detection frame. When the user - /// actually pulls the alt-trigger we 'EXECUTE' the snap; finishing any remaining work to setup the SnapSpace. - /// SnapTo() is then called continuously to 'MODIFY' the snap. - /// - public abstract class SnapSpace : CoordinateSystem { +namespace com.google.apps.peltzer.client.alignment +{ /// - /// The type of snapping that generated this SnapSpace. The type of snapping determines the properities of the - /// space and how the held mesh is translated by SnapTo(). + /// Defines a SnapSpace: a special kind of coordinate system that an MMesh can be orientated in and snapped to. + /// + /// Each method in ISnapSpace represents a step in the snapping flow. When we 'DETECT' what snap should occur we + /// construct the space. This is a partial setup, every class that inherits from this class should set required + /// metadata and maintain any data that was already calculated while detecting the snap. This helps us avoid any + /// duplicate work when the snap is executed, and extraneous calculations in every detection frame. When the user + /// actually pulls the alt-trigger we 'EXECUTE' the snap; finishing any remaining work to setup the SnapSpace. + /// SnapTo() is then called continuously to 'MODIFY' the snap. /// - public abstract SnapType SnapType { - get; - } + public abstract class SnapSpace : CoordinateSystem + { + /// + /// The type of snapping that generated this SnapSpace. The type of snapping determines the properities of the + /// space and how the held mesh is translated by SnapTo(). + /// + public abstract SnapType SnapType + { + get; + } - /// - /// Called once on alt-trigger when a user wants to execute the detected snap. This is expected to do all the - /// heavy lifting calculations avoided on construction to maximize performance. - /// - public abstract void Execute(); + /// + /// Called once on alt-trigger when a user wants to execute the detected snap. This is expected to do all the + /// heavy lifting calculations avoided on construction to maximize performance. + /// + public abstract void Execute(); - /// - /// Checks if the state of a snap is still valid. - /// - /// Whether the snap is still valid. - public abstract bool IsValid(); + /// + /// Checks if the state of a snap is still valid. + /// + /// Whether the snap is still valid. + public abstract bool IsValid(); - /// - /// Called every frame while the user is holding down alt-trigger to calculate the snapped transform of the - /// held mesh. The method should take in the unsnapped position and rotation of the held mesh, translate it into - /// the snap space and return the new snapped position and rotation for this frame. The tool calling SnapTo() is - /// then responsible for actually updating the transform of the held mesh. - /// - /// The position of the mesh being snapped. - /// The rotation of the mesh being snapped. - /// The snapped position and rotation as a SnapTransform. - public abstract SnapTransform Snap(Vector3 position, Quaternion rotation); + /// + /// Called every frame while the user is holding down alt-trigger to calculate the snapped transform of the + /// held mesh. The method should take in the unsnapped position and rotation of the held mesh, translate it into + /// the snap space and return the new snapped position and rotation for this frame. The tool calling SnapTo() is + /// then responsible for actually updating the transform of the held mesh. + /// + /// The position of the mesh being snapped. + /// The rotation of the mesh being snapped. + /// The snapped position and rotation as a SnapTransform. + public abstract SnapTransform Snap(Vector3 position, Quaternion rotation); - /// - /// Called when a snap has been started and then stopped. Useful for clearing any UI elements. - /// - public abstract void StopSnap(); + /// + /// Called when a snap has been started and then stopped. Useful for clearing any UI elements. + /// + public abstract void StopSnap(); - /// - /// Checks whether a SnapSpace is equivalent to another SnapSpace. - /// - /// The other SnapSpace. - /// Whether they are equal. - public abstract bool Equals(SnapSpace otherSpace); - } + /// + /// Checks whether a SnapSpace is equivalent to another SnapSpace. + /// + /// The other SnapSpace. + /// Whether they are equal. + public abstract bool Equals(SnapSpace otherSpace); + } } diff --git a/Assets/Scripts/alignment/SnapTransform.cs b/Assets/Scripts/alignment/SnapTransform.cs index 78f2f972..fe4c2c72 100644 --- a/Assets/Scripts/alignment/SnapTransform.cs +++ b/Assets/Scripts/alignment/SnapTransform.cs @@ -16,17 +16,20 @@ using System.Collections.Generic; using UnityEngine; -namespace com.google.apps.peltzer.client.alignment { - /// - /// Holds the position and rotation for a snap. - /// - public class SnapTransform { - public Vector3 position; - public Quaternion rotation; +namespace com.google.apps.peltzer.client.alignment +{ + /// + /// Holds the position and rotation for a snap. + /// + public class SnapTransform + { + public Vector3 position; + public Quaternion rotation; - public SnapTransform(Vector3 position, Quaternion rotation) { - this.position = position; - this.rotation = rotation; + public SnapTransform(Vector3 position, Quaternion rotation) + { + this.position = position; + this.rotation = rotation; + } } - } } diff --git a/Assets/Scripts/alignment/SnapType.cs b/Assets/Scripts/alignment/SnapType.cs index 3dab7675..da3b8b43 100644 --- a/Assets/Scripts/alignment/SnapType.cs +++ b/Assets/Scripts/alignment/SnapType.cs @@ -12,28 +12,30 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace com.google.apps.peltzer.client.alignment { - /// - /// The possible types of snapping. Each type of snapping identifies what is being snapped together. - /// IMPORTANT: enum values are used as indices, so the values must be numbered sequentially from 0 - /// (default enum value assignment). - /// - public enum SnapType { - NONE, +namespace com.google.apps.peltzer.client.alignment +{ /// - /// mesh --> mesh. - /// Snapping a source mesh to a target mesh. + /// The possible types of snapping. Each type of snapping identifies what is being snapped together. + /// IMPORTANT: enum values are used as indices, so the values must be numbered sequentially from 0 + /// (default enum value assignment). /// - MESH, - /// - /// face --> face. - /// Snapping a source face to a target face. - /// - FACE, - /// - /// mesh --> universe. - /// Snapping a source mesh to the universal coordinate system. - /// - UNIVERSAL - } + public enum SnapType + { + NONE, + /// + /// mesh --> mesh. + /// Snapping a source mesh to a target mesh. + /// + MESH, + /// + /// face --> face. + /// Snapping a source face to a target face. + /// + FACE, + /// + /// mesh --> universe. + /// Snapping a source mesh to the universal coordinate system. + /// + UNIVERSAL + } } \ No newline at end of file diff --git a/Assets/Scripts/alignment/UniversalSnapSpace.cs b/Assets/Scripts/alignment/UniversalSnapSpace.cs index 85b3bc51..d0f2d9f2 100644 --- a/Assets/Scripts/alignment/UniversalSnapSpace.cs +++ b/Assets/Scripts/alignment/UniversalSnapSpace.cs @@ -16,80 +16,88 @@ using com.google.apps.peltzer.client.tools.utils; using UnityEngine; -namespace com.google.apps.peltzer.client.alignment { - /// - /// A UniversalSnapSpace is a coordinate system that an MMesh can be orientated in and snapped to. - /// - /// A UniversalSnapSpace shares the same characteristics as the universal coordinate system. It has identity - /// rotation, its origin is { 0, 0, 0 } and its axes are the unit vectors. - /// - /// When an MMesh is snapped to a - /// UniversalSnapSpace its rotation is snapped to the nearest 90 degrees of Quaternion.Identity and if GridMode is - /// on the MMesh is moved so its bounding box lines up with the grid. - /// - public class UniversalSnapSpace : SnapSpace { - private const SnapType snapType = SnapType.UNIVERSAL; - // The bounds of the mesh being snapped to this UniversalSnapSpace. When we snap we want to snap the bounds of the - // mesh to the universal grid so that Universal Snapping mimics GridMode. - // TODO (bug): Remove the above comment once gridmode is refactored to use universal snapping. - private Bounds sourceMeshBounds; - - public UniversalSnapSpace(Bounds bounds) { - sourceMeshBounds = bounds; - } - +namespace com.google.apps.peltzer.client.alignment +{ /// - /// Sets the origin, rotation and axes of the UniversalSnapSpace to the coordinate system defaults. + /// A UniversalSnapSpace is a coordinate system that an MMesh can be orientated in and snapped to. + /// + /// A UniversalSnapSpace shares the same characteristics as the universal coordinate system. It has identity + /// rotation, its origin is { 0, 0, 0 } and its axes are the unit vectors. + /// + /// When an MMesh is snapped to a + /// UniversalSnapSpace its rotation is snapped to the nearest 90 degrees of Quaternion.Identity and if GridMode is + /// on the MMesh is moved so its bounding box lines up with the grid. /// - public override void Execute() { - Setup(DEFAULT_ORIGIN, DEFAULT_ROTATION, DEFAULT_AXES); - } + public class UniversalSnapSpace : SnapSpace + { + private const SnapType snapType = SnapType.UNIVERSAL; + // The bounds of the mesh being snapped to this UniversalSnapSpace. When we snap we want to snap the bounds of the + // mesh to the universal grid so that Universal Snapping mimics GridMode. + // TODO (bug): Remove the above comment once gridmode is refactored to use universal snapping. + private Bounds sourceMeshBounds; - public override bool IsValid() { - // A universal snap is always valid. - return true; - } + public UniversalSnapSpace(Bounds bounds) + { + sourceMeshBounds = bounds; + } - /// - /// Translates a transform into the SnapSpace. - /// - /// A position is snapped to a UniversalSnapSpace by snapping to a grid defined by the coordinate system if grid - /// mode is on. - /// - /// A rotation is snapped to a UniversalSnapSpace by snapping the rotation to the nearest 90 degrees of the - /// UniversalSnapSpace rotation. - /// - /// The position of the mesh being snapped. - /// The rotation of the mesh being snapped. - /// The snapped position and rotation as a SnapTransform. - public override SnapTransform Snap(Vector3 position, Quaternion rotation) { - // If grid mode is on snap the bounding box to the grid. - Vector3 snappedPosition = PeltzerMain.Instance.peltzerController.isBlockMode ? - GridUtils.SnapToGrid(position, sourceMeshBounds) : - position; + /// + /// Sets the origin, rotation and axes of the UniversalSnapSpace to the coordinate system defaults. + /// + public override void Execute() + { + Setup(DEFAULT_ORIGIN, DEFAULT_ROTATION, DEFAULT_AXES); + } - // Snap the rotation to the nearest 90 degrees of the universal coordinate system. - Quaternion snappedRotation = GridUtils.SnapToNearest(rotation, this.rotation, 90f); - return new SnapTransform(snappedPosition, snappedRotation); - } + public override bool IsValid() + { + // A universal snap is always valid. + return true; + } - /// - /// Handles stopping snap logic maintained by the UniversalSnapSpace. - /// - public override void StopSnap() { - // Does nothing. - } + /// + /// Translates a transform into the SnapSpace. + /// + /// A position is snapped to a UniversalSnapSpace by snapping to a grid defined by the coordinate system if grid + /// mode is on. + /// + /// A rotation is snapped to a UniversalSnapSpace by snapping the rotation to the nearest 90 degrees of the + /// UniversalSnapSpace rotation. + /// + /// The position of the mesh being snapped. + /// The rotation of the mesh being snapped. + /// The snapped position and rotation as a SnapTransform. + public override SnapTransform Snap(Vector3 position, Quaternion rotation) + { + // If grid mode is on snap the bounding box to the grid. + Vector3 snappedPosition = PeltzerMain.Instance.peltzerController.isBlockMode ? + GridUtils.SnapToGrid(position, sourceMeshBounds) : + position; - /// - /// Checks if another SnapSpace is equivalent to this space. The UniversalSnapSpace has no unique properties so all - /// UniversalSnapSpaces are equivalent to each other. - /// - /// The other SnapSpace. - /// Whether they are equal. - public override bool Equals(SnapSpace otherSpace) { - return otherSpace != null && otherSpace.SnapType == snapType; - } + // Snap the rotation to the nearest 90 degrees of the universal coordinate system. + Quaternion snappedRotation = GridUtils.SnapToNearest(rotation, this.rotation, 90f); + return new SnapTransform(snappedPosition, snappedRotation); + } + + /// + /// Handles stopping snap logic maintained by the UniversalSnapSpace. + /// + public override void StopSnap() + { + // Does nothing. + } - public override SnapType SnapType { get { return snapType; } } - } + /// + /// Checks if another SnapSpace is equivalent to this space. The UniversalSnapSpace has no unique properties so all + /// UniversalSnapSpaces are equivalent to each other. + /// + /// The other SnapSpace. + /// Whether they are equal. + public override bool Equals(SnapSpace otherSpace) + { + return otherSpace != null && otherSpace.SnapType == snapType; + } + + public override SnapType SnapType { get { return snapType; } } + } } diff --git a/Assets/Scripts/api_clients/assets_service_client/AssetsServiceClient.cs b/Assets/Scripts/api_clients/assets_service_client/AssetsServiceClient.cs index 11197726..dbcb7798 100644 --- a/Assets/Scripts/api_clients/assets_service_client/AssetsServiceClient.cs +++ b/Assets/Scripts/api_clients/assets_service_client/AssetsServiceClient.cs @@ -31,956 +31,1110 @@ using com.google.apps.peltzer.client.zandria; using com.google.apps.peltzer.client.menu; -namespace com.google.apps.peltzer.client.api_clients.assets_service_client { - - public class AssetsServiceClientWork : MonoBehaviour, BackgroundWork { - private AssetsServiceClient assetsServiceClient; - private string assetId; - private HashSet remixIds; - private SaveData saveData; - private byte[] objMultiPartBytes; - private byte[] triangulatedObjMultiPartBytes; - private byte[] mtlMultiPartBytes; - private byte[] fbxMultiPartBytes; - private byte[] blocksMultiPartBytes; - private byte[] thumbnailMultiPartBytes; - private bool publish; - private bool saveSelected; - - public void Setup(AssetsServiceClient assetsServiceClient, string assetId, HashSet remixIds, - SaveData saveData, bool publish, bool saveSelected) { - this.assetsServiceClient = assetsServiceClient; - this.assetId = assetId; - this.remixIds = remixIds; - this.saveData = saveData; - this.publish = publish; - this.saveSelected = saveSelected; - } +namespace com.google.apps.peltzer.client.api_clients.assets_service_client +{ + + public class AssetsServiceClientWork : MonoBehaviour, BackgroundWork + { + private AssetsServiceClient assetsServiceClient; + private string assetId; + private HashSet remixIds; + private SaveData saveData; + private byte[] objMultiPartBytes; + private byte[] triangulatedObjMultiPartBytes; + private byte[] mtlMultiPartBytes; + private byte[] fbxMultiPartBytes; + private byte[] blocksMultiPartBytes; + private byte[] thumbnailMultiPartBytes; + private bool publish; + private bool saveSelected; + + public void Setup(AssetsServiceClient assetsServiceClient, string assetId, HashSet remixIds, + SaveData saveData, bool publish, bool saveSelected) + { + this.assetsServiceClient = assetsServiceClient; + this.assetId = assetId; + this.remixIds = remixIds; + this.saveData = saveData; + this.publish = publish; + this.saveSelected = saveSelected; + } - public void BackgroundWork() { - saveData.GLTFfiles.root.multipartBytes = assetsServiceClient.MultiPartContent(saveData.GLTFfiles.root.fileName, - saveData.GLTFfiles.root.mimeType, saveData.GLTFfiles.root.bytes); - foreach (FormatDataFile file in saveData.GLTFfiles.resources) { - file.multipartBytes = assetsServiceClient.MultiPartContent(file.fileName, file.mimeType, file.bytes); - } - - objMultiPartBytes = assetsServiceClient.MultiPartContent(ExportUtils.OBJ_FILENAME, "text/plain", saveData.objFile); - triangulatedObjMultiPartBytes = assetsServiceClient.MultiPartContent(ExportUtils.TRIANGULATED_OBJ_FILENAME, - "text/plain", saveData.triangulatedObjFile); - mtlMultiPartBytes = assetsServiceClient.MultiPartContent(ExportUtils.MTL_FILENAME, "text/plain", saveData.mtlFile); - fbxMultiPartBytes = assetsServiceClient.MultiPartContent(ExportUtils.FBX_FILENAME, "application/octet-stream", - saveData.fbxFile); - blocksMultiPartBytes = assetsServiceClient.MultiPartContent(ExportUtils.BLOCKS_FILENAME, "application/octet-stream", - saveData.blocksFile); - thumbnailMultiPartBytes = assetsServiceClient.MultiPartContent(ExportUtils.THUMBNAIL_FILENAME, "image/png", - saveData.thumbnailBytes); - } + public void BackgroundWork() + { + saveData.GLTFfiles.root.multipartBytes = assetsServiceClient.MultiPartContent(saveData.GLTFfiles.root.fileName, + saveData.GLTFfiles.root.mimeType, saveData.GLTFfiles.root.bytes); + foreach (FormatDataFile file in saveData.GLTFfiles.resources) + { + file.multipartBytes = assetsServiceClient.MultiPartContent(file.fileName, file.mimeType, file.bytes); + } - public void PostWork() { - if (assetId == null || saveSelected) { - StartCoroutine(assetsServiceClient.UploadModel(remixIds, objMultiPartBytes, saveData.objPolyCount, - triangulatedObjMultiPartBytes, saveData.triangulatedObjPolyCount, mtlMultiPartBytes, saveData.GLTFfiles, - fbxMultiPartBytes, blocksMultiPartBytes, thumbnailMultiPartBytes, publish, saveSelected)); - } else { - StartCoroutine(assetsServiceClient.UpdateModel(assetId, remixIds, objMultiPartBytes, saveData.objPolyCount, - triangulatedObjMultiPartBytes, saveData.triangulatedObjPolyCount, mtlMultiPartBytes, saveData.GLTFfiles, - fbxMultiPartBytes, blocksMultiPartBytes, thumbnailMultiPartBytes, publish)); - } - } - } - - public class ParseAssetsBackgroundWork : BackgroundWork { - private string response; - private PolyMenuMain.CreationType creationType; - private System.Action successCallback; - private System.Action failureCallback; - private bool hackUrls; - - private bool success; - private ObjectStoreSearchResult objectStoreSearchResult; - - public ParseAssetsBackgroundWork(string response, PolyMenuMain.CreationType creationType, - System.Action successCallback, - System.Action failureCallback, bool hackUrls = false) { - this.response = response; - this.creationType = creationType; - this.successCallback = successCallback; - this.failureCallback = failureCallback; - this.hackUrls = hackUrls; - } + objMultiPartBytes = assetsServiceClient.MultiPartContent(ExportUtils.OBJ_FILENAME, "text/plain", saveData.objFile); + triangulatedObjMultiPartBytes = assetsServiceClient.MultiPartContent(ExportUtils.TRIANGULATED_OBJ_FILENAME, + "text/plain", saveData.triangulatedObjFile); + mtlMultiPartBytes = assetsServiceClient.MultiPartContent(ExportUtils.MTL_FILENAME, "text/plain", saveData.mtlFile); + fbxMultiPartBytes = assetsServiceClient.MultiPartContent(ExportUtils.FBX_FILENAME, "application/octet-stream", + saveData.fbxFile); + blocksMultiPartBytes = assetsServiceClient.MultiPartContent(ExportUtils.BLOCKS_FILENAME, "application/octet-stream", + saveData.blocksFile); + thumbnailMultiPartBytes = assetsServiceClient.MultiPartContent(ExportUtils.THUMBNAIL_FILENAME, "image/png", + saveData.thumbnailBytes); + } - public void BackgroundWork() { - success = AssetsServiceClient.ParseReturnedAssets(response, creationType, out objectStoreSearchResult, hackUrls); + public void PostWork() + { + if (assetId == null || saveSelected) + { + StartCoroutine(assetsServiceClient.UploadModel(remixIds, objMultiPartBytes, saveData.objPolyCount, + triangulatedObjMultiPartBytes, saveData.triangulatedObjPolyCount, mtlMultiPartBytes, saveData.GLTFfiles, + fbxMultiPartBytes, blocksMultiPartBytes, thumbnailMultiPartBytes, publish, saveSelected)); + } + else + { + StartCoroutine(assetsServiceClient.UpdateModel(assetId, remixIds, objMultiPartBytes, saveData.objPolyCount, + triangulatedObjMultiPartBytes, saveData.triangulatedObjPolyCount, mtlMultiPartBytes, saveData.GLTFfiles, + fbxMultiPartBytes, blocksMultiPartBytes, thumbnailMultiPartBytes, publish)); + } + } } - public void PostWork() { - if (success) { - successCallback(objectStoreSearchResult); - } else { - failureCallback(); - } + public class ParseAssetsBackgroundWork : BackgroundWork + { + private string response; + private PolyMenuMain.CreationType creationType; + private System.Action successCallback; + private System.Action failureCallback; + private bool hackUrls; + + private bool success; + private ObjectStoreSearchResult objectStoreSearchResult; + + public ParseAssetsBackgroundWork(string response, PolyMenuMain.CreationType creationType, + System.Action successCallback, + System.Action failureCallback, bool hackUrls = false) + { + this.response = response; + this.creationType = creationType; + this.successCallback = successCallback; + this.failureCallback = failureCallback; + this.hackUrls = hackUrls; + } + + public void BackgroundWork() + { + success = AssetsServiceClient.ParseReturnedAssets(response, creationType, out objectStoreSearchResult, hackUrls); + } + + public void PostWork() + { + if (success) + { + successCallback(objectStoreSearchResult); + } + else + { + failureCallback(); + } + } } - } - public class ParseAssetBackgroundWork : BackgroundWork { - private string response; - private System.Action callback; - private bool hackUrls; + public class ParseAssetBackgroundWork : BackgroundWork + { + private string response; + private System.Action callback; + private bool hackUrls; - private bool success; - private ObjectStoreEntry objectStoreEntry; + private bool success; + private ObjectStoreEntry objectStoreEntry; - public ParseAssetBackgroundWork(string response, System.Action callback, bool hackUrls = false) { - this.response = response; - this.callback = callback; - this.hackUrls = hackUrls; - } + public ParseAssetBackgroundWork(string response, System.Action callback, bool hackUrls = false) + { + this.response = response; + this.callback = callback; + this.hackUrls = hackUrls; + } - public void BackgroundWork() { - success = AssetsServiceClient.ParseAsset(response, out objectStoreEntry, hackUrls); - } + public void BackgroundWork() + { + success = AssetsServiceClient.ParseAsset(response, out objectStoreEntry, hackUrls); + } - public void PostWork() { - if (success) { - callback(objectStoreEntry); - } - } - } - - public class AssetsServiceClient : MonoBehaviour { - // The base for API requests to the assets service. - public static string AUTOPUSH_BASE_URL = "[Removed]"; - public static string PROD_BASE_URL = "[Removed]"; - public static string BaseUrl() { return Features.useZandriaProd ? PROD_BASE_URL : AUTOPUSH_BASE_URL; } - // The base for the URL to be opened in a user's browser if they wish to publish. - public static string AUTOPUSH_PUBLISH_URL_BASE = "[Removed]"; - public static string PROD_DEFAULT_PUBLISH_URL_BASE = "[Removed]"; - public static string PublishUrl() { return Features.useZandriaProd ? PROD_DEFAULT_PUBLISH_URL_BASE : AUTOPUSH_PUBLISH_URL_BASE; } - // The base for the URL to be opened in a user's browser if they have saved. - // Also used as the target for the "Your models" desktop menu - public static string AUTOPUSH_SAVE_URL = "[Removed]"; - public static string PROD_DEFAULT_SAVE_URL = "[Removed]"; - public static string SaveUrl() { return Features.useZandriaProd ? PROD_DEFAULT_SAVE_URL : AUTOPUSH_SAVE_URL; } - - // Poly's application key for the assets service/ - public const string POLY_KEY = "[Removed]"; - - // Search request strings corresponding to ListAssetRequest protos, see point of call for details. - private static string FeaturedModelsSearchUrl() { - int pageSize = ZandriaCreationsManager.MAX_NUMBER_OF_PAGES * ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE; - return String.Format("{0}/v1/assets?key={1}&filter=format_type:BLOCKS,admin_tag:blocksgallery,license:CREATIVE_COMMONS_BY" + - "&order_by=create_time%20desc&page_size={2}", BaseUrl(), POLY_KEY, pageSize); + public void PostWork() + { + if (success) + { + callback(objectStoreEntry); + } + } } - private static string LikedModelsSearchUrl() { - int pageSize = ZandriaCreationsManager.MAX_NUMBER_OF_PAGES * ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE; + public class AssetsServiceClient : MonoBehaviour + { + // The base for API requests to the assets service. + public static string AUTOPUSH_BASE_URL = "[Removed]"; + public static string PROD_BASE_URL = "[Removed]"; + public static string BaseUrl() { return Features.useZandriaProd ? PROD_BASE_URL : AUTOPUSH_BASE_URL; } + // The base for the URL to be opened in a user's browser if they wish to publish. + public static string AUTOPUSH_PUBLISH_URL_BASE = "[Removed]"; + public static string PROD_DEFAULT_PUBLISH_URL_BASE = "[Removed]"; + public static string PublishUrl() { return Features.useZandriaProd ? PROD_DEFAULT_PUBLISH_URL_BASE : AUTOPUSH_PUBLISH_URL_BASE; } + // The base for the URL to be opened in a user's browser if they have saved. + // Also used as the target for the "Your models" desktop menu + public static string AUTOPUSH_SAVE_URL = "[Removed]"; + public static string PROD_DEFAULT_SAVE_URL = "[Removed]"; + public static string SaveUrl() { return Features.useZandriaProd ? PROD_DEFAULT_SAVE_URL : AUTOPUSH_SAVE_URL; } + + // Poly's application key for the assets service/ + public const string POLY_KEY = "[Removed]"; + + // Search request strings corresponding to ListAssetRequest protos, see point of call for details. + private static string FeaturedModelsSearchUrl() + { + int pageSize = ZandriaCreationsManager.MAX_NUMBER_OF_PAGES * ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE; + return String.Format("{0}/v1/assets?key={1}&filter=format_type:BLOCKS,admin_tag:blocksgallery,license:CREATIVE_COMMONS_BY" + + "&order_by=create_time%20desc&page_size={2}", BaseUrl(), POLY_KEY, pageSize); + } - return String.Format("{0}/v1/assets?key={1}&filter=format_type:BLOCKS,liked:true,license:CREATIVE_COMMONS_BY" + - "&order_by=liked_time%20desc&page_size={2}", BaseUrl(), POLY_KEY, pageSize); - } - private static string YourModelsSearchUrl() { - int pageSize = ZandriaCreationsManager.MAX_NUMBER_OF_PAGES * ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE; + private static string LikedModelsSearchUrl() + { + int pageSize = ZandriaCreationsManager.MAX_NUMBER_OF_PAGES * ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE; - return String.Format("{0}/v1/accounts/me/assets?key={1}&filter=format_type:BLOCKS&access_level=PRIVATE" + - "&order_by=create_time%20desc&page_size={2}", BaseUrl(), POLY_KEY, pageSize); - } + return String.Format("{0}/v1/assets?key={1}&filter=format_type:BLOCKS,liked:true,license:CREATIVE_COMMONS_BY" + + "&order_by=liked_time%20desc&page_size={2}", BaseUrl(), POLY_KEY, pageSize); + } + private static string YourModelsSearchUrl() + { + int pageSize = ZandriaCreationsManager.MAX_NUMBER_OF_PAGES * ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE; - // Some regex. - private const string BOUNDARY = "!&!Peltzer12!&!Peltzer34!&!Peltzer56!&!"; - private const string ASSET_ID_MATCH = "assetId\": \"(.+?)\""; - private const string ELEMENT_ID_MATCH = "elementId\": \"(.+?)\""; - - // Most recent asset IDs we have seen in the "Featured" and "Liked" sections. - // Used for polling economically (so we know which part of the results is new and which part isn't). - public static string mostRecentFeaturedAssetId; - public static string mostRecentLikedAssetId; - - // Some state around an upload. - public enum UploadState { IN_PROGRESS, FAILED, SUCCEEDED } - private string assetId; - private Dictionary elementIds = new Dictionary(); - private Dictionary elementUploadStates = new Dictionary(); - private bool assetCreationSuccess; - private bool resourceUploadSuccess; - private bool hasSavedSuccessfully; - - private bool compressResourceUpload = true; - private readonly object deflateMutex = new object(); - private byte[] tempDeflateBuffer = new byte[65536 * 4]; - - /// - /// Takes a string, representing the ListAssetsResponse proto, and fills objectStoreSearchResult with - /// relevant fields from the response and returns true, if the response is of the expected format. - /// - public static bool ParseReturnedAssets(string response, PolyMenuMain.CreationType type, - out ObjectStoreSearchResult objectStoreSearchResult, bool hackUrls = false) { - objectStoreSearchResult = new ObjectStoreSearchResult(); - - // Try and actually parse the string. - JObject results = JObject.Parse(response); - IJEnumerable assets = results["asset"].AsJEnumerable(); - if (assets == null) { - return false; - } - - // Build accountId to name map first - Dictionary authorNamesById = new Dictionary(); - JToken accounts = results["account"]; - if (accounts != null) { - foreach (JToken account in accounts) { - string id = account.First["accountId"].ToString(); - string name = ""; - JToken displayName = account.First["displayName"]; - if (displayName != null) { - name = displayName.ToString(); - } - authorNamesById.Add(id, name); - } - } - - // Then parse the assets. - List objectStoreEntries = new List(); - - string firstAssetId = null; - foreach (JToken asset in assets) { - ObjectStoreEntry objectStoreEntry; - string author = null; - var accountId = asset["accountId"]; - if (accountId != null) { - authorNamesById.TryGetValue(accountId.ToString(), out author); - } - - if (type == PolyMenuMain.CreationType.FEATURED || type == PolyMenuMain.CreationType.LIKED) { - string assetId = asset["assetId"].ToString(); - if (firstAssetId == null) { - firstAssetId = assetId; - } - // Once we've seen an ID we've seen before, no need to continue through the list. This helps with polling - // regularly. This assumes new items always appear at the top of the list; we explicitly ask Zandria to sort by - // featured/liked time, descending. - if ((type == PolyMenuMain.CreationType.FEATURED && mostRecentFeaturedAssetId == assetId) - || (type == PolyMenuMain.CreationType.LIKED && mostRecentLikedAssetId == assetId)) { - break; - } - } - if (ParseAsset(asset, out objectStoreEntry, hackUrls)) { - objectStoreEntry.author = author; - objectStoreEntries.Add(objectStoreEntry); - } - } - - if (type == PolyMenuMain.CreationType.FEATURED) { - mostRecentFeaturedAssetId = firstAssetId; - } else if (type == PolyMenuMain.CreationType.LIKED) { - mostRecentLikedAssetId = firstAssetId; - } - objectStoreSearchResult.results = objectStoreEntries.ToArray(); - return true; - } + return String.Format("{0}/v1/accounts/me/assets?key={1}&filter=format_type:BLOCKS&access_level=PRIVATE" + + "&order_by=create_time%20desc&page_size={2}", BaseUrl(), POLY_KEY, pageSize); + } - /// - /// Parses a single asset as defined in vr/assets/v1/asset.proto - /// - /// - public static bool ParseAsset(JToken asset, out ObjectStoreEntry objectStoreEntry, bool hackUrls) { - objectStoreEntry = new ObjectStoreEntry(); - - if (asset["accessLevel"] == null) { - Debug.Log("Asset had no access level set"); - return false; - } - objectStoreEntry.isPrivateAsset = asset["accessLevel"].ToString() == "PRIVATE"; - - objectStoreEntry.id = asset["assetId"].ToString(); - JToken thumbnailRoot = asset["thumbnail"]; - if (thumbnailRoot != null) { - IJEnumerable thumbnailElements = asset["thumbnail"].AsJEnumerable(); - foreach (JToken thumbnailElement in thumbnailElements) { - objectStoreEntry.thumbnail = thumbnailElement["typeInfo"]["imageInfo"]["fifeUrl"].ToString(); - break; - } - } - List tags = new List(); - IJEnumerable assetTags = asset["tag"].AsJEnumerable(); - if (assetTags != null) { - foreach (JToken assetTag in assetTags) { - tags.Add(assetTag.ToString()); - } - if (tags.Count > 0) { - objectStoreEntry.tags = tags.ToArray(); - } - } - ObjectStoreObjectAssetsWrapper entryAssets = new ObjectStoreObjectAssetsWrapper(); - ObjectStorePeltzerAssets blocksAsset = new ObjectStorePeltzerAssets(); - // 7 is the enum for Blocks in ElementType - // A bit ugly: we simply take one arbitrary entry (we assume only one entry exists, as we only ever upload one). - blocksAsset.rootUrl = asset["formatList"]["7"]["format"][0]["root"]["dataUrl"].ToString(); - - blocksAsset.baseFile = ""; - entryAssets.peltzer = blocksAsset; - objectStoreEntry.assets = entryAssets; - objectStoreEntry.title = asset["displayName"].ToString(); - objectStoreEntry.createdDate = DateTime.Parse(asset["createTime"].ToString()); - objectStoreEntry.cameraForward = GetCameraForward(asset["cameraParams"]); - return true; - } + // Some regex. + private const string BOUNDARY = "!&!Peltzer12!&!Peltzer34!&!Peltzer56!&!"; + private const string ASSET_ID_MATCH = "assetId\": \"(.+?)\""; + private const string ELEMENT_ID_MATCH = "elementId\": \"(.+?)\""; + + // Most recent asset IDs we have seen in the "Featured" and "Liked" sections. + // Used for polling economically (so we know which part of the results is new and which part isn't). + public static string mostRecentFeaturedAssetId; + public static string mostRecentLikedAssetId; + + // Some state around an upload. + public enum UploadState { IN_PROGRESS, FAILED, SUCCEEDED } + private string assetId; + private Dictionary elementIds = new Dictionary(); + private Dictionary elementUploadStates = new Dictionary(); + private bool assetCreationSuccess; + private bool resourceUploadSuccess; + private bool hasSavedSuccessfully; + + private bool compressResourceUpload = true; + private readonly object deflateMutex = new object(); + private byte[] tempDeflateBuffer = new byte[65536 * 4]; + + /// + /// Takes a string, representing the ListAssetsResponse proto, and fills objectStoreSearchResult with + /// relevant fields from the response and returns true, if the response is of the expected format. + /// + public static bool ParseReturnedAssets(string response, PolyMenuMain.CreationType type, + out ObjectStoreSearchResult objectStoreSearchResult, bool hackUrls = false) + { + objectStoreSearchResult = new ObjectStoreSearchResult(); + + // Try and actually parse the string. + JObject results = JObject.Parse(response); + IJEnumerable assets = results["asset"].AsJEnumerable(); + if (assets == null) + { + return false; + } - /// - /// Parse the camera parameter matrix from Zandria to extract the camera's forward, if available. - /// - /// A 4x4 matrix holding information about the camera's position and - /// rotation: - /// Row major - /// * * Fx Px - /// * * Fy Py - /// * * Fz Pz - /// 0 0 0 1 - /// A string of three float values separated by spaces that represent the camera forward. - private static Vector3 GetCameraForward(JToken cameraParams) { - JToken cameraMatrix = cameraParams["matrix4x4"]; - if (cameraMatrix == null) return Vector3.zero; - // We want the third column, which holds the camera's forward. - Vector3 cameraForward = new Vector3(); - cameraForward.x = float.Parse(cameraMatrix[2].ToString()); - cameraForward.y = float.Parse(cameraMatrix[6].ToString()); - cameraForward.z = float.Parse(cameraMatrix[10].ToString()); - return cameraForward; - } + // Build accountId to name map first + Dictionary authorNamesById = new Dictionary(); + JToken accounts = results["account"]; + if (accounts != null) + { + foreach (JToken account in accounts) + { + string id = account.First["accountId"].ToString(); + string name = ""; + JToken displayName = account.First["displayName"]; + if (displayName != null) + { + name = displayName.ToString(); + } + authorNamesById.Add(id, name); + } + } - // As above, accepting a string response (such that we can parse on a background thread). - public static bool ParseAsset(string response, out ObjectStoreEntry objectStoreEntry, bool hackUrls) { - return ParseAsset(JObject.Parse(response), out objectStoreEntry, hackUrls); - } + // Then parse the assets. + List objectStoreEntries = new List(); + + string firstAssetId = null; + foreach (JToken asset in assets) + { + ObjectStoreEntry objectStoreEntry; + string author = null; + var accountId = asset["accountId"]; + if (accountId != null) + { + authorNamesById.TryGetValue(accountId.ToString(), out author); + } + + if (type == PolyMenuMain.CreationType.FEATURED || type == PolyMenuMain.CreationType.LIKED) + { + string assetId = asset["assetId"].ToString(); + if (firstAssetId == null) + { + firstAssetId = assetId; + } + // Once we've seen an ID we've seen before, no need to continue through the list. This helps with polling + // regularly. This assumes new items always appear at the top of the list; we explicitly ask Zandria to sort by + // featured/liked time, descending. + if ((type == PolyMenuMain.CreationType.FEATURED && mostRecentFeaturedAssetId == assetId) + || (type == PolyMenuMain.CreationType.LIKED && mostRecentLikedAssetId == assetId)) + { + break; + } + } + if (ParseAsset(asset, out objectStoreEntry, hackUrls)) + { + objectStoreEntry.author = author; + objectStoreEntries.Add(objectStoreEntry); + } + } - /// - /// Fetch a list of featured models, together with their metadata, from the assets service. - /// Only searches for models with CC-BY licensing to avoid any complicated questions around non-remixable models. - /// Requests a create-time-descending ordering. - /// - /// A callback to which to pass the results. - /// Whether this is not the first call to this function. - public void GetFeaturedModels(System.Action successCallback, System.Action failureCallback, - bool isRecursion = false) { - // We wrap in a for loop so we can re-authorise if access tokens have become stale. - UnityWebRequest request = GetRequest(FeaturedModelsSearchUrl(), "text/text"); - PeltzerMain.Instance.webRequestManager.EnqueueRequest( - () => { return request; }, - (bool success, int responseCode, byte[] responseBytes) => StartCoroutine( - ProcessGetFeaturedModelsResponse( - success, responseCode, responseBytes, request, successCallback, failureCallback)), - maxAgeMillis: WebRequestManager.CACHE_NONE); - } + if (type == PolyMenuMain.CreationType.FEATURED) + { + mostRecentFeaturedAssetId = firstAssetId; + } + else if (type == PolyMenuMain.CreationType.LIKED) + { + mostRecentLikedAssetId = firstAssetId; + } + objectStoreSearchResult.results = objectStoreEntries.ToArray(); + return true; + } - // Deals with the response of a GetFeaturedModels request, retrying it if an auth token was stale. - private IEnumerator ProcessGetFeaturedModelsResponse(bool success, int responseCode, byte[] responseBytes, - UnityWebRequest request, System.Action successCallback, - System.Action failureCallback, bool isRecursion = false) { - if (!success || responseCode == 401) { - if (isRecursion) { - Debug.Log(GetDebugString(request, "Failed to get featured models")); - yield break; - } - yield return OAuth2Identity.Instance.Reauthorize(); - GetFeaturedModels(successCallback, failureCallback, /* isRecursion */ true); - } else { - PeltzerMain.Instance.DoPolyMenuBackgroundWork( - new ParseAssetsBackgroundWork(Encoding.UTF8.GetString(responseBytes), - PolyMenuMain.CreationType.FEATURED, successCallback, failureCallback)); - } - } + /// + /// Parses a single asset as defined in vr/assets/v1/asset.proto + /// + /// + public static bool ParseAsset(JToken asset, out ObjectStoreEntry objectStoreEntry, bool hackUrls) + { + objectStoreEntry = new ObjectStoreEntry(); + + if (asset["accessLevel"] == null) + { + Debug.Log("Asset had no access level set"); + return false; + } + objectStoreEntry.isPrivateAsset = asset["accessLevel"].ToString() == "PRIVATE"; + + objectStoreEntry.id = asset["assetId"].ToString(); + JToken thumbnailRoot = asset["thumbnail"]; + if (thumbnailRoot != null) + { + IJEnumerable thumbnailElements = asset["thumbnail"].AsJEnumerable(); + foreach (JToken thumbnailElement in thumbnailElements) + { + objectStoreEntry.thumbnail = thumbnailElement["typeInfo"]["imageInfo"]["fifeUrl"].ToString(); + break; + } + } + List tags = new List(); + IJEnumerable assetTags = asset["tag"].AsJEnumerable(); + if (assetTags != null) + { + foreach (JToken assetTag in assetTags) + { + tags.Add(assetTag.ToString()); + } + if (tags.Count > 0) + { + objectStoreEntry.tags = tags.ToArray(); + } + } + ObjectStoreObjectAssetsWrapper entryAssets = new ObjectStoreObjectAssetsWrapper(); + ObjectStorePeltzerAssets blocksAsset = new ObjectStorePeltzerAssets(); + // 7 is the enum for Blocks in ElementType + // A bit ugly: we simply take one arbitrary entry (we assume only one entry exists, as we only ever upload one). + blocksAsset.rootUrl = asset["formatList"]["7"]["format"][0]["root"]["dataUrl"].ToString(); + + blocksAsset.baseFile = ""; + entryAssets.peltzer = blocksAsset; + objectStoreEntry.assets = entryAssets; + objectStoreEntry.title = asset["displayName"].ToString(); + objectStoreEntry.createdDate = DateTime.Parse(asset["createTime"].ToString()); + objectStoreEntry.cameraForward = GetCameraForward(asset["cameraParams"]); + return true; + } - /// - /// Fetch a list of the authenticated user's models, together with their metadata, from the assets service. - /// Requests a create-time-descending ordering. - /// - /// A callback to which to pass the results. - public void GetYourModels(System.Action successCallback, System.Action failureCallback, - bool isRecursion = false) { - UnityWebRequest request = GetRequest(YourModelsSearchUrl(), "text/text"); - PeltzerMain.Instance.webRequestManager.EnqueueRequest( - () => { return request; }, - (bool success, int responseCode, byte[] responseBytes) => StartCoroutine( - ProcessGetYourModelsResponse( - success, responseCode, responseBytes, request, successCallback, failureCallback)), - maxAgeMillis: WebRequestManager.CACHE_NONE); - } + /// + /// Parse the camera parameter matrix from Zandria to extract the camera's forward, if available. + /// + /// A 4x4 matrix holding information about the camera's position and + /// rotation: + /// Row major + /// * * Fx Px + /// * * Fy Py + /// * * Fz Pz + /// 0 0 0 1 + /// A string of three float values separated by spaces that represent the camera forward. + private static Vector3 GetCameraForward(JToken cameraParams) + { + JToken cameraMatrix = cameraParams["matrix4x4"]; + if (cameraMatrix == null) return Vector3.zero; + // We want the third column, which holds the camera's forward. + Vector3 cameraForward = new Vector3(); + cameraForward.x = float.Parse(cameraMatrix[2].ToString()); + cameraForward.y = float.Parse(cameraMatrix[6].ToString()); + cameraForward.z = float.Parse(cameraMatrix[10].ToString()); + return cameraForward; + } - // Deals with the response of a GetYourModels request, retrying it if an auth token was stale. - private IEnumerator ProcessGetYourModelsResponse(bool success, int responseCode, byte[] responseBytes, - UnityWebRequest request, System.Action successCallback, - System.Action failureCallback, bool isRecursion = false) { - if (!success || responseCode == 401) { - if (isRecursion) { - Debug.Log(GetDebugString(request, "Failed to get your models")); - yield break; - } - yield return OAuth2Identity.Instance.Reauthorize(); - GetYourModels(successCallback, failureCallback); - } else { - PeltzerMain.Instance.DoPolyMenuBackgroundWork(new ParseAssetsBackgroundWork( - Encoding.UTF8.GetString(responseBytes), PolyMenuMain.CreationType.YOUR, successCallback, - failureCallback, hackUrls: true)); - } - } + // As above, accepting a string response (such that we can parse on a background thread). + public static bool ParseAsset(string response, out ObjectStoreEntry objectStoreEntry, bool hackUrls) + { + return ParseAsset(JObject.Parse(response), out objectStoreEntry, hackUrls); + } - /// - /// Fetch a list of models authenticated user has liked, together with their metadata, from the assets service. - /// Only searches for models with CC-BY licensing to avoid any complicated questions around non-remixable models. - /// Requests a create-time-descending ordering. - /// - /// A callback to which to pass the results. - public void GetLikedModels(System.Action successCallback, System.Action failureCallback) { - UnityWebRequest request = GetRequest(LikedModelsSearchUrl(), "text/text"); - PeltzerMain.Instance.webRequestManager.EnqueueRequest( - () => { return request; }, - (bool success, int responseCode, byte[] responseBytes) => StartCoroutine( - ProcessGetLikedModelsResponse( - success, responseCode, responseBytes, request, successCallback, failureCallback)), - maxAgeMillis: WebRequestManager.CACHE_NONE); - } + /// + /// Fetch a list of featured models, together with their metadata, from the assets service. + /// Only searches for models with CC-BY licensing to avoid any complicated questions around non-remixable models. + /// Requests a create-time-descending ordering. + /// + /// A callback to which to pass the results. + /// Whether this is not the first call to this function. + public void GetFeaturedModels(System.Action successCallback, System.Action failureCallback, + bool isRecursion = false) + { + // We wrap in a for loop so we can re-authorise if access tokens have become stale. + UnityWebRequest request = GetRequest(FeaturedModelsSearchUrl(), "text/text"); + PeltzerMain.Instance.webRequestManager.EnqueueRequest( + () => { return request; }, + (bool success, int responseCode, byte[] responseBytes) => StartCoroutine( + ProcessGetFeaturedModelsResponse( + success, responseCode, responseBytes, request, successCallback, failureCallback)), + maxAgeMillis: WebRequestManager.CACHE_NONE); + } - // Deals with the response of a GetLikedModels request, retrying it if an auth token was stale. - private IEnumerator ProcessGetLikedModelsResponse(bool success, int responseCode, byte[] responseBytes, - UnityWebRequest request, System.Action successCallback, System.Action failureCallback, - bool isRecursion = false) { - if (!success || responseCode == 401) { - if (isRecursion) { - Debug.Log(GetDebugString(request, "Failed to get liked models")); - yield break; - } - yield return OAuth2Identity.Instance.Reauthorize(); - GetLikedModels(successCallback, failureCallback); - } else { - PeltzerMain.Instance.DoPolyMenuBackgroundWork(new ParseAssetsBackgroundWork( - Encoding.UTF8.GetString(responseBytes), PolyMenuMain.CreationType.LIKED, successCallback, failureCallback)); - } - } + // Deals with the response of a GetFeaturedModels request, retrying it if an auth token was stale. + private IEnumerator ProcessGetFeaturedModelsResponse(bool success, int responseCode, byte[] responseBytes, + UnityWebRequest request, System.Action successCallback, + System.Action failureCallback, bool isRecursion = false) + { + if (!success || responseCode == 401) + { + if (isRecursion) + { + Debug.Log(GetDebugString(request, "Failed to get featured models")); + yield break; + } + yield return OAuth2Identity.Instance.Reauthorize(); + GetFeaturedModels(successCallback, failureCallback, /* isRecursion */ true); + } + else + { + PeltzerMain.Instance.DoPolyMenuBackgroundWork( + new ParseAssetsBackgroundWork(Encoding.UTF8.GetString(responseBytes), + PolyMenuMain.CreationType.FEATURED, successCallback, failureCallback)); + } + } - /// - /// Fetch a specific asset. - /// - /// A callback to which to pass the results. - public void GetAsset(string assetId, System.Action callback) { - string url = String.Format("{0}/v1/assets/{1}?key={2}", BaseUrl(), assetId, POLY_KEY); - UnityWebRequest request = GetRequest(url, "text/text"); - PeltzerMain.Instance.webRequestManager.EnqueueRequest( - () => { return request; }, - (bool success, int responseCode, byte[] responseBytes) => StartCoroutine( - ProcessGetAssetResponse(success, responseCode, responseBytes, request, assetId, callback)), - maxAgeMillis: WebRequestManager.CACHE_NONE); - } + /// + /// Fetch a list of the authenticated user's models, together with their metadata, from the assets service. + /// Requests a create-time-descending ordering. + /// + /// A callback to which to pass the results. + public void GetYourModels(System.Action successCallback, System.Action failureCallback, + bool isRecursion = false) + { + UnityWebRequest request = GetRequest(YourModelsSearchUrl(), "text/text"); + PeltzerMain.Instance.webRequestManager.EnqueueRequest( + () => { return request; }, + (bool success, int responseCode, byte[] responseBytes) => StartCoroutine( + ProcessGetYourModelsResponse( + success, responseCode, responseBytes, request, successCallback, failureCallback)), + maxAgeMillis: WebRequestManager.CACHE_NONE); + } - // Deals with the response of a GetAsset request, retrying it if an auth token was stale. - private IEnumerator ProcessGetAssetResponse(bool success, int responseCode, byte[] responseBytes, - UnityWebRequest request, string assetId, System.Action callback, bool isRecursion = false) { - if (!success || responseCode == 401) { - if (isRecursion) { - Debug.Log(GetDebugString(request, "Failed to fetch an asset with id " + assetId)); - yield break; - } - yield return OAuth2Identity.Instance.Reauthorize(); - GetAsset(assetId, callback); - } else { - PeltzerMain.Instance.DoPolyMenuBackgroundWork(new ParseAssetBackgroundWork( - Encoding.UTF8.GetString(responseBytes), callback, hackUrls: true)); - } - } + // Deals with the response of a GetYourModels request, retrying it if an auth token was stale. + private IEnumerator ProcessGetYourModelsResponse(bool success, int responseCode, byte[] responseBytes, + UnityWebRequest request, System.Action successCallback, + System.Action failureCallback, bool isRecursion = false) + { + if (!success || responseCode == 401) + { + if (isRecursion) + { + Debug.Log(GetDebugString(request, "Failed to get your models")); + yield break; + } + yield return OAuth2Identity.Instance.Reauthorize(); + GetYourModels(successCallback, failureCallback); + } + else + { + PeltzerMain.Instance.DoPolyMenuBackgroundWork(new ParseAssetsBackgroundWork( + Encoding.UTF8.GetString(responseBytes), PolyMenuMain.CreationType.YOUR, successCallback, + failureCallback, hackUrls: true)); + } + } - /// - /// Uploads all the resources for a model to the Assets Service (in parallel). - /// If every upload was successful, creates an asset out of them, and calls PeltzerMain to handle success. - /// Else, calls PeltzerMain to handle failure. - /// Whilst propagating success/failure as a return type might be more idiomatic, it's a pain here, so we - /// avoid it. /shrug. - /// - /// The bytes of an OBJ file representing this model - /// The poly count of the OBJ file - /// The bytes of a triangulated OBJ file representing this model - /// The poly count of the triangulated OBJ file - /// The bytes of an MTL file to pair with the OBJ file - /// All data required for the glTF files representing this model - /// The bytes of a .fbx file representing this model - /// The bytes of a PeltzerFile representing this model - /// The bytes of an image file giving a thumbnail view of this model - /// If true, opens the 'publish' dialog on a user's browser after successful creation - /// If true, only the currently selected content is saved. - public IEnumerator UploadModel(HashSet remixIds, byte[] objFile, int objPolyCount, - byte[] triangulatedObjFile, int triangulatedObjPolyCount, byte[] mtlFile, FormatSaveData gltfData, - byte[] fbxFile, byte[] blocksFile, byte[] thumbnailFile, bool publish, bool saveSelected) { - - // Upload the resources. - yield return UploadResources(objFile, triangulatedObjFile, mtlFile, gltfData, fbxFile, - blocksFile, thumbnailFile, saveSelected); - - // Create an asset if all uploads succeded. - if (resourceUploadSuccess) { - yield return CreateNewAsset(gltfData, objPolyCount, triangulatedObjPolyCount, remixIds, saveSelected); - } - - // Show a toast informing the user that they uploaded to Zandria (or that there was an error.) - PeltzerMain.Instance - .HandleSaveComplete(/* success */ assetCreationSuccess, assetCreationSuccess ? "Saved" : "Save failed"); - if (assetCreationSuccess) { - PeltzerMain.Instance.LoadSavedModelOntoPolyMenu(assetId, publish); - } - - if (assetCreationSuccess) { - // If we are only saving the selected content, then we don't want to overwrite the LastSavedAssetId - // as the id we are currently using is meant to be temporary. - if (!saveSelected) { - PeltzerMain.Instance.LastSavedAssetId = assetId; - } - if (publish) { - OpenPublishUrl(assetId); - } else { - // Don't prompt to publish if the tutorial is active or if we are only saving a selected - // subset of the model. - if (!PeltzerMain.Instance.tutorialManager.TutorialOccurring() && !saveSelected) { - // Encourage users to publish their creation. - PeltzerMain.Instance.SetPublishAfterSavePromptActive(); - } - if (!hasSavedSuccessfully) { - // On the first successful save to Zandria we want to open up the browser to the users models so that they - // understand that we save to the cloud and shows them where they can find their models. - hasSavedSuccessfully = true; - OpenSaveUrl(); - } - } - } - } + /// + /// Fetch a list of models authenticated user has liked, together with their metadata, from the assets service. + /// Only searches for models with CC-BY licensing to avoid any complicated questions around non-remixable models. + /// Requests a create-time-descending ordering. + /// + /// A callback to which to pass the results. + public void GetLikedModels(System.Action successCallback, System.Action failureCallback) + { + UnityWebRequest request = GetRequest(LikedModelsSearchUrl(), "text/text"); + PeltzerMain.Instance.webRequestManager.EnqueueRequest( + () => { return request; }, + (bool success, int responseCode, byte[] responseBytes) => StartCoroutine( + ProcessGetLikedModelsResponse( + success, responseCode, responseBytes, request, successCallback, failureCallback)), + maxAgeMillis: WebRequestManager.CACHE_NONE); + } - /// - /// Updates an existing asset after uploading the new resources for it. - /// - public IEnumerator UpdateModel(string assetId, HashSet remixIds, byte[] objFile, int objPolyCount, - byte[] triangulatedObjFile, int triangulatedObjPolyCount, byte[] mtlFile, FormatSaveData gltfData, - byte[] fbxFile, byte[] blocksFile, byte[] thumbnailFile, bool publish) { - - // Upload the resources. - yield return UploadResources(objFile, triangulatedObjFile, mtlFile, gltfData, fbxFile, - blocksFile, thumbnailFile, saveSelected:false); - - // Update the asset if all uploads succeded. - if (resourceUploadSuccess) { - yield return UpdateAsset(assetId, gltfData, objPolyCount, triangulatedObjPolyCount, remixIds); - } - - // Show a toast informing the user that they uploaded to Zandria, or that there was an error. - PeltzerMain.Instance - .HandleSaveComplete(/* success */ assetCreationSuccess, assetCreationSuccess ? "Saved" : "Save failed"); - if (assetCreationSuccess) { - PeltzerMain.Instance.LastSavedAssetId = assetId; - if (publish) { - OpenPublishUrl(assetId); - } else { - } - } else { - } - } + // Deals with the response of a GetLikedModels request, retrying it if an auth token was stale. + private IEnumerator ProcessGetLikedModelsResponse(bool success, int responseCode, byte[] responseBytes, + UnityWebRequest request, System.Action successCallback, System.Action failureCallback, + bool isRecursion = false) + { + if (!success || responseCode == 401) + { + if (isRecursion) + { + Debug.Log(GetDebugString(request, "Failed to get liked models")); + yield break; + } + yield return OAuth2Identity.Instance.Reauthorize(); + GetLikedModels(successCallback, failureCallback); + } + else + { + PeltzerMain.Instance.DoPolyMenuBackgroundWork(new ParseAssetsBackgroundWork( + Encoding.UTF8.GetString(responseBytes), PolyMenuMain.CreationType.LIKED, successCallback, failureCallback)); + } + } - public static void OpenPublishUrl(string assetId) { - string publishUrl = PublishUrl() + assetId; - string emailAddress = OAuth2Identity.Instance.Profile == null ? null : OAuth2Identity.Instance.Profile.email; - string urlToOpen = emailAddress == null ? publishUrl : - string.Format("https://accounts.google.com/AccountChooser?Email={0}&continue={1}", emailAddress, publishUrl); - PeltzerMain.Instance.paletteController.SetPublishDialogActive(); - System.Diagnostics.Process.Start(urlToOpen); - } + /// + /// Fetch a specific asset. + /// + /// A callback to which to pass the results. + public void GetAsset(string assetId, System.Action callback) + { + string url = String.Format("{0}/v1/assets/{1}?key={2}", BaseUrl(), assetId, POLY_KEY); + UnityWebRequest request = GetRequest(url, "text/text"); + PeltzerMain.Instance.webRequestManager.EnqueueRequest( + () => { return request; }, + (bool success, int responseCode, byte[] responseBytes) => StartCoroutine( + ProcessGetAssetResponse(success, responseCode, responseBytes, request, assetId, callback)), + maxAgeMillis: WebRequestManager.CACHE_NONE); + } - private void OpenSaveUrl() { - if (PeltzerMain.Instance.HasOpenedSaveUrlThisSession) { - return; - } - string emailAddress = OAuth2Identity.Instance.Profile == null ? null : OAuth2Identity.Instance.Profile.email; - string urlToOpen = emailAddress == null ? SaveUrl() : - string.Format("https://accounts.google.com/AccountChooser?Email={0}&continue={1}", emailAddress, SaveUrl()); - System.Diagnostics.Process.Start(urlToOpen); - PeltzerMain.Instance.HasOpenedSaveUrlThisSession = true; - } + // Deals with the response of a GetAsset request, retrying it if an auth token was stale. + private IEnumerator ProcessGetAssetResponse(bool success, int responseCode, byte[] responseBytes, + UnityWebRequest request, string assetId, System.Action callback, bool isRecursion = false) + { + if (!success || responseCode == 401) + { + if (isRecursion) + { + Debug.Log(GetDebugString(request, "Failed to fetch an asset with id " + assetId)); + yield break; + } + yield return OAuth2Identity.Instance.Reauthorize(); + GetAsset(assetId, callback); + } + else + { + PeltzerMain.Instance.DoPolyMenuBackgroundWork(new ParseAssetBackgroundWork( + Encoding.UTF8.GetString(responseBytes), callback, hackUrls: true)); + } + } - /// - /// Upload all required resources for a creation/overwrite request. - /// - private IEnumerator UploadResources(byte[] objFile, byte[] triangulatedObjFile, - byte[] mtlFile, FormatSaveData gltfData, byte[] fbxFile, byte[] blocksFile, byte[] thumbnailFile, - bool saveSelected) { - StartCoroutine(AddResource(ExportUtils.OBJ_FILENAME, "text/plain", objFile, "obj")); - StartCoroutine(AddResource(ExportUtils.TRIANGULATED_OBJ_FILENAME, "text/plain", triangulatedObjFile, - "triangulated-obj")); - StartCoroutine(AddResource(ExportUtils.MTL_FILENAME, "text/plain", mtlFile, "mtl")); - StartCoroutine(AddResource(ExportUtils.FBX_FILENAME, "application/octet-stream", fbxFile, "fbx")); - StartCoroutine(AddResource(gltfData.root.fileName, gltfData.root.mimeType, gltfData.root.multipartBytes, - gltfData.root.tag)); - - for (int i = 0; i < gltfData.resources.Count; i++) { - FormatDataFile file = gltfData.resources[i]; - StartCoroutine(AddResource(file.fileName, file.mimeType, file.multipartBytes, file.tag + i)); - } - - StartCoroutine(AddResource(ExportUtils.BLOCKS_FILENAME, "application/octet-stream", blocksFile, "blocks")); - if (!saveSelected){ - StartCoroutine(AddResource(ExportUtils.THUMBNAIL_FILENAME, "image/png", thumbnailFile, "png")); - } - - // Wait for all uploads to complete (or fail); - UploadState overallState = UploadState.IN_PROGRESS; - while (overallState == UploadState.IN_PROGRESS) { - bool allSucceeded = true; - foreach (KeyValuePair pair in elementUploadStates) { - switch (pair.Value) { - case UploadState.FAILED: - Debug.Log("Failed to upload " + pair.Key); - allSucceeded = false; - overallState = UploadState.FAILED; - resourceUploadSuccess = false; - break; - case UploadState.IN_PROGRESS: - allSucceeded = false; - break; - } - } - if (allSucceeded) { - overallState = UploadState.SUCCEEDED; - resourceUploadSuccess = true; - } - yield return null; - } - } + /// + /// Uploads all the resources for a model to the Assets Service (in parallel). + /// If every upload was successful, creates an asset out of them, and calls PeltzerMain to handle success. + /// Else, calls PeltzerMain to handle failure. + /// Whilst propagating success/failure as a return type might be more idiomatic, it's a pain here, so we + /// avoid it. /shrug. + /// + /// The bytes of an OBJ file representing this model + /// The poly count of the OBJ file + /// The bytes of a triangulated OBJ file representing this model + /// The poly count of the triangulated OBJ file + /// The bytes of an MTL file to pair with the OBJ file + /// All data required for the glTF files representing this model + /// The bytes of a .fbx file representing this model + /// The bytes of a PeltzerFile representing this model + /// The bytes of an image file giving a thumbnail view of this model + /// If true, opens the 'publish' dialog on a user's browser after successful creation + /// If true, only the currently selected content is saved. + public IEnumerator UploadModel(HashSet remixIds, byte[] objFile, int objPolyCount, + byte[] triangulatedObjFile, int triangulatedObjPolyCount, byte[] mtlFile, FormatSaveData gltfData, + byte[] fbxFile, byte[] blocksFile, byte[] thumbnailFile, bool publish, bool saveSelected) + { + + // Upload the resources. + yield return UploadResources(objFile, triangulatedObjFile, mtlFile, gltfData, fbxFile, + blocksFile, thumbnailFile, saveSelected); + + // Create an asset if all uploads succeded. + if (resourceUploadSuccess) + { + yield return CreateNewAsset(gltfData, objPolyCount, triangulatedObjPolyCount, remixIds, saveSelected); + } - /// - /// Create a new asset from the uploaded files. - /// - private IEnumerator CreateNewAsset(FormatSaveData saveData, int objPolyCount, int triangulatedObjPolyCount, - HashSet remixIds, bool saveSelected) { - string json = CreateJsonForAssetResources(saveData, remixIds, objPolyCount, triangulatedObjPolyCount, - /* displayName */ "(Untitled)", saveSelected); - string url = String.Format("{0}/v1/assets?key={1}", BaseUrl(), POLY_KEY); - UnityWebRequest request = new UnityWebRequest(); - - // We wrap in a for loop so we can re-authorise if access tokens have become stale. - for (int i = 0; i < 2; i++) { - request = PostRequest(url, "application/json", Encoding.UTF8.GetBytes(json)); - request.downloadHandler = new DownloadHandlerBuffer(); - - yield return request.Send(); - - if (request.responseCode == 401 || request.isNetworkError) { - yield return OAuth2Identity.Instance.Reauthorize(); - continue; - } else { - assetId = null; - Regex regex = new Regex(ASSET_ID_MATCH); - Match match = regex.Match(request.downloadHandler.text); - if (match.Success) { - assetId = match.Groups[1].Captures[0].Value; - // Only update the global AssetId if the user has not hit 'new model' or opened a model - // since this save began, and if we are not only saving selected content, as the id used - // is meant to be temporary. - if (!PeltzerMain.Instance.newModelSinceLastSaved && !saveSelected) { - PeltzerMain.Instance.AssetId = assetId; - } - assetCreationSuccess = true; - } else { - Debug.Log("Failed to save to Assets Store. Response: " + request.downloadHandler.text); - } - yield break; - } - } - - Debug.Log(GetDebugString(request, "Failed to save to asset store")); - } + // Show a toast informing the user that they uploaded to Zandria (or that there was an error.) + PeltzerMain.Instance + .HandleSaveComplete(/* success */ assetCreationSuccess, assetCreationSuccess ? "Saved" : "Save failed"); + if (assetCreationSuccess) + { + PeltzerMain.Instance.LoadSavedModelOntoPolyMenu(assetId, publish); + } - /// - /// Update an existing asset. - /// - private IEnumerator UpdateAsset(string assetId, FormatSaveData saveData, int objPolyCount, - int triangulatedObjPolyCount, HashSet remixIds) { - string json = CreateJsonForAssetResources(saveData, remixIds, objPolyCount, triangulatedObjPolyCount, - /* displayName */ null, saveSelected:false); - string url = String.Format("{0}/v1/assets/{1}:updateData?key={2}", BaseUrl(), assetId, POLY_KEY); - UnityWebRequest request = new UnityWebRequest(); - - // We wrap in a for loop so we can re-authorise if access tokens have become stale. - for (int i = 0; i < 2; i++) { - request = Patch(url, "application/json", Encoding.UTF8.GetBytes(json)); - request.downloadHandler = new DownloadHandlerBuffer(); - - yield return request.Send(); - - if (request.responseCode == 401 || request.isNetworkError) { - yield return OAuth2Identity.Instance.Reauthorize(); - continue; - } else { - assetId = null; - Regex regex = new Regex(ASSET_ID_MATCH); - Match match = regex.Match(request.downloadHandler.text); - if (match.Success) { - assetId = match.Groups[1].Captures[0].Value; - PeltzerMain.Instance.UpdateCloudModelOntoPolyMenu(request.downloadHandler.text); - assetCreationSuccess = true; - } else { - Debug.Log("Failed to update " + assetId + " in Assets Store. Response: " + request.downloadHandler.text); - } - yield break; - } - } - Debug.Log(GetDebugString(request, "Failed to save to asset store")); - } + if (assetCreationSuccess) + { + // If we are only saving the selected content, then we don't want to overwrite the LastSavedAssetId + // as the id we are currently using is meant to be temporary. + if (!saveSelected) + { + PeltzerMain.Instance.LastSavedAssetId = assetId; + } + if (publish) + { + OpenPublishUrl(assetId); + } + else + { + // Don't prompt to publish if the tutorial is active or if we are only saving a selected + // subset of the model. + if (!PeltzerMain.Instance.tutorialManager.TutorialOccurring() && !saveSelected) + { + // Encourage users to publish their creation. + PeltzerMain.Instance.SetPublishAfterSavePromptActive(); + } + if (!hasSavedSuccessfully) + { + // On the first successful save to Zandria we want to open up the browser to the users models so that they + // understand that we save to the cloud and shows them where they can find their models. + hasSavedSuccessfully = true; + OpenSaveUrl(); + } + } + } + } - private string CreateJsonForAssetResources(FormatSaveData saveData, HashSet remixIds, - int objPolyCount, int triangulatedObjPolyCount, string displayName, bool saveSelected) { - List gltfResourceFiles = new List(); - for (int i = 0; i < saveData.resources.Count; i++) { - FormatDataFile dataFile = saveData.resources[i]; - gltfResourceFiles.Add(String.Format("\"resource_id\": \"{0}\"", elementIds[dataFile.tag + i])); - } - - string gltfFormatComplexity = ""; - if (saveData.triangleCount > 0) { - gltfFormatComplexity = String.Format("\"format_complexity\": {{ \"triangle_count\": {0} }},", - saveData.triangleCount); - } - string objFormatComplexity = String.Format("\"format_complexity\": {{ \"triangle_count\": {0} }},", - objPolyCount); - string triangulatedObjFormatComplexity = String.Format("\"format_complexity\": {{ \"triangle_count\": {0} }},", - triangulatedObjPolyCount); - - // Create asset using the uploaded components. - // Newtonsoft library doesn't like repeated keys, so we do it by hand. - string gltfResources = String.Join(",", gltfResourceFiles.ToArray()); - string gltfBlock = String.Format("\"format\": [ {{ \"root_id\": \"{0}\", {1} " + - "{2} }} ]", elementIds[saveData.root.tag], gltfFormatComplexity, gltfResources); - - string remixBlock; - // Note: we have to include the remix_info section even if it's empty, because its absence would - // mean "keep the existing remix IDs" (incorrect), not "there are no remix IDs" (correct). - StringBuilder remixBlockBuilder = new StringBuilder("\"remix_info\": { "); - foreach (string remixId in remixIds) { - remixBlockBuilder.Append("\"source_asset\": \"").Append(remixId).Append("\", "); - } - remixBlockBuilder.Append("}"); - remixBlock = remixBlockBuilder.ToString(); - - string prelude = displayName == null ? "{ " : String.Format("{{ \"display_name\": \"{0}\",", displayName); - - string json; - if (!saveSelected) { - json = String.Format( - "{0}\"thumbnail_id\": \"{1}\"," + - "\"format\": [ {{ \"format_type\": \"FORMAT_WAVEFRONT_OBJ\", \"root_id\": \"{2}\", {3} \"resource_id\": \"{4}\" }} ]," + - "\"format\": [ {{ \"format_type\": \"FORMAT_WAVEFRONT_OBJ_TRIANGULATED\", \"root_id\": \"{5}\", {6} \"resource_id\": \"{7}\" }} ]," + - "\"format\": [ {{ \"format_type\": \"FORMAT_AUTODESK_FBX\", \"root_id\": \"{8}\" }} ]," + - "\"format\": [ {{ \"format_type\": \"FORMAT_BLOCKS\", \"root_id\": \"{9}\" }} ], {10}, {11} }}", - prelude, elementIds["png"], - elementIds["obj"], objFormatComplexity, elementIds["mtl"], - elementIds["triangulated-obj"], triangulatedObjFormatComplexity, elementIds["mtl"], - elementIds["fbx"], - elementIds["blocks"], gltfBlock, remixBlock); - } - else { - json = String.Format( - "{0}" + - "\"format\": [ {{ \"format_type\": \"FORMAT_WAVEFRONT_OBJ\", \"root_id\": \"{1}\", {2} \"resource_id\": \"{3}\" }} ]," + - "\"format\": [ {{ \"format_type\": \"FORMAT_WAVEFRONT_OBJ_TRIANGULATED\", \"root_id\": \"{4}\", {5} \"resource_id\": \"{6}\" }} ]," + - "\"format\": [ {{ \"format_type\": \"FORMAT_AUTODESK_FBX\", \"root_id\": \"{7}\" }} ]," + - "\"format\": [ {{ \"format_type\": \"FORMAT_BLOCKS\", \"root_id\": \"{8}\" }} ], {9}, {10} }}", - prelude, - elementIds["obj"], objFormatComplexity, elementIds["mtl"], - elementIds["triangulated-obj"], triangulatedObjFormatComplexity, elementIds["mtl"], - elementIds["fbx"], - elementIds["blocks"], gltfBlock, remixBlock); - } - return json; - } + /// + /// Updates an existing asset after uploading the new resources for it. + /// + public IEnumerator UpdateModel(string assetId, HashSet remixIds, byte[] objFile, int objPolyCount, + byte[] triangulatedObjFile, int triangulatedObjPolyCount, byte[] mtlFile, FormatSaveData gltfData, + byte[] fbxFile, byte[] blocksFile, byte[] thumbnailFile, bool publish) + { + + // Upload the resources. + yield return UploadResources(objFile, triangulatedObjFile, mtlFile, gltfData, fbxFile, + blocksFile, thumbnailFile, saveSelected: false); + + // Update the asset if all uploads succeded. + if (resourceUploadSuccess) + { + yield return UpdateAsset(assetId, gltfData, objPolyCount, triangulatedObjPolyCount, remixIds); + } - /// - /// Add a resource to the existing asset. - /// - private IEnumerator AddResource(string filename, string mimeType, byte[] data, string key) { - elementUploadStates.Add(key, UploadState.IN_PROGRESS); - string url = string.Format("{0}/uploads", BaseUrl()); - UnityWebRequest request = new UnityWebRequest(); - - // We wrap in a for loop so we can re-authorise if access tokens have become stale. - for (int i = 0; i < 2; i++) { - request = PostRequest(url, "multipart/form-data; boundary=" + BOUNDARY, data, compressResourceUpload); - request.SetRequestHeader("X-Google-Project-Override", "apikey"); - request.downloadHandler = new DownloadHandlerBuffer(); - - yield return request.Send(); - - if (request.responseCode == 401 || request.isNetworkError) { - yield return OAuth2Identity.Instance.Reauthorize(); - continue; - } else { - Regex regex = new Regex(ELEMENT_ID_MATCH); - Match match = regex.Match(request.downloadHandler.text); - if (match.Success) { - elementIds[key] = match.Groups[1].Captures[0].Value; - elementUploadStates[key] = UploadState.SUCCEEDED; - } else { - Debug.Log(GetDebugString(request, "Failed to save " + filename + " to Assets Store.")); - elementUploadStates[key] = UploadState.FAILED; - } - yield break; + // Show a toast informing the user that they uploaded to Zandria, or that there was an error. + PeltzerMain.Instance + .HandleSaveComplete(/* success */ assetCreationSuccess, assetCreationSuccess ? "Saved" : "Save failed"); + if (assetCreationSuccess) + { + PeltzerMain.Instance.LastSavedAssetId = assetId; + if (publish) + { + OpenPublishUrl(assetId); + } + else + { + } + } + else + { + } } - } - elementUploadStates[key] = UploadState.FAILED; - Debug.Log(GetDebugString(request, "Failed to save " + filename + " to asset store")); - } + public static void OpenPublishUrl(string assetId) + { + string publishUrl = PublishUrl() + assetId; + string emailAddress = OAuth2Identity.Instance.Profile == null ? null : OAuth2Identity.Instance.Profile.email; + string urlToOpen = emailAddress == null ? publishUrl : + string.Format("https://accounts.google.com/AccountChooser?Email={0}&continue={1}", emailAddress, publishUrl); + PeltzerMain.Instance.paletteController.SetPublishDialogActive(); + System.Diagnostics.Process.Start(urlToOpen); + } - /// - /// Returns a debug string for an upload. - /// - /// - public static string GetDebugString(UnityWebRequest request, string preface) { - StringBuilder debugString = new StringBuilder(preface).AppendLine() - .Append("Response: ").AppendLine(request.downloadHandler.text) - .Append("Response Code: ").AppendLine(request.responseCode.ToString()) - .Append("Error Message: ").AppendLine(request.error); - - foreach (KeyValuePair header in request.GetResponseHeaders()) { - debugString.Append(header.Key).Append(" : ").AppendLine(header.Value); - } - return debugString.ToString(); - } + private void OpenSaveUrl() + { + if (PeltzerMain.Instance.HasOpenedSaveUrlThisSession) + { + return; + } + string emailAddress = OAuth2Identity.Instance.Profile == null ? null : OAuth2Identity.Instance.Profile.email; + string urlToOpen = emailAddress == null ? SaveUrl() : + string.Format("https://accounts.google.com/AccountChooser?Email={0}&continue={1}", emailAddress, SaveUrl()); + System.Diagnostics.Process.Start(urlToOpen); + PeltzerMain.Instance.HasOpenedSaveUrlThisSession = true; + } - /// - /// Build the binary multipart content manually, since Unity's multipart stuff is borked. - /// - public byte[] MultiPartContent(string filename, string mimeType, byte[] data) { - MemoryStream stream = new MemoryStream(); - StreamWriter sw = new StreamWriter(stream); - - // Write the media part of the request from the data. - sw.Write("--" + BOUNDARY); - sw.Write(string.Format( - "\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{0}\"\r\nContent-Type: {1}\r\n\r\n", - filename, mimeType)); - sw.Flush(); - stream.Write(data, 0, data.Length); - sw.Write("\r\n--" + BOUNDARY + "--\r\n"); - sw.Close(); - - return stream.ToArray(); - } + /// + /// Upload all required resources for a creation/overwrite request. + /// + private IEnumerator UploadResources(byte[] objFile, byte[] triangulatedObjFile, + byte[] mtlFile, FormatSaveData gltfData, byte[] fbxFile, byte[] blocksFile, byte[] thumbnailFile, + bool saveSelected) + { + StartCoroutine(AddResource(ExportUtils.OBJ_FILENAME, "text/plain", objFile, "obj")); + StartCoroutine(AddResource(ExportUtils.TRIANGULATED_OBJ_FILENAME, "text/plain", triangulatedObjFile, + "triangulated-obj")); + StartCoroutine(AddResource(ExportUtils.MTL_FILENAME, "text/plain", mtlFile, "mtl")); + StartCoroutine(AddResource(ExportUtils.FBX_FILENAME, "application/octet-stream", fbxFile, "fbx")); + StartCoroutine(AddResource(gltfData.root.fileName, gltfData.root.mimeType, gltfData.root.multipartBytes, + gltfData.root.tag)); + + for (int i = 0; i < gltfData.resources.Count; i++) + { + FormatDataFile file = gltfData.resources[i]; + StartCoroutine(AddResource(file.fileName, file.mimeType, file.multipartBytes, file.tag + i)); + } - /// - /// Compress bytes using deflate. - /// - private byte[] Deflate(byte[] data) { - Deflater deflater = new Deflater(Deflater.DEFLATED, true); - deflater.SetInput(data); - deflater.Finish(); - - using (var ms = new MemoryStream()) { - lock (deflateMutex) { - while (!deflater.IsFinished) { - var read = deflater.Deflate(tempDeflateBuffer); - ms.Write(tempDeflateBuffer, 0, read); - } - deflater.Reset(); - } - return ms.ToArray(); - } - } + StartCoroutine(AddResource(ExportUtils.BLOCKS_FILENAME, "application/octet-stream", blocksFile, "blocks")); + if (!saveSelected) + { + StartCoroutine(AddResource(ExportUtils.THUMBNAIL_FILENAME, "image/png", thumbnailFile, "png")); + } - /// - /// Delete the specified asset. - /// - public IEnumerator DeleteAsset(string assetId) { - string url = String.Format("{0}/v1/assets/{1}?key={2}", BaseUrl(), assetId, POLY_KEY); - UnityWebRequest request = new UnityWebRequest(); + // Wait for all uploads to complete (or fail); + UploadState overallState = UploadState.IN_PROGRESS; + while (overallState == UploadState.IN_PROGRESS) + { + bool allSucceeded = true; + foreach (KeyValuePair pair in elementUploadStates) + { + switch (pair.Value) + { + case UploadState.FAILED: + Debug.Log("Failed to upload " + pair.Key); + allSucceeded = false; + overallState = UploadState.FAILED; + resourceUploadSuccess = false; + break; + case UploadState.IN_PROGRESS: + allSucceeded = false; + break; + } + } + if (allSucceeded) + { + overallState = UploadState.SUCCEEDED; + resourceUploadSuccess = true; + } + yield return null; + } + } - // We wrap in a for loop so we can re-authorise if access tokens have become stale. - for (int i = 0; i < 2; i++) { - request = DeleteRequest(url, "application/json"); + /// + /// Create a new asset from the uploaded files. + /// + private IEnumerator CreateNewAsset(FormatSaveData saveData, int objPolyCount, int triangulatedObjPolyCount, + HashSet remixIds, bool saveSelected) + { + string json = CreateJsonForAssetResources(saveData, remixIds, objPolyCount, triangulatedObjPolyCount, + /* displayName */ "(Untitled)", saveSelected); + string url = String.Format("{0}/v1/assets?key={1}", BaseUrl(), POLY_KEY); + UnityWebRequest request = new UnityWebRequest(); + + // We wrap in a for loop so we can re-authorise if access tokens have become stale. + for (int i = 0; i < 2; i++) + { + request = PostRequest(url, "application/json", Encoding.UTF8.GetBytes(json)); + request.downloadHandler = new DownloadHandlerBuffer(); + + yield return request.Send(); + + if (request.responseCode == 401 || request.isNetworkError) + { + yield return OAuth2Identity.Instance.Reauthorize(); + continue; + } + else + { + assetId = null; + Regex regex = new Regex(ASSET_ID_MATCH); + Match match = regex.Match(request.downloadHandler.text); + if (match.Success) + { + assetId = match.Groups[1].Captures[0].Value; + // Only update the global AssetId if the user has not hit 'new model' or opened a model + // since this save began, and if we are not only saving selected content, as the id used + // is meant to be temporary. + if (!PeltzerMain.Instance.newModelSinceLastSaved && !saveSelected) + { + PeltzerMain.Instance.AssetId = assetId; + } + assetCreationSuccess = true; + } + else + { + Debug.Log("Failed to save to Assets Store. Response: " + request.downloadHandler.text); + } + yield break; + } + } - yield return request.Send(); + Debug.Log(GetDebugString(request, "Failed to save to asset store")); + } - if (request.responseCode == 401 || request.isNetworkError) { - yield return OAuth2Identity.Instance.Reauthorize(); - continue; - } else { - yield break; + /// + /// Update an existing asset. + /// + private IEnumerator UpdateAsset(string assetId, FormatSaveData saveData, int objPolyCount, + int triangulatedObjPolyCount, HashSet remixIds) + { + string json = CreateJsonForAssetResources(saveData, remixIds, objPolyCount, triangulatedObjPolyCount, + /* displayName */ null, saveSelected: false); + string url = String.Format("{0}/v1/assets/{1}:updateData?key={2}", BaseUrl(), assetId, POLY_KEY); + UnityWebRequest request = new UnityWebRequest(); + + // We wrap in a for loop so we can re-authorise if access tokens have become stale. + for (int i = 0; i < 2; i++) + { + request = Patch(url, "application/json", Encoding.UTF8.GetBytes(json)); + request.downloadHandler = new DownloadHandlerBuffer(); + + yield return request.Send(); + + if (request.responseCode == 401 || request.isNetworkError) + { + yield return OAuth2Identity.Instance.Reauthorize(); + continue; + } + else + { + assetId = null; + Regex regex = new Regex(ASSET_ID_MATCH); + Match match = regex.Match(request.downloadHandler.text); + if (match.Success) + { + assetId = match.Groups[1].Captures[0].Value; + PeltzerMain.Instance.UpdateCloudModelOntoPolyMenu(request.downloadHandler.text); + assetCreationSuccess = true; + } + else + { + Debug.Log("Failed to update " + assetId + " in Assets Store. Response: " + request.downloadHandler.text); + } + yield break; + } + } + Debug.Log(GetDebugString(request, "Failed to save to asset store")); } - } - Debug.Log(GetDebugString(request, "Failed to delete " + assetId)); - } + private string CreateJsonForAssetResources(FormatSaveData saveData, HashSet remixIds, + int objPolyCount, int triangulatedObjPolyCount, string displayName, bool saveSelected) + { + List gltfResourceFiles = new List(); + for (int i = 0; i < saveData.resources.Count; i++) + { + FormatDataFile dataFile = saveData.resources[i]; + gltfResourceFiles.Add(String.Format("\"resource_id\": \"{0}\"", elementIds[dataFile.tag + i])); + } - /// - /// Forms a GET request from a HTTP path. - /// - public UnityWebRequest GetRequest(string path, string contentType) { - // The default constructor for a UnityWebRequest gives a GET request. - UnityWebRequest request = new UnityWebRequest(path); - request.SetRequestHeader("Content-type", contentType); - if (OAuth2Identity.Instance.HasAccessToken) { - OAuth2Identity.Instance.Authenticate(request); - } - return request; - } + string gltfFormatComplexity = ""; + if (saveData.triangleCount > 0) + { + gltfFormatComplexity = String.Format("\"format_complexity\": {{ \"triangle_count\": {0} }},", + saveData.triangleCount); + } + string objFormatComplexity = String.Format("\"format_complexity\": {{ \"triangle_count\": {0} }},", + objPolyCount); + string triangulatedObjFormatComplexity = String.Format("\"format_complexity\": {{ \"triangle_count\": {0} }},", + triangulatedObjPolyCount); + + // Create asset using the uploaded components. + // Newtonsoft library doesn't like repeated keys, so we do it by hand. + string gltfResources = String.Join(",", gltfResourceFiles.ToArray()); + string gltfBlock = String.Format("\"format\": [ {{ \"root_id\": \"{0}\", {1} " + + "{2} }} ]", elementIds[saveData.root.tag], gltfFormatComplexity, gltfResources); + + string remixBlock; + // Note: we have to include the remix_info section even if it's empty, because its absence would + // mean "keep the existing remix IDs" (incorrect), not "there are no remix IDs" (correct). + StringBuilder remixBlockBuilder = new StringBuilder("\"remix_info\": { "); + foreach (string remixId in remixIds) + { + remixBlockBuilder.Append("\"source_asset\": \"").Append(remixId).Append("\", "); + } + remixBlockBuilder.Append("}"); + remixBlock = remixBlockBuilder.ToString(); + + string prelude = displayName == null ? "{ " : String.Format("{{ \"display_name\": \"{0}\",", displayName); + + string json; + if (!saveSelected) + { + json = String.Format( + "{0}\"thumbnail_id\": \"{1}\"," + + "\"format\": [ {{ \"format_type\": \"FORMAT_WAVEFRONT_OBJ\", \"root_id\": \"{2}\", {3} \"resource_id\": \"{4}\" }} ]," + + "\"format\": [ {{ \"format_type\": \"FORMAT_WAVEFRONT_OBJ_TRIANGULATED\", \"root_id\": \"{5}\", {6} \"resource_id\": \"{7}\" }} ]," + + "\"format\": [ {{ \"format_type\": \"FORMAT_AUTODESK_FBX\", \"root_id\": \"{8}\" }} ]," + + "\"format\": [ {{ \"format_type\": \"FORMAT_BLOCKS\", \"root_id\": \"{9}\" }} ], {10}, {11} }}", + prelude, elementIds["png"], + elementIds["obj"], objFormatComplexity, elementIds["mtl"], + elementIds["triangulated-obj"], triangulatedObjFormatComplexity, elementIds["mtl"], + elementIds["fbx"], + elementIds["blocks"], gltfBlock, remixBlock); + } + else + { + json = String.Format( + "{0}" + + "\"format\": [ {{ \"format_type\": \"FORMAT_WAVEFRONT_OBJ\", \"root_id\": \"{1}\", {2} \"resource_id\": \"{3}\" }} ]," + + "\"format\": [ {{ \"format_type\": \"FORMAT_WAVEFRONT_OBJ_TRIANGULATED\", \"root_id\": \"{4}\", {5} \"resource_id\": \"{6}\" }} ]," + + "\"format\": [ {{ \"format_type\": \"FORMAT_AUTODESK_FBX\", \"root_id\": \"{7}\" }} ]," + + "\"format\": [ {{ \"format_type\": \"FORMAT_BLOCKS\", \"root_id\": \"{8}\" }} ], {9}, {10} }}", + prelude, + elementIds["obj"], objFormatComplexity, elementIds["mtl"], + elementIds["triangulated-obj"], triangulatedObjFormatComplexity, elementIds["mtl"], + elementIds["fbx"], + elementIds["blocks"], gltfBlock, remixBlock); + } + return json; + } - /// - /// Forms a DELETE request from a HTTP path. - /// - public UnityWebRequest DeleteRequest(string path, string contentType) { - UnityWebRequest request = new UnityWebRequest(path, UnityWebRequest.kHttpVerbDELETE); - request.SetRequestHeader("Content-type", contentType); - if (OAuth2Identity.Instance.HasAccessToken) { - OAuth2Identity.Instance.Authenticate(request); - } - return request; - } + /// + /// Add a resource to the existing asset. + /// + private IEnumerator AddResource(string filename, string mimeType, byte[] data, string key) + { + elementUploadStates.Add(key, UploadState.IN_PROGRESS); + string url = string.Format("{0}/uploads", BaseUrl()); + UnityWebRequest request = new UnityWebRequest(); + + // We wrap in a for loop so we can re-authorise if access tokens have become stale. + for (int i = 0; i < 2; i++) + { + request = PostRequest(url, "multipart/form-data; boundary=" + BOUNDARY, data, compressResourceUpload); + request.SetRequestHeader("X-Google-Project-Override", "apikey"); + request.downloadHandler = new DownloadHandlerBuffer(); + + yield return request.Send(); + + if (request.responseCode == 401 || request.isNetworkError) + { + yield return OAuth2Identity.Instance.Reauthorize(); + continue; + } + else + { + Regex regex = new Regex(ELEMENT_ID_MATCH); + Match match = regex.Match(request.downloadHandler.text); + if (match.Success) + { + elementIds[key] = match.Groups[1].Captures[0].Value; + elementUploadStates[key] = UploadState.SUCCEEDED; + } + else + { + Debug.Log(GetDebugString(request, "Failed to save " + filename + " to Assets Store.")); + elementUploadStates[key] = UploadState.FAILED; + } + yield break; + } + } - /// - /// Forms a POST request from a HTTP path, contentType and the data. - /// - public UnityWebRequest PostRequest(string path, string contentType, byte[] data, bool compressData = false) { - // Create the uploadHandler. - UploadHandler uploader = null; - if (data.Length != 0) { - uploader = new UploadHandlerRaw(compressData ? Deflate(data) : data); - uploader.contentType = contentType; - } - - // Create the request. - UnityWebRequest request = - new UnityWebRequest(path, UnityWebRequest.kHttpVerbPOST, new DownloadHandlerBuffer(), uploader); - request.SetRequestHeader("Content-type", contentType); - if (compressData) { - request.SetRequestHeader("Content-Encoding", "deflate"); - } - if (OAuth2Identity.Instance.HasAccessToken) { - OAuth2Identity.Instance.Authenticate(request); - } - return request; - } + elementUploadStates[key] = UploadState.FAILED; + Debug.Log(GetDebugString(request, "Failed to save " + filename + " to asset store")); + } + + /// + /// Returns a debug string for an upload. + /// + /// + public static string GetDebugString(UnityWebRequest request, string preface) + { + StringBuilder debugString = new StringBuilder(preface).AppendLine() + .Append("Response: ").AppendLine(request.downloadHandler.text) + .Append("Response Code: ").AppendLine(request.responseCode.ToString()) + .Append("Error Message: ").AppendLine(request.error); + + foreach (KeyValuePair header in request.GetResponseHeaders()) + { + debugString.Append(header.Key).Append(" : ").AppendLine(header.Value); + } + return debugString.ToString(); + } + + /// + /// Build the binary multipart content manually, since Unity's multipart stuff is borked. + /// + public byte[] MultiPartContent(string filename, string mimeType, byte[] data) + { + MemoryStream stream = new MemoryStream(); + StreamWriter sw = new StreamWriter(stream); + + // Write the media part of the request from the data. + sw.Write("--" + BOUNDARY); + sw.Write(string.Format( + "\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{0}\"\r\nContent-Type: {1}\r\n\r\n", + filename, mimeType)); + sw.Flush(); + stream.Write(data, 0, data.Length); + sw.Write("\r\n--" + BOUNDARY + "--\r\n"); + sw.Close(); + + return stream.ToArray(); + } - /// - /// Forms a PATCH request from a HTTP path, contentType and the data. - /// - public UnityWebRequest Patch(string path, string contentType, byte[] data) { - // Create the uploadHandler. - UploadHandler uploader = null; - if (data.Length != 0) { - uploader = new UploadHandlerRaw(data); - uploader.contentType = contentType; - } - - // Create the request. - UnityWebRequest request = new UnityWebRequest(path); - request.downloadHandler = new DownloadHandlerBuffer(); - request.method = "PATCH"; - request.uploadHandler = uploader; - request.SetRequestHeader("Content-type", contentType); - if (OAuth2Identity.Instance.HasAccessToken) { - OAuth2Identity.Instance.Authenticate(request); - } - request.downloadHandler = new DownloadHandlerBuffer(); - return request; + /// + /// Compress bytes using deflate. + /// + private byte[] Deflate(byte[] data) + { + Deflater deflater = new Deflater(Deflater.DEFLATED, true); + deflater.SetInput(data); + deflater.Finish(); + + using (var ms = new MemoryStream()) + { + lock (deflateMutex) + { + while (!deflater.IsFinished) + { + var read = deflater.Deflate(tempDeflateBuffer); + ms.Write(tempDeflateBuffer, 0, read); + } + deflater.Reset(); + } + return ms.ToArray(); + } + } + + /// + /// Delete the specified asset. + /// + public IEnumerator DeleteAsset(string assetId) + { + string url = String.Format("{0}/v1/assets/{1}?key={2}", BaseUrl(), assetId, POLY_KEY); + UnityWebRequest request = new UnityWebRequest(); + + // We wrap in a for loop so we can re-authorise if access tokens have become stale. + for (int i = 0; i < 2; i++) + { + request = DeleteRequest(url, "application/json"); + + yield return request.Send(); + + if (request.responseCode == 401 || request.isNetworkError) + { + yield return OAuth2Identity.Instance.Reauthorize(); + continue; + } + else + { + yield break; + } + } + + Debug.Log(GetDebugString(request, "Failed to delete " + assetId)); + } + + /// + /// Forms a GET request from a HTTP path. + /// + public UnityWebRequest GetRequest(string path, string contentType) + { + // The default constructor for a UnityWebRequest gives a GET request. + UnityWebRequest request = new UnityWebRequest(path); + request.SetRequestHeader("Content-type", contentType); + if (OAuth2Identity.Instance.HasAccessToken) + { + OAuth2Identity.Instance.Authenticate(request); + } + return request; + } + + /// + /// Forms a DELETE request from a HTTP path. + /// + public UnityWebRequest DeleteRequest(string path, string contentType) + { + UnityWebRequest request = new UnityWebRequest(path, UnityWebRequest.kHttpVerbDELETE); + request.SetRequestHeader("Content-type", contentType); + if (OAuth2Identity.Instance.HasAccessToken) + { + OAuth2Identity.Instance.Authenticate(request); + } + return request; + } + + /// + /// Forms a POST request from a HTTP path, contentType and the data. + /// + public UnityWebRequest PostRequest(string path, string contentType, byte[] data, bool compressData = false) + { + // Create the uploadHandler. + UploadHandler uploader = null; + if (data.Length != 0) + { + uploader = new UploadHandlerRaw(compressData ? Deflate(data) : data); + uploader.contentType = contentType; + } + + // Create the request. + UnityWebRequest request = + new UnityWebRequest(path, UnityWebRequest.kHttpVerbPOST, new DownloadHandlerBuffer(), uploader); + request.SetRequestHeader("Content-type", contentType); + if (compressData) + { + request.SetRequestHeader("Content-Encoding", "deflate"); + } + if (OAuth2Identity.Instance.HasAccessToken) + { + OAuth2Identity.Instance.Authenticate(request); + } + return request; + } + + /// + /// Forms a PATCH request from a HTTP path, contentType and the data. + /// + public UnityWebRequest Patch(string path, string contentType, byte[] data) + { + // Create the uploadHandler. + UploadHandler uploader = null; + if (data.Length != 0) + { + uploader = new UploadHandlerRaw(data); + uploader.contentType = contentType; + } + + // Create the request. + UnityWebRequest request = new UnityWebRequest(path); + request.downloadHandler = new DownloadHandlerBuffer(); + request.method = "PATCH"; + request.uploadHandler = uploader; + request.SetRequestHeader("Content-type", contentType); + if (OAuth2Identity.Instance.HasAccessToken) + { + OAuth2Identity.Instance.Authenticate(request); + } + request.downloadHandler = new DownloadHandlerBuffer(); + return request; + } } - } } diff --git a/Assets/Scripts/api_clients/objectstore_client/ObjectStoreClient.cs b/Assets/Scripts/api_clients/objectstore_client/ObjectStoreClient.cs index d484d796..94d9600f 100644 --- a/Assets/Scripts/api_clients/objectstore_client/ObjectStoreClient.cs +++ b/Assets/Scripts/api_clients/objectstore_client/ObjectStoreClient.cs @@ -27,327 +27,411 @@ using System; using com.google.apps.peltzer.client.entitlement; -namespace com.google.apps.peltzer.client.api_clients.objectstore_client { - internal class CreateMeshWork : BackgroundWork { - private Dictionary materials; - private string objString; - private bool successfullyReadMesh; - private System.Action> callback; - - private Dictionary> meshes; - - internal CreateMeshWork(Dictionary materials, string obj, - System.Action> callback) { - this.materials = materials; - this.objString = obj; - this.callback = callback; - } +namespace com.google.apps.peltzer.client.api_clients.objectstore_client +{ + internal class CreateMeshWork : BackgroundWork + { + private Dictionary materials; + private string objString; + private bool successfullyReadMesh; + private System.Action> callback; + + private Dictionary> meshes; + + internal CreateMeshWork(Dictionary materials, string obj, + System.Action> callback) + { + this.materials = materials; + this.objString = obj; + this.callback = callback; + } - public void BackgroundWork() { - successfullyReadMesh = ObjImporter.ImportMeshes(objString, materials, out meshes); - } + public void BackgroundWork() + { + successfullyReadMesh = ObjImporter.ImportMeshes(objString, materials, out meshes); + } - public void PostWork() { - if (successfullyReadMesh) { - Dictionary finalizedMeshes = new Dictionary(); - foreach (KeyValuePair> materialAndMesh in meshes) { - foreach (MeshVerticesAndTriangles mesh in materialAndMesh.Value) { - finalizedMeshes.Add(new Material(materialAndMesh.Key), mesh.ToMesh()); - } + public void PostWork() + { + if (successfullyReadMesh) + { + Dictionary finalizedMeshes = new Dictionary(); + foreach (KeyValuePair> materialAndMesh in meshes) + { + foreach (MeshVerticesAndTriangles mesh in materialAndMesh.Value) + { + finalizedMeshes.Add(new Material(materialAndMesh.Key), mesh.ToMesh()); + } + } + callback(finalizedMeshes); + } } - callback(finalizedMeshes); - } } - } - - public class ObjectStoreClient { - public static readonly string OBJECT_STORE_BASE_URL = "[Removed]"; - public ObjectStoreClient() { } + public class ObjectStoreClient + { + public static readonly string OBJECT_STORE_BASE_URL = "[Removed]"; - // Create a url string for making web requests. - public StringBuilder GetObjectStoreURL(StringBuilder tag) { - StringBuilder url = new StringBuilder(OBJECT_STORE_BASE_URL).Append("/s"); + public ObjectStoreClient() { } - if (tag != null) { - url.Append("?q=" + tag); - } + // Create a url string for making web requests. + public StringBuilder GetObjectStoreURL(StringBuilder tag) + { + StringBuilder url = new StringBuilder(OBJECT_STORE_BASE_URL).Append("/s"); - return url; - } + if (tag != null) + { + url.Append("?q=" + tag); + } - // Makes a query to the ObjectStore for objects with a given tag. - public IEnumerator GetObjectStoreListingsForTag(string tag, System.Action callback) { - string url = OBJECT_STORE_BASE_URL + "/s"; - if (tag != null) { - url += "?q=" + tag; - } - return GetObjectStoreListings(GetNewGetRequest(new StringBuilder(url), "text/json"), callback); - } + return url; + } - // Makes a query to the ObjectStore for objects made by a user. - public IEnumerator GetObjectStoreListingsForUser(string userId, System.Action callback) { - string url = OBJECT_STORE_BASE_URL + "/s"; - if (userId != null) { - url += "?q=userId=" + userId; - } - return GetObjectStoreListings(GetNewGetRequest(new StringBuilder(url), "text/json"), callback); - } + // Makes a query to the ObjectStore for objects with a given tag. + public IEnumerator GetObjectStoreListingsForTag(string tag, System.Action callback) + { + string url = OBJECT_STORE_BASE_URL + "/s"; + if (tag != null) + { + url += "?q=" + tag; + } + return GetObjectStoreListings(GetNewGetRequest(new StringBuilder(url), "text/json"), callback); + } - // Makes a query to the ObjectStore for a given UnityWeb search request. - public IEnumerator GetObjectStoreListings(UnityWebRequest searchRequest, - System.Action callback) { - using (searchRequest) { - yield return searchRequest.Send(); - if (!searchRequest.isNetworkError) { - callback(JsonUtility.FromJson(searchRequest.downloadHandler.text)); + // Makes a query to the ObjectStore for objects made by a user. + public IEnumerator GetObjectStoreListingsForUser(string userId, System.Action callback) + { + string url = OBJECT_STORE_BASE_URL + "/s"; + if (userId != null) + { + url += "?q=userId=" + userId; + } + return GetObjectStoreListings(GetNewGetRequest(new StringBuilder(url), "text/json"), callback); } - } - } - // Given the entry metadata for an object queries the actual object from the ObjectStore. - public IEnumerator GetObject(ObjectStoreEntry entry, System.Action> callback) { - // First, check and see if there's a zip file, because it will load a lot faster. - if (entry.assets.object_package != null - && !string.IsNullOrEmpty(entry.assets.object_package.rootUrl) - && !string.IsNullOrEmpty(entry.assets.object_package.baseFile)) { - StringBuilder zipUrl = new StringBuilder( - OBJECT_STORE_BASE_URL).Append(entry.assets.object_package.rootUrl) - .Append(entry.assets.object_package.baseFile); - using (UnityWebRequest fetchRequest = GetNewGetRequest(zipUrl, "text/plain")) { - yield return fetchRequest.Send(); - if (!fetchRequest.isNetworkError) { - PeltzerMain.Instance.DoPolyMenuBackgroundWork(new CreateMeshFromStreamWork(fetchRequest.downloadHandler, callback)); - } + // Makes a query to the ObjectStore for a given UnityWeb search request. + public IEnumerator GetObjectStoreListings(UnityWebRequest searchRequest, + System.Action callback) + { + using (searchRequest) + { + yield return searchRequest.Send(); + if (!searchRequest.isNetworkError) + { + callback(JsonUtility.FromJson(searchRequest.downloadHandler.text)); + } + } } - } else { - StringBuilder url = - new StringBuilder(OBJECT_STORE_BASE_URL).Append(entry.assets.obj.rootUrl).Append(entry.assets.obj.baseFile); - using (UnityWebRequest fetchRequest = GetNewGetRequest(url, "text/plain")) { - yield return fetchRequest.Send(); - if (!fetchRequest.isNetworkError) { - } else { - if (entry.assets.obj.supportingFiles != null && entry.assets.obj.supportingFiles.Length > 0) { - using (UnityWebRequest materialFetch = - GetNewGetRequest(new StringBuilder(OBJECT_STORE_BASE_URL).Append(entry.assets.obj.rootUrl).Append( - entry.assets.obj.supportingFiles[0]), "text/plain")) { - yield return materialFetch.Send(); - if (!materialFetch.isNetworkError) { - PeltzerMain.Instance.DoPolyMenuBackgroundWork(new CreateMeshWork(ObjImporter.ImportMaterials( - materialFetch.downloadHandler.text), fetchRequest.downloadHandler.text, callback)); + + // Given the entry metadata for an object queries the actual object from the ObjectStore. + public IEnumerator GetObject(ObjectStoreEntry entry, System.Action> callback) + { + // First, check and see if there's a zip file, because it will load a lot faster. + if (entry.assets.object_package != null + && !string.IsNullOrEmpty(entry.assets.object_package.rootUrl) + && !string.IsNullOrEmpty(entry.assets.object_package.baseFile)) + { + StringBuilder zipUrl = new StringBuilder( + OBJECT_STORE_BASE_URL).Append(entry.assets.object_package.rootUrl) + .Append(entry.assets.object_package.baseFile); + using (UnityWebRequest fetchRequest = GetNewGetRequest(zipUrl, "text/plain")) + { + yield return fetchRequest.Send(); + if (!fetchRequest.isNetworkError) + { + PeltzerMain.Instance.DoPolyMenuBackgroundWork(new CreateMeshFromStreamWork(fetchRequest.downloadHandler, callback)); + } + } + } + else + { + StringBuilder url = + new StringBuilder(OBJECT_STORE_BASE_URL).Append(entry.assets.obj.rootUrl).Append(entry.assets.obj.baseFile); + using (UnityWebRequest fetchRequest = GetNewGetRequest(url, "text/plain")) + { + yield return fetchRequest.Send(); + if (!fetchRequest.isNetworkError) + { + } + else + { + if (entry.assets.obj.supportingFiles != null && entry.assets.obj.supportingFiles.Length > 0) + { + using (UnityWebRequest materialFetch = + GetNewGetRequest(new StringBuilder(OBJECT_STORE_BASE_URL).Append(entry.assets.obj.rootUrl).Append( + entry.assets.obj.supportingFiles[0]), "text/plain")) + { + yield return materialFetch.Send(); + if (!materialFetch.isNetworkError) + { + PeltzerMain.Instance.DoPolyMenuBackgroundWork(new CreateMeshWork(ObjImporter.ImportMaterials( + materialFetch.downloadHandler.text), fetchRequest.downloadHandler.text, callback)); + } + } + } + } } - } } - } } - } - } - /// - /// Downloads the raw file data for an object. This method was originally designed for the Object Store - /// (predecessor of Zandria) but is actually agnostic to the underlying service, as it just pulls data - /// from a URL, so we use it from ZandriaCreationsManager. - /// - /// The entry for which to load the raw data. - /// The callback to call when loading is complete. - public static void GetRawFileData(ObjectStoreEntry entry, System.Action callback) { - if (entry.localPeltzerFile != null) { - callback(File.ReadAllBytes(entry.localPeltzerFile)); - } else if (entry.assets.peltzer_package != null + /// + /// Downloads the raw file data for an object. This method was originally designed for the Object Store + /// (predecessor of Zandria) but is actually agnostic to the underlying service, as it just pulls data + /// from a URL, so we use it from ZandriaCreationsManager. + /// + /// The entry for which to load the raw data. + /// The callback to call when loading is complete. + public static void GetRawFileData(ObjectStoreEntry entry, System.Action callback) + { + if (entry.localPeltzerFile != null) + { + callback(File.ReadAllBytes(entry.localPeltzerFile)); + } + else if (entry.assets.peltzer_package != null + && !string.IsNullOrEmpty(entry.assets.peltzer_package.rootUrl) + && !string.IsNullOrEmpty(entry.assets.peltzer_package.baseFile)) + { + StringBuilder zipUrl = new StringBuilder(entry.assets.peltzer_package.rootUrl) + .Append(entry.assets.peltzer_package.baseFile); + + PeltzerMain.Instance.webRequestManager.EnqueueRequest( + () => { return GetNewGetRequest(zipUrl, "text/plain"); }, + (bool success, int responseCode, byte[] responseBytes) => + { + if (!success) + { + callback(null); + } + else + { + PeltzerMain.Instance.DoPolyMenuBackgroundWork(new CopyStreamWork(responseBytes, callback)); + } + }); + } + else + { + StringBuilder url = new StringBuilder(entry.assets.peltzer.rootUrl) + .Append(entry.assets.peltzer.baseFile); + + PeltzerMain.Instance.webRequestManager.EnqueueRequest( + () => { return GetNewGetRequest(url, "text/plain"); }, + (bool success, int responseCode, byte[] responseBytes) => + { + if (!success) + { + callback(null); + } + else + { + callback(responseBytes); + } + }); + } + } + + // Queries the ObjectStore for an object given its entry metadata and parses it into a PeltzerFile. + public IEnumerator GetPeltzerFile(ObjectStoreEntry entry, System.Action callback) + { + if (entry.assets.peltzer_package != null && !string.IsNullOrEmpty(entry.assets.peltzer_package.rootUrl) - && !string.IsNullOrEmpty(entry.assets.peltzer_package.baseFile)) { - StringBuilder zipUrl = new StringBuilder(entry.assets.peltzer_package.rootUrl) - .Append(entry.assets.peltzer_package.baseFile); - - PeltzerMain.Instance.webRequestManager.EnqueueRequest( - () => { return GetNewGetRequest(zipUrl, "text/plain"); }, - (bool success, int responseCode, byte[] responseBytes) => { - if (!success) { - callback(null); - } else { - PeltzerMain.Instance.DoPolyMenuBackgroundWork(new CopyStreamWork(responseBytes, callback)); + && !string.IsNullOrEmpty(entry.assets.peltzer_package.baseFile)) + { + StringBuilder zipUrl = new StringBuilder(OBJECT_STORE_BASE_URL).Append(entry.assets.peltzer_package.rootUrl) + .Append(entry.assets.peltzer_package.baseFile); + using (UnityWebRequest fetchRequest = GetNewGetRequest(zipUrl, "text/plain")) + { + yield return fetchRequest.Send(); + if (!fetchRequest.isNetworkError) + { + PeltzerMain.Instance.DoPolyMenuBackgroundWork(new CopyStreamWork(fetchRequest.downloadHandler.data, /* byteCallback */ null, callback)); + } + } } - }); - } else { - StringBuilder url = new StringBuilder(entry.assets.peltzer.rootUrl) - .Append(entry.assets.peltzer.baseFile); - - PeltzerMain.Instance.webRequestManager.EnqueueRequest( - () => { return GetNewGetRequest(url, "text/plain"); }, - (bool success, int responseCode, byte[] responseBytes) => { - if (!success) { - callback(null); - } else { - callback(responseBytes); + else + { + StringBuilder url = new StringBuilder(OBJECT_STORE_BASE_URL).Append(entry.assets.peltzer.rootUrl) + .Append(entry.assets.peltzer.baseFile); + using (UnityWebRequest fetchRequest = GetNewGetRequest(url, "text/plain")) + { + yield return fetchRequest.Send(); + if (!fetchRequest.isNetworkError) + { + PeltzerFile peltzerFile; + bool validFile = + PeltzerFileHandler.PeltzerFileFromBytes(fetchRequest.downloadHandler.data, out peltzerFile); + + if (validFile) + { + callback(peltzerFile); + } + } + } } - }); - } - } + } - // Queries the ObjectStore for an object given its entry metadata and parses it into a PeltzerFile. - public IEnumerator GetPeltzerFile(ObjectStoreEntry entry, System.Action callback) { - if (entry.assets.peltzer_package != null - && !string.IsNullOrEmpty(entry.assets.peltzer_package.rootUrl) - && !string.IsNullOrEmpty(entry.assets.peltzer_package.baseFile)) { - StringBuilder zipUrl = new StringBuilder(OBJECT_STORE_BASE_URL).Append(entry.assets.peltzer_package.rootUrl) - .Append(entry.assets.peltzer_package.baseFile); - using (UnityWebRequest fetchRequest = GetNewGetRequest(zipUrl, "text/plain")) { - yield return fetchRequest.Send(); - if (!fetchRequest.isNetworkError) { - PeltzerMain.Instance.DoPolyMenuBackgroundWork(new CopyStreamWork(fetchRequest.downloadHandler.data, /* byteCallback */ null, callback)); - } + // Sets properties for a UnityWebRequest. + public IEnumerator SetListingProperties(string id, string title, string author, string description) + { + string url = OBJECT_STORE_BASE_URL + "/m/" + id + "?"; + if (!string.IsNullOrEmpty(title)) + { + url += "title=" + title + "&"; + } + if (!string.IsNullOrEmpty(author)) + { + url += "author=" + author + "&"; + } + if (!string.IsNullOrEmpty(description)) + { + url += "description=" + description; + } + UnityWebRequest request = new UnityWebRequest(url); + request.method = UnityWebRequest.kHttpVerbPOST; + request.SetRequestHeader("Content-Type", "text/plain"); + request.SetRequestHeader("Token", "[Removed]"); + using (UnityWebRequest propRequest = request) + { + yield return propRequest.Send(); + } } - } else { - StringBuilder url = new StringBuilder(OBJECT_STORE_BASE_URL).Append(entry.assets.peltzer.rootUrl) - .Append(entry.assets.peltzer.baseFile); - using (UnityWebRequest fetchRequest = GetNewGetRequest(url, "text/plain")) { - yield return fetchRequest.Send(); - if (!fetchRequest.isNetworkError) { - PeltzerFile peltzerFile; - bool validFile = - PeltzerFileHandler.PeltzerFileFromBytes(fetchRequest.downloadHandler.data, out peltzerFile); - - if (validFile) { - callback(peltzerFile); + + // Helps create a UnityWebRequest from a given url and contentType. + public static UnityWebRequest GetNewGetRequest(StringBuilder url, string contentType) + { + UnityWebRequest request = new UnityWebRequest(url.ToString()); + request.method = UnityWebRequest.kHttpVerbGET; + request.SetRequestHeader("Content-Type", contentType); + request.SetRequestHeader("Token", "[Removed]"); + request.downloadHandler = new DownloadHandlerBuffer(); + + if (OAuth2Identity.Instance.HasAccessToken) + { + OAuth2Identity.Instance.Authenticate(request); } - } + return request; } - } - } - // Sets properties for a UnityWebRequest. - public IEnumerator SetListingProperties(string id, string title, string author, string description) { - string url = OBJECT_STORE_BASE_URL + "/m/" + id + "?"; - if (!string.IsNullOrEmpty(title)) { - url += "title=" + title + "&"; - } - if (!string.IsNullOrEmpty(author)) { - url += "author=" + author + "&"; - } - if (!string.IsNullOrEmpty(description)) { - url += "description=" + description; - } - UnityWebRequest request = new UnityWebRequest(url); - request.method = UnityWebRequest.kHttpVerbPOST; - request.SetRequestHeader("Content-Type", "text/plain"); - request.SetRequestHeader("Token", "[Removed]"); - using (UnityWebRequest propRequest = request) { - yield return propRequest.Send(); - } - } + public static void CopyStream(Stream input, Stream output) + { + byte[] buffer = new byte[32768]; + int read; + while ((read = input.Read(buffer, 0, buffer.Length)) > 0) + { + output.Write(buffer, 0, read); + } + } - // Helps create a UnityWebRequest from a given url and contentType. - public static UnityWebRequest GetNewGetRequest(StringBuilder url, string contentType) { - UnityWebRequest request = new UnityWebRequest(url.ToString()); - request.method = UnityWebRequest.kHttpVerbGET; - request.SetRequestHeader("Content-Type", contentType); - request.SetRequestHeader("Token", "[Removed]"); - request.downloadHandler = new DownloadHandlerBuffer(); - - if (OAuth2Identity.Instance.HasAccessToken) { - OAuth2Identity.Instance.Authenticate(request); - } - return request; - } + /// + /// BackgroundWork for copying a stream (a zip-file containing a .peltzer/poly file) into memory + /// and then sending a callback. + /// + public class CopyStreamWork : BackgroundWork + { + // Optional callbacks. + private readonly System.Action byteCallback; + private readonly System.Action peltzerFileCallback; + + private byte[] inputBytes; + private MemoryStream outputStream; + private byte[] outputBytes; + + public CopyStreamWork(byte[] inputBytes, + System.Action byteCallback = null, System.Action peltzerFileCallback = null) + { + this.inputBytes = inputBytes; + this.byteCallback = byteCallback; + this.peltzerFileCallback = peltzerFileCallback; + + outputStream = new MemoryStream(); + } - public static void CopyStream(Stream input, Stream output) { - byte[] buffer = new byte[32768]; - int read; - while ((read = input.Read(buffer, 0, buffer.Length)) > 0) { - output.Write(buffer, 0, read); - } - } + public void BackgroundWork() + { + using (ZipFile zipFile = new ZipFile(new MemoryStream(inputBytes))) + { + foreach (ZipEntry zipEntry in zipFile) + { + if (zipEntry.Name.EndsWith(".peltzer") || zipEntry.Name.EndsWith(".poly") + || zipEntry.Name.EndsWith(".blocks")) + { + CopyStream(zipFile.GetInputStream(zipEntry), outputStream); + outputBytes = outputStream.ToArray(); + break; + } + } + } + } - /// - /// BackgroundWork for copying a stream (a zip-file containing a .peltzer/poly file) into memory - /// and then sending a callback. - /// - public class CopyStreamWork : BackgroundWork { - // Optional callbacks. - private readonly System.Action byteCallback; - private readonly System.Action peltzerFileCallback; - - private byte[] inputBytes; - private MemoryStream outputStream; - private byte[] outputBytes; - - public CopyStreamWork(byte[] inputBytes, - System.Action byteCallback = null, System.Action peltzerFileCallback = null) { - this.inputBytes = inputBytes; - this.byteCallback = byteCallback; - this.peltzerFileCallback = peltzerFileCallback; - - outputStream = new MemoryStream(); - } - - public void BackgroundWork() { - using (ZipFile zipFile = new ZipFile(new MemoryStream(inputBytes))) { - foreach (ZipEntry zipEntry in zipFile) { - if (zipEntry.Name.EndsWith(".peltzer") || zipEntry.Name.EndsWith(".poly") - || zipEntry.Name.EndsWith(".blocks")) { - CopyStream(zipFile.GetInputStream(zipEntry), outputStream); - outputBytes = outputStream.ToArray(); - break; + public void PostWork() + { + if (byteCallback != null) + { + byteCallback(outputBytes); + } + if (peltzerFileCallback != null) + { + PeltzerFile peltzerFile; + bool validFile = PeltzerFileHandler.PeltzerFileFromBytes(outputBytes, out peltzerFile); + + if (validFile) + { + peltzerFileCallback(peltzerFile); + } + } } - } } - } - public void PostWork() { - if (byteCallback != null) { - byteCallback(outputBytes); - } - if (peltzerFileCallback != null) { - PeltzerFile peltzerFile; - bool validFile = PeltzerFileHandler.PeltzerFileFromBytes(outputBytes, out peltzerFile); + /// + /// BackgroundWork for copying a stream (a zip-file containing a .obj file and a .mtl file) into memory + /// and then creating a mesh and sending a callback. + /// + public class CreateMeshFromStreamWork : BackgroundWork + { + DownloadHandler downloadHandler; + string objFile; + string mtlFile; + System.Action> callback; + + public CreateMeshFromStreamWork(DownloadHandler downloadHandler, System.Action> callback) + { + this.downloadHandler = downloadHandler; + this.callback = callback; + } - if (validFile) { - peltzerFileCallback(peltzerFile); - } - } - } - } + public void BackgroundWork() + { + // Go through our zip file entries and find the obj and mtl. + byte[] zippedData = downloadHandler.data; + using (ZipFile zipFile = new ZipFile(new MemoryStream(zippedData))) + { + foreach (ZipEntry zipEntry in zipFile) + { + if (zipEntry.Name.EndsWith(".obj")) + { + using (MemoryStream unzippedData = new MemoryStream()) + { + CopyStream(zipFile.GetInputStream(zipEntry), unzippedData); + objFile = System.Text.Encoding.Default.GetString(unzippedData.ToArray()); + } + } + else if (zipEntry.Name.EndsWith(".mtl")) + { + using (MemoryStream unzippedData = new MemoryStream()) + { + CopyStream(zipFile.GetInputStream(zipEntry), unzippedData); + mtlFile = System.Text.Encoding.Default.GetString(unzippedData.ToArray()); + } + } + } + } + } - /// - /// BackgroundWork for copying a stream (a zip-file containing a .obj file and a .mtl file) into memory - /// and then creating a mesh and sending a callback. - /// - public class CreateMeshFromStreamWork : BackgroundWork { - DownloadHandler downloadHandler; - string objFile; - string mtlFile; - System.Action> callback; - - public CreateMeshFromStreamWork(DownloadHandler downloadHandler, System.Action> callback) { - this.downloadHandler = downloadHandler; - this.callback = callback; - } - - public void BackgroundWork() { - // Go through our zip file entries and find the obj and mtl. - byte[] zippedData = downloadHandler.data; - using (ZipFile zipFile = new ZipFile(new MemoryStream(zippedData))) { - foreach (ZipEntry zipEntry in zipFile) { - if (zipEntry.Name.EndsWith(".obj")) { - using (MemoryStream unzippedData = new MemoryStream()) { - CopyStream(zipFile.GetInputStream(zipEntry), unzippedData); - objFile = System.Text.Encoding.Default.GetString(unzippedData.ToArray()); - } - } else if (zipEntry.Name.EndsWith(".mtl")) { - using (MemoryStream unzippedData = new MemoryStream()) { - CopyStream(zipFile.GetInputStream(zipEntry), unzippedData); - mtlFile = System.Text.Encoding.Default.GetString(unzippedData.ToArray()); - } + public void PostWork() + { + // Create meshes + PeltzerMain.Instance.DoPolyMenuBackgroundWork(new CreateMeshWork(ObjImporter.ImportMaterials(mtlFile), + objFile, callback)); } - } } - } - - public void PostWork() { - // Create meshes - PeltzerMain.Instance.DoPolyMenuBackgroundWork(new CreateMeshWork(ObjImporter.ImportMaterials(mtlFile), - objFile, callback)); - } } - } } diff --git a/Assets/Scripts/api_clients/objectstore_client/ObjectStoreEntry.cs b/Assets/Scripts/api_clients/objectstore_client/ObjectStoreEntry.cs index c655941e..810c0ff8 100644 --- a/Assets/Scripts/api_clients/objectstore_client/ObjectStoreEntry.cs +++ b/Assets/Scripts/api_clients/objectstore_client/ObjectStoreEntry.cs @@ -24,22 +24,24 @@ /// was obtained from. This is why you might see in the code that ObjectStoreEntry (and related classes) are /// used for Zandria loading code. /// -namespace com.google.apps.peltzer.client.api_clients.objectstore_client { - [Serializable] - public class ObjectStoreEntry { - public string id; // This is the 'asset id', we can't rename it due to a dependency in mogwai-objectstore, - public string localId; - public string[] tags; - public ObjectStoreObjectAssetsWrapper assets; - public bool isPrivateAsset; - public string thumbnail; - public string author; - public string title; - public string description; - public string webViewConfig; - public DateTime createdDate; - public string localThumbnailFile; - public string localPeltzerFile; - public Vector3 cameraForward; - } +namespace com.google.apps.peltzer.client.api_clients.objectstore_client +{ + [Serializable] + public class ObjectStoreEntry + { + public string id; // This is the 'asset id', we can't rename it due to a dependency in mogwai-objectstore, + public string localId; + public string[] tags; + public ObjectStoreObjectAssetsWrapper assets; + public bool isPrivateAsset; + public string thumbnail; + public string author; + public string title; + public string description; + public string webViewConfig; + public DateTime createdDate; + public string localThumbnailFile; + public string localPeltzerFile; + public Vector3 cameraForward; + } } \ No newline at end of file diff --git a/Assets/Scripts/api_clients/objectstore_client/ObjectStoreObjectAssets.cs b/Assets/Scripts/api_clients/objectstore_client/ObjectStoreObjectAssets.cs index c4a80a50..ef75ddc4 100644 --- a/Assets/Scripts/api_clients/objectstore_client/ObjectStoreObjectAssets.cs +++ b/Assets/Scripts/api_clients/objectstore_client/ObjectStoreObjectAssets.cs @@ -14,41 +14,47 @@ using System; -namespace com.google.apps.peltzer.client.api_clients.objectstore_client { +namespace com.google.apps.peltzer.client.api_clients.objectstore_client +{ - [Serializable] - public class ObjectStoreObjectAssets { - public string rootUrl; - public string[] supportingFiles; - public string baseFile; - } + [Serializable] + public class ObjectStoreObjectAssets + { + public string rootUrl; + public string[] supportingFiles; + public string baseFile; + } - [Serializable] - public class ObjectStorePeltzerAssets { - public string rootUrl; - public string[] supportingFiles; - public string baseFile; - } + [Serializable] + public class ObjectStorePeltzerAssets + { + public string rootUrl; + public string[] supportingFiles; + public string baseFile; + } - [Serializable] - public class ObjectStorePeltzerPackageAssets { - public string rootUrl; - public string[] supportingFiles; - public string baseFile; - } + [Serializable] + public class ObjectStorePeltzerPackageAssets + { + public string rootUrl; + public string[] supportingFiles; + public string baseFile; + } - [Serializable] - public class ObjectStoreObjMtlPackageAssets { - public string rootUrl; - public string[] supportingFiles; - public string baseFile; - } + [Serializable] + public class ObjectStoreObjMtlPackageAssets + { + public string rootUrl; + public string[] supportingFiles; + public string baseFile; + } - [Serializable] - public class ObjectStoreObjectAssetsWrapper { - public ObjectStoreObjectAssets obj; - public ObjectStorePeltzerAssets peltzer; - public ObjectStorePeltzerPackageAssets peltzer_package; - public ObjectStoreObjMtlPackageAssets object_package; - } + [Serializable] + public class ObjectStoreObjectAssetsWrapper + { + public ObjectStoreObjectAssets obj; + public ObjectStorePeltzerAssets peltzer; + public ObjectStorePeltzerPackageAssets peltzer_package; + public ObjectStoreObjMtlPackageAssets object_package; + } } \ No newline at end of file diff --git a/Assets/Scripts/api_clients/objectstore_client/ObjectStoreSearchResult.cs b/Assets/Scripts/api_clients/objectstore_client/ObjectStoreSearchResult.cs index b435abba..0a8b654e 100644 --- a/Assets/Scripts/api_clients/objectstore_client/ObjectStoreSearchResult.cs +++ b/Assets/Scripts/api_clients/objectstore_client/ObjectStoreSearchResult.cs @@ -16,9 +16,11 @@ using System.Collections; using System; -namespace com.google.apps.peltzer.client.api_clients.objectstore_client { - [Serializable] - public class ObjectStoreSearchResult { - public ObjectStoreEntry[] results; - } +namespace com.google.apps.peltzer.client.api_clients.objectstore_client +{ + [Serializable] + public class ObjectStoreSearchResult + { + public ObjectStoreEntry[] results; + } } diff --git a/Assets/Scripts/app/Config.cs b/Assets/Scripts/app/Config.cs index 4312ee7c..51166ee0 100644 --- a/Assets/Scripts/app/Config.cs +++ b/Assets/Scripts/app/Config.cs @@ -20,141 +20,168 @@ /// /// Holds app-level configuration information. /// -namespace com.google.apps.peltzer.client.app { - public enum VrHardware { - Unset, - None, - Rift, - Vive, - } - - public enum SdkMode { - Unset = -1, - Oculus = 0, - SteamVR, - } - - public class Config : MonoBehaviour { - private static Config instance; - public static Config Instance { - get { - if (instance == null) { - instance = GameObject.FindObjectOfType(); - Debug.Assert(instance != null, "No Config object found in scene!"); - } - return instance; - } +namespace com.google.apps.peltzer.client.app +{ + public enum VrHardware + { + Unset, + None, + Rift, + Vive, } - public OculusHandTrackingManager oculusHandTrackingManager; - - public string appName = "[Removed]"; - // The SDK being used -- Oculus or Steam. Set from the Editor. - public SdkMode sdkMode; - // The current version ID -- 'debug' or something more meaningful. Set from the Editor. - public string version = "debug"; - - [SerializeField] private GameObject cameraRigGameObject; - [SerializeField] private GameObject controllerLeftGameObject; - [SerializeField] private GameObject controllerRightGameObject; - - // The hardware being used -- Vive or Rift. Detected at runtime. - private VrHardware vrHardware; - - // Find or fetch the hardware being used. - public VrHardware VrHardware { - // This is set lazily the first time VrHardware is accesssed. - get { - if (vrHardware == VrHardware.Unset) { - if (sdkMode == SdkMode.Oculus) { - vrHardware = VrHardware.Rift; - } -#if STEAMVRBUILD - else if (sdkMode == SdkMode.SteamVR) { - // If SteamVR fails for some reason we will discover it here. - try { - if (Valve.VR.OpenVR.System == null) { - vrHardware = VrHardware.None; - return vrHardware; - } - } catch (Exception) { - vrHardware = VrHardware.None; - return vrHardware; - } + public enum SdkMode + { + Unset = -1, + Oculus = 0, + SteamVR, + } - // RiftUsedInSteamVr relies on headset detection, so controllers don't have to be on. - if (RiftUsedInSteamVr()) { - vrHardware = VrHardware.Rift; - } else { - vrHardware = VrHardware.Vive; + public class Config : MonoBehaviour + { + private static Config instance; + public static Config Instance + { + get + { + if (instance == null) + { + instance = GameObject.FindObjectOfType(); + Debug.Assert(instance != null, "No Config object found in scene!"); + } + return instance; } - } -#endif - else { - vrHardware = VrHardware.None; - } } - return vrHardware; - } - } + public OculusHandTrackingManager oculusHandTrackingManager; + + public string appName = "[Removed]"; + // The SDK being used -- Oculus or Steam. Set from the Editor. + public SdkMode sdkMode; + // The current version ID -- 'debug' or something more meaningful. Set from the Editor. + public string version = "debug"; + + [SerializeField] private GameObject cameraRigGameObject; + [SerializeField] private GameObject controllerLeftGameObject; + [SerializeField] private GameObject controllerRightGameObject; + + // The hardware being used -- Vive or Rift. Detected at runtime. + private VrHardware vrHardware; + + // Find or fetch the hardware being used. + public VrHardware VrHardware + { + // This is set lazily the first time VrHardware is accesssed. + get + { + if (vrHardware == VrHardware.Unset) + { + if (sdkMode == SdkMode.Oculus) + { + vrHardware = VrHardware.Rift; + } +#if STEAMVRBUILD + else if (sdkMode == SdkMode.SteamVR) + { + // If SteamVR fails for some reason we will discover it here. + try + { + if (Valve.VR.OpenVR.System == null) + { + vrHardware = VrHardware.None; + return vrHardware; + } + } + catch (Exception) + { + vrHardware = VrHardware.None; + return vrHardware; + } + + // RiftUsedInSteamVr relies on headset detection, so controllers don't have to be on. + if (RiftUsedInSteamVr()) + { + vrHardware = VrHardware.Rift; + } + else + { + vrHardware = VrHardware.Vive; + } + } +#endif + else + { + vrHardware = VrHardware.None; + } + } - void Start() { - instance = this; - if (sdkMode == SdkMode.Oculus) { - oculusHandTrackingManager = cameraRigGameObject.AddComponent(); - oculusHandTrackingManager.leftTransform = controllerLeftGameObject.transform; - oculusHandTrackingManager.rightTransform = controllerRightGameObject.transform; - } else if (sdkMode == SdkMode.SteamVR) { + return vrHardware; + } + } + + void Start() + { + instance = this; + if (sdkMode == SdkMode.Oculus) + { + oculusHandTrackingManager = cameraRigGameObject.AddComponent(); + oculusHandTrackingManager.leftTransform = controllerLeftGameObject.transform; + oculusHandTrackingManager.rightTransform = controllerRightGameObject.transform; + } + else if (sdkMode == SdkMode.SteamVR) + { #if STEAMVRBUILD - var controllerLeftTracking = controllerLeftGameObject.AddComponent(); - controllerLeftTracking.SetDeviceIndex(1); - var controllerRightTracking = controllerRightGameObject.AddComponent(); - controllerRightTracking.SetDeviceIndex(2); - var manager = cameraRigGameObject.AddComponent(); - manager.left = controllerLeftGameObject; - manager.right = controllerRightGameObject; - manager.UpdateTargets(); + var controllerLeftTracking = controllerLeftGameObject.AddComponent(); + controllerLeftTracking.SetDeviceIndex(1); + var controllerRightTracking = controllerRightGameObject.AddComponent(); + controllerRightTracking.SetDeviceIndex(2); + var manager = cameraRigGameObject.AddComponent(); + manager.left = controllerLeftGameObject; + manager.right = controllerRightGameObject; + manager.UpdateTargets(); #endif - } - } + } + } #if STEAMVRBUILD - // Check if the Rift hardware is being used in SteamVR. - private bool RiftUsedInSteamVr() { - Valve.VR.CVRSystem system = Valve.VR.OpenVR.System; - // If system == null, then somehow, the SteamVR SDK was not properly loaded in. - Debug.Assert(system != null, "OpenVR System not found, check \"Virtual Reality Supported\""); - - // Index 0 is always the HMD. - return CheckRiftTrackedInSteamVr(0); - } + // Check if the Rift hardware is being used in SteamVR. + private bool RiftUsedInSteamVr() + { + Valve.VR.CVRSystem system = Valve.VR.OpenVR.System; + // If system == null, then somehow, the SteamVR SDK was not properly loaded in. + Debug.Assert(system != null, "OpenVR System not found, check \"Virtual Reality Supported\""); + + // Index 0 is always the HMD. + return CheckRiftTrackedInSteamVr(0); + } - // Check if the tracked object is Rift hardware. - private bool CheckRiftTrackedInSteamVr(uint index) { - var system = Valve.VR.OpenVR.System; - var error = Valve.VR.ETrackedPropertyError.TrackedProp_Success; - - var capacity = system.GetStringTrackedDeviceProperty( - index, - Valve.VR.ETrackedDeviceProperty.Prop_ManufacturerName_String, - null, - 0, - ref error); - System.Text.StringBuilder buffer = new System.Text.StringBuilder((int)capacity); - system.GetStringTrackedDeviceProperty( - index, - Valve.VR.ETrackedDeviceProperty.Prop_ManufacturerName_String, - buffer, - capacity, - ref error); - string s = buffer.ToString(); - - if (s.Contains("Oculus")) { - return true; - } - return false; - } + // Check if the tracked object is Rift hardware. + private bool CheckRiftTrackedInSteamVr(uint index) + { + var system = Valve.VR.OpenVR.System; + var error = Valve.VR.ETrackedPropertyError.TrackedProp_Success; + + var capacity = system.GetStringTrackedDeviceProperty( + index, + Valve.VR.ETrackedDeviceProperty.Prop_ManufacturerName_String, + null, + 0, + ref error); + System.Text.StringBuilder buffer = new System.Text.StringBuilder((int)capacity); + system.GetStringTrackedDeviceProperty( + index, + Valve.VR.ETrackedDeviceProperty.Prop_ManufacturerName_String, + buffer, + capacity, + ref error); + string s = buffer.ToString(); + + if (s.Contains("Oculus")) + { + return true; + } + return false; + } #endif #if UNITY_EDITOR @@ -184,5 +211,5 @@ public void OnValidate() { } } #endif - } + } } diff --git a/Assets/Scripts/audio/AudioLibrary.cs b/Assets/Scripts/audio/AudioLibrary.cs index 554feccd..a08dcbca 100644 --- a/Assets/Scripts/audio/AudioLibrary.cs +++ b/Assets/Scripts/audio/AudioLibrary.cs @@ -19,244 +19,263 @@ /// /// AudioSource to fade out. /// -class Fade { - public AudioSource source; - public float startTime; - public float startVolume; - public float duration; +class Fade +{ + public AudioSource source; + public float startTime; + public float startVolume; + public float duration; - public Fade(AudioSource source, float startTime, float startVolume, float duration) { - this.source = source; - this.startTime = startTime; - this.startVolume = startVolume; - this.duration = duration; - } + public Fade(AudioSource source, float startTime, float startVolume, float duration) + { + this.source = source; + this.startTime = startTime; + this.startVolume = startVolume; + this.duration = duration; + } } /// /// A library of sounds and API to play them. /// Generates a new AudioSource for each play request, and periodically garbage collects. /// -public class AudioLibrary : MonoBehaviour { - // Named audio clips, each of which must be loaded in Start(). - public AudioClip alignSound; - public AudioClip breakSound; - public AudioClip confettiSound; - public AudioClip copySound; - public AudioClip decrementSound; - public AudioClip deleteSound; - public AudioClip errorSound; - public AudioClip genericSelectSound; - public AudioClip genericReleaseSound; - public AudioClip grabMeshSound; - public AudioClip grabMeshPartSound; - public AudioClip groupSound; - public AudioClip incrementSound; - public AudioClip insertVolumeSound; - public AudioClip menuSelectSound; - public AudioClip modifyMeshSound; - public AudioClip paintSound; - public AudioClip pasteMeshSound; - public AudioClip redoSound; - public AudioClip releaseMeshSound; - public AudioClip saveSound; - public AudioClip selectToolSound; - public AudioClip shapeMenuEndSound; - public AudioClip snapSound; - public AudioClip subdivideSound; - public AudioClip startupSound; - public AudioClip successSound; - public AudioClip swipeLeftSound; - public AudioClip swipeRightSound; - public AudioClip toggleMenuSound; - public AudioClip tutorialCompletionSound; - public AudioClip tutorialIntroSound; - public AudioClip tutorialMeshAnimateInSound; - public AudioClip undoSound; - public AudioClip ungroupSound; - public AudioClip zoomResetSound; +public class AudioLibrary : MonoBehaviour +{ + // Named audio clips, each of which must be loaded in Start(). + public AudioClip alignSound; + public AudioClip breakSound; + public AudioClip confettiSound; + public AudioClip copySound; + public AudioClip decrementSound; + public AudioClip deleteSound; + public AudioClip errorSound; + public AudioClip genericSelectSound; + public AudioClip genericReleaseSound; + public AudioClip grabMeshSound; + public AudioClip grabMeshPartSound; + public AudioClip groupSound; + public AudioClip incrementSound; + public AudioClip insertVolumeSound; + public AudioClip menuSelectSound; + public AudioClip modifyMeshSound; + public AudioClip paintSound; + public AudioClip pasteMeshSound; + public AudioClip redoSound; + public AudioClip releaseMeshSound; + public AudioClip saveSound; + public AudioClip selectToolSound; + public AudioClip shapeMenuEndSound; + public AudioClip snapSound; + public AudioClip subdivideSound; + public AudioClip startupSound; + public AudioClip successSound; + public AudioClip swipeLeftSound; + public AudioClip swipeRightSound; + public AudioClip toggleMenuSound; + public AudioClip tutorialCompletionSound; + public AudioClip tutorialIntroSound; + public AudioClip tutorialMeshAnimateInSound; + public AudioClip undoSound; + public AudioClip ungroupSound; + public AudioClip zoomResetSound; + + /// + /// All AudioSources generated by play requests, to be periodically cleaned up. + /// + private List sources = new List(); - /// - /// All AudioSources generated by play requests, to be periodically cleaned up. - /// - private List sources = new List(); + /// + /// All AudioSources that are being faded. + /// + private HashSet sourcesToFade = new HashSet(); - /// - /// All AudioSources that are being faded. - /// - private HashSet sourcesToFade = new HashSet(); + /// + /// Periodicity for cleanup, in seconds. + /// + private static float CLEANUP_INTERVAL = 5; + /// + /// Maximum number of items we'll cleanup in a tick. + /// + private static float CLEANUP_LIMIT = 5; + /// + /// Maximum duration of the fade. + /// + private static float FADE_DURATION = 0.5f; + /// + /// Timestamp of most recent cleanup. + /// + private float lastCleanup; + /// + /// Whether sounds are enabled. + /// + private bool soundsEnabled; - /// - /// Periodicity for cleanup, in seconds. - /// - private static float CLEANUP_INTERVAL = 5; - /// - /// Maximum number of items we'll cleanup in a tick. - /// - private static float CLEANUP_LIMIT = 5; - /// - /// Maximum duration of the fade. - /// - private static float FADE_DURATION = 0.5f; - /// - /// Timestamp of most recent cleanup. - /// - private float lastCleanup; - /// - /// Whether sounds are enabled. - /// - private bool soundsEnabled; + // Leave time between repeat plays of the same clip to prevent ear-overload. + private const float INTERVAL_BETWEEN_PLAYS = .25f; + private Dictionary clipsLastPlayTime = new Dictionary(); - // Leave time between repeat plays of the same clip to prevent ear-overload. - private const float INTERVAL_BETWEEN_PLAYS = .25f; - private Dictionary clipsLastPlayTime = new Dictionary(); + /// + /// Call once to initialize and load all sounds. + /// + public void Setup() + { + alignSound = Resources.Load("Audio/Poly_InsertPrimitiveShape_03"); + breakSound = Resources.Load("Audio/Poly_ObjectBreak_04"); + confettiSound = Resources.Load("Audio/Poly_ObjectBreak_10"); + copySound = Resources.Load("Audio/Poly_Copy_02"); + decrementSound = Resources.Load("Audio/Poly_Decrement"); + deleteSound = Resources.Load("Audio/Poly_Erase_01"); + errorSound = Resources.Load("Audio/Poly_InvalidOperation_01"); + groupSound = Resources.Load("Audio/Poly_Group_04"); + grabMeshSound = Resources.Load("Audio/Poly_GrabObject_04"); + grabMeshPartSound = Resources.Load("Audio/Poly_GrabVertex_02"); + incrementSound = Resources.Load("Audio/Poly_Increment"); + genericSelectSound = Resources.Load("Audio/Poly_GenericSelect_07"); + genericReleaseSound = Resources.Load("Audio/Poly_GenericRelease_07"); + insertVolumeSound = Resources.Load("Audio/Poly_InsertVolume_08"); + menuSelectSound = Resources.Load("Audio/Poly_GenericSelect_07"); + modifyMeshSound = Resources.Load("Audio/Poly_Move_08"); + paintSound = Resources.Load("Audio/Poly_Paint_05"); + pasteMeshSound = Resources.Load("Audio/Poly_Paste_01"); + redoSound = Resources.Load("Audio/Poly_Redo_02"); + releaseMeshSound = Resources.Load("Audio/Poly_ReleaseObject_04"); + saveSound = Resources.Load("Audio/Poly_Success_10"); + shapeMenuEndSound = Resources.Load("Audio/Poly_IncrementSizeUp_01"); + selectToolSound = Resources.Load("Audio/Poly_SelectToolFromPalette_01"); + snapSound = Resources.Load("Audio/Poly_InsertVolume_03"); + subdivideSound = Resources.Load("Audio/Poly_Subdivide_01"); + startupSound = Resources.Load("Audio/Poly_IntroAnim_FullStereo"); + successSound = Resources.Load("Audio/Poly_Success_05"); + swipeLeftSound = Resources.Load("Audio/Poly_ChangeShapeLeft_01"); + swipeRightSound = Resources.Load("Audio/Poly_ChangeShapeRight_01"); + toggleMenuSound = Resources.Load("Audio/Poly_GrabObject_02"); + tutorialCompletionSound = Resources.Load("Audio/Poly_Success_12"); + tutorialIntroSound = Resources.Load("Audio/Poly_IntroAnim_PolyNotes_01"); + tutorialMeshAnimateInSound = Resources.Load("Audio/Poly_Copy_04"); + undoSound = Resources.Load("Audio/Poly_Undo_02"); + ungroupSound = Resources.Load("Audio/Poly_UnGroup_04"); + zoomResetSound = Resources.Load("Audio/Poly_ZoomReset_01"); - /// - /// Call once to initialize and load all sounds. - /// - public void Setup() { - alignSound = Resources.Load("Audio/Poly_InsertPrimitiveShape_03"); - breakSound = Resources.Load("Audio/Poly_ObjectBreak_04"); - confettiSound = Resources.Load("Audio/Poly_ObjectBreak_10"); - copySound = Resources.Load("Audio/Poly_Copy_02"); - decrementSound = Resources.Load("Audio/Poly_Decrement"); - deleteSound = Resources.Load("Audio/Poly_Erase_01"); - errorSound = Resources.Load("Audio/Poly_InvalidOperation_01"); - groupSound = Resources.Load("Audio/Poly_Group_04"); - grabMeshSound = Resources.Load("Audio/Poly_GrabObject_04"); - grabMeshPartSound = Resources.Load("Audio/Poly_GrabVertex_02"); - incrementSound = Resources.Load("Audio/Poly_Increment"); - genericSelectSound = Resources.Load("Audio/Poly_GenericSelect_07"); - genericReleaseSound = Resources.Load("Audio/Poly_GenericRelease_07"); - insertVolumeSound = Resources.Load("Audio/Poly_InsertVolume_08"); - menuSelectSound = Resources.Load("Audio/Poly_GenericSelect_07"); - modifyMeshSound = Resources.Load("Audio/Poly_Move_08"); - paintSound = Resources.Load("Audio/Poly_Paint_05"); - pasteMeshSound = Resources.Load("Audio/Poly_Paste_01"); - redoSound = Resources.Load("Audio/Poly_Redo_02"); - releaseMeshSound = Resources.Load("Audio/Poly_ReleaseObject_04"); - saveSound = Resources.Load("Audio/Poly_Success_10"); - shapeMenuEndSound = Resources.Load("Audio/Poly_IncrementSizeUp_01"); - selectToolSound = Resources.Load("Audio/Poly_SelectToolFromPalette_01"); - snapSound = Resources.Load("Audio/Poly_InsertVolume_03"); - subdivideSound = Resources.Load("Audio/Poly_Subdivide_01"); - startupSound = Resources.Load("Audio/Poly_IntroAnim_FullStereo"); - successSound = Resources.Load("Audio/Poly_Success_05"); - swipeLeftSound = Resources.Load("Audio/Poly_ChangeShapeLeft_01"); - swipeRightSound = Resources.Load("Audio/Poly_ChangeShapeRight_01"); - toggleMenuSound = Resources.Load("Audio/Poly_GrabObject_02"); - tutorialCompletionSound = Resources.Load("Audio/Poly_Success_12"); - tutorialIntroSound = Resources.Load("Audio/Poly_IntroAnim_PolyNotes_01"); - tutorialMeshAnimateInSound = Resources.Load("Audio/Poly_Copy_04"); - undoSound = Resources.Load("Audio/Poly_Undo_02"); - ungroupSound = Resources.Load("Audio/Poly_UnGroup_04"); - zoomResetSound = Resources.Load("Audio/Poly_ZoomReset_01"); + // Enable sounds on start. + soundsEnabled = true; + } - // Enable sounds on start. - soundsEnabled = true; - } + /// + /// Periodically cleans up any expired AudioSources. + /// + void Update() + { + HashSet fadesToRemove = new HashSet(); + foreach (Fade sourceToFade in sourcesToFade) + { + float fadePct = (Time.time - sourceToFade.startTime) / sourceToFade.duration; + if (fadePct > 1.0f) + { + fadesToRemove.Add(sourceToFade); + fadePct = 1.0f; + } + sourceToFade.source.volume = sourceToFade.startVolume * (1 - fadePct); + } + if (Time.time - lastCleanup > CLEANUP_INTERVAL) + { + lastCleanup = Time.time; + int itemsDestroyed = 0; + List newList = new List(); - /// - /// Periodically cleans up any expired AudioSources. - /// - void Update() { - HashSet fadesToRemove = new HashSet(); - foreach (Fade sourceToFade in sourcesToFade) { - float fadePct = (Time.time - sourceToFade.startTime) / sourceToFade.duration; - if (fadePct > 1.0f) { - fadesToRemove.Add(sourceToFade); - fadePct = 1.0f; - } - sourceToFade.source.volume = sourceToFade.startVolume * (1 - fadePct); + // The sources in fadesToRemove will be destoyed later through the following loop. + sourcesToFade.ExceptWith(fadesToRemove); + foreach (AudioSource source in sources) + { + if (!source.isPlaying && itemsDestroyed++ < CLEANUP_LIMIT) + Destroy(source); + else + newList.Add(source); + } + sources = newList; + } } - if (Time.time - lastCleanup > CLEANUP_INTERVAL) { - lastCleanup = Time.time; - int itemsDestroyed = 0; - List newList = new List(); - // The sources in fadesToRemove will be destoyed later through the following loop. - sourcesToFade.ExceptWith(fadesToRemove); - foreach (AudioSource source in sources) { - if (!source.isPlaying && itemsDestroyed++ < CLEANUP_LIMIT) - Destroy(source); - else - newList.Add(source); - } - sources = newList; + /// + /// Creates a new AudioSource and plays the given clip once with standard pitch. + /// + public void PlayClip(AudioClip clip) + { + PlayClip(clip, /* pitch */ 1.0f); } - } - /// - /// Creates a new AudioSource and plays the given clip once with standard pitch. - /// - public void PlayClip(AudioClip clip) { - PlayClip(clip, /* pitch */ 1.0f); - } + /// + /// Creates a new AudioSource and plays the given clip once with the given pitch. + /// + public void PlayClip(AudioClip clip, float pitch) + { + // Don't play audio clips if the user has disabled sounds. + if (clip == null || !soundsEnabled) + return; - /// - /// Creates a new AudioSource and plays the given clip once with the given pitch. - /// - public void PlayClip(AudioClip clip, float pitch) { - // Don't play audio clips if the user has disabled sounds. - if (clip == null || !soundsEnabled) - return; + // Don't repeat audio-clips too quickly. + float lastPlayedTime; + if (clipsLastPlayTime.TryGetValue(clip, out lastPlayedTime) + && Time.time - lastPlayedTime < INTERVAL_BETWEEN_PLAYS) + { + return; + } + clipsLastPlayTime[clip] = Time.time; - // Don't repeat audio-clips too quickly. - float lastPlayedTime; - if (clipsLastPlayTime.TryGetValue(clip, out lastPlayedTime) - && Time.time - lastPlayedTime < INTERVAL_BETWEEN_PLAYS) { - return; + AudioSource source = gameObject.AddComponent(); + source.clip = clip; + source.pitch = pitch; + source.PlayOneShot(clip); + sources.Add(source); } - clipsLastPlayTime[clip] = Time.time; - AudioSource source = gameObject.AddComponent(); - source.clip = clip; - source.pitch = pitch; - source.PlayOneShot(clip); - sources.Add(source); - } + /// + /// Stops all clips of a given type from playing. + /// + public void StopClip(AudioClip clip) + { + // Don't bother if the user has disabled sounds. + if (clip == null || !soundsEnabled) + return; - /// - /// Stops all clips of a given type from playing. - /// - public void StopClip(AudioClip clip) { - // Don't bother if the user has disabled sounds. - if (clip == null || !soundsEnabled) - return; - - foreach (AudioSource audioSource in sources) { - // If the given audio matches the type we are trying to stop, - // then stop it. Note: Multiple instances of the same clip will - // all be stopped. - if (audioSource.isPlaying && audioSource.clip == clip) { - audioSource.Stop(); - } + foreach (AudioSource audioSource in sources) + { + // If the given audio matches the type we are trying to stop, + // then stop it. Note: Multiple instances of the same clip will + // all be stopped. + if (audioSource.isPlaying && audioSource.clip == clip) + { + audioSource.Stop(); + } + } } - } - /// - /// Begins fading all clips of a given type. - /// - public void FadeClip(AudioClip clip) { - // Don't bother if the user has disabled sounds. - if (clip == null || !soundsEnabled) - return; + /// + /// Begins fading all clips of a given type. + /// + public void FadeClip(AudioClip clip) + { + // Don't bother if the user has disabled sounds. + if (clip == null || !soundsEnabled) + return; - foreach (AudioSource audioSource in sources) { - // If the given audio matches and is playing, the add it to our fading list. - if (audioSource.isPlaying && audioSource.clip == clip) { - sourcesToFade.Add(new Fade(audioSource, Time.time, audioSource.volume, FADE_DURATION)); - } + foreach (AudioSource audioSource in sources) + { + // If the given audio matches and is playing, the add it to our fading list. + if (audioSource.isPlaying && audioSource.clip == clip) + { + sourcesToFade.Add(new Fade(audioSource, Time.time, audioSource.volume, FADE_DURATION)); + } + } } - } - /// - /// Toggles whether sounds are enabled. - /// - public void ToggleSounds() { - soundsEnabled = !soundsEnabled; - PlayerPrefs.SetString(PeltzerMain.DISABLE_SOUNDS_KEY, soundsEnabled ? "false" : "true"); - ObjectFinder.ObjectById("ID_sounds_are_on").SetActive(soundsEnabled); - ObjectFinder.ObjectById("ID_sounds_are_off").SetActive(!soundsEnabled); - } + /// + /// Toggles whether sounds are enabled. + /// + public void ToggleSounds() + { + soundsEnabled = !soundsEnabled; + PlayerPrefs.SetString(PeltzerMain.DISABLE_SOUNDS_KEY, soundsEnabled ? "false" : "true"); + ObjectFinder.ObjectById("ID_sounds_are_on").SetActive(soundsEnabled); + ObjectFinder.ObjectById("ID_sounds_are_off").SetActive(!soundsEnabled); + } } diff --git a/Assets/Scripts/audio/Objectionary.cs b/Assets/Scripts/audio/Objectionary.cs index d2153e0a..8be61ff0 100644 --- a/Assets/Scripts/audio/Objectionary.cs +++ b/Assets/Scripts/audio/Objectionary.cs @@ -21,139 +21,154 @@ /// /// Let's have a nice game of Objectionary. /// -public class Objectionary : MonoBehaviour { - private static readonly System.Random rnd = new System.Random(); - - // All clips, and which clip to play next. - private Dictionary audioClipsByName = new Dictionary(); - private List audioClipNames = new List(); - private int nextClipToPlay = -1; - - // Announcer/countdown timer. - private AudioSource begin; - private AudioSource tick; - private AudioSource end; - - // Game state/constants. - private bool isPlaying; - private float timeGameStarted; - private static float GAME_DURATION = 120; - - // We'll abuse the Poly version text for a countdown timer. - Text titleText; - - /// - /// All AudioSources generated by play requests, to be periodically cleaned up. - /// - private List sources = new List(); - - /// - /// Periodicity for cleanup, in seconds. - /// - private static float CLEANUP_INTERVAL = 5; - /// - /// Timestamp of most recent cleanup. - /// - private float lastCleanup; - - void Start() { - begin = gameObject.AddComponent(); - tick = gameObject.AddComponent(); - end = gameObject.AddComponent(); - begin.playOnAwake = false; - tick.playOnAwake = false; - end.playOnAwake = false; - begin.clip = Resources.Load("ObjectionaryMeta/begin"); - tick.clip = Resources.Load("ObjectionaryMeta/tick"); - end.clip = Resources.Load("ObjectionaryMeta/end"); - - titleText = GameObject.Find("DesktopAppTitleText").GetComponentInChildren(); - - Object[] allAudioClips = Resources.LoadAll("Objectionary"); - foreach (Object o in allAudioClips) { - AudioClip audioClip = (AudioClip)o; - audioClipsByName.Add(audioClip.name, audioClip); - audioClipNames.Add(audioClip.name); +public class Objectionary : MonoBehaviour +{ + private static readonly System.Random rnd = new System.Random(); + + // All clips, and which clip to play next. + private Dictionary audioClipsByName = new Dictionary(); + private List audioClipNames = new List(); + private int nextClipToPlay = -1; + + // Announcer/countdown timer. + private AudioSource begin; + private AudioSource tick; + private AudioSource end; + + // Game state/constants. + private bool isPlaying; + private float timeGameStarted; + private static float GAME_DURATION = 120; + + // We'll abuse the Poly version text for a countdown timer. + Text titleText; + + /// + /// All AudioSources generated by play requests, to be periodically cleaned up. + /// + private List sources = new List(); + + /// + /// Periodicity for cleanup, in seconds. + /// + private static float CLEANUP_INTERVAL = 5; + /// + /// Timestamp of most recent cleanup. + /// + private float lastCleanup; + + void Start() + { + begin = gameObject.AddComponent(); + tick = gameObject.AddComponent(); + end = gameObject.AddComponent(); + begin.playOnAwake = false; + tick.playOnAwake = false; + end.playOnAwake = false; + begin.clip = Resources.Load("ObjectionaryMeta/begin"); + tick.clip = Resources.Load("ObjectionaryMeta/tick"); + end.clip = Resources.Load("ObjectionaryMeta/end"); + + titleText = GameObject.Find("DesktopAppTitleText").GetComponentInChildren(); + + Object[] allAudioClips = Resources.LoadAll("Objectionary"); + foreach (Object o in allAudioClips) + { + AudioClip audioClip = (AudioClip)o; + audioClipsByName.Add(audioClip.name, audioClip); + audioClipNames.Add(audioClip.name); + } + // 'Randomly order' the list. + audioClipNames = audioClipNames.OrderBy(item => rnd.Next()).ToList(); } - // 'Randomly order' the list. - audioClipNames = audioClipNames.OrderBy(item => rnd.Next()).ToList(); - } - - /// - /// Periodically cleans up any expired AudioSources. - /// - void Update() { - // Countdown & game over. - if (isPlaying) { - if (Time.time - timeGameStarted > GAME_DURATION - 10f) { - if (!tick.isPlaying) { - tick.Play(); + + /// + /// Periodically cleans up any expired AudioSources. + /// + void Update() + { + // Countdown & game over. + if (isPlaying) + { + if (Time.time - timeGameStarted > GAME_DURATION - 10f) + { + if (!tick.isPlaying) + { + tick.Play(); + } + + if (Time.time - timeGameStarted > GAME_DURATION) + { + tick.Stop(); + end.Play(); + isPlaying = false; + } + } } - if (Time.time - timeGameStarted > GAME_DURATION) { - tick.Stop(); - end.Play(); - isPlaying = false; + // Countdown timer text. + if (isPlaying) + { + float timeLeft = GAME_DURATION - (Time.time - timeGameStarted); + string text = "Objectionary: " + timeLeft.ToString("0.00"); + titleText.text = text; } - } - } - // Countdown timer text. - if (isPlaying) { - float timeLeft = GAME_DURATION - (Time.time - timeGameStarted); - string text = "Objectionary: " + timeLeft.ToString("0.00"); - titleText.text = text; + // Clean up. + if (Time.time - lastCleanup > CLEANUP_INTERVAL) + { + lastCleanup = Time.time; + List newList = new List(); + foreach (AudioSource source in sources) + { + if (!source.isPlaying) + DestroyImmediate(source); + else + newList.Add(source); + } + sources = newList; + } } - // Clean up. - if (Time.time - lastCleanup > CLEANUP_INTERVAL) { - lastCleanup = Time.time; - List newList = new List(); - foreach (AudioSource source in sources) { - if (!source.isPlaying) - DestroyImmediate(source); - else - newList.Add(source); - } - sources = newList; - } - } - - /// - /// Play the next object, or cycle around and re-randomise the list. - /// - public void PlayNewObject() { - if (audioClipNames.Count == 0) { - Debug.Log("No audio clips"); - return; - } + /// + /// Play the next object, or cycle around and re-randomise the list. + /// + public void PlayNewObject() + { + if (audioClipNames.Count == 0) + { + Debug.Log("No audio clips"); + return; + } - // Start over if they've heard everything. - if (++nextClipToPlay == audioClipNames.Count) { - audioClipNames = audioClipNames.OrderBy(item => rnd.Next()).ToList(); - nextClipToPlay = 0; - } + // Start over if they've heard everything. + if (++nextClipToPlay == audioClipNames.Count) + { + audioClipNames = audioClipNames.OrderBy(item => rnd.Next()).ToList(); + nextClipToPlay = 0; + } - // Get the next clip. - AudioSource source = gameObject.AddComponent(); - AudioClip toPlay = audioClipsByName[audioClipNames[nextClipToPlay]]; - - // First time. - if (!isPlaying) { - isPlaying = true; - timeGameStarted = Time.time; - begin.Play(); - - // Play delayed. - source.clip = toPlay; - source.PlayDelayed(1.5f); - lastCleanup = Time.time + 5f; // Avoid cleaning up before playing. - sources.Add(source); - return; - } + // Get the next clip. + AudioSource source = gameObject.AddComponent(); + AudioClip toPlay = audioClipsByName[audioClipNames[nextClipToPlay]]; + + // First time. + if (!isPlaying) + { + isPlaying = true; + timeGameStarted = Time.time; + begin.Play(); + + // Play delayed. + source.clip = toPlay; + source.PlayDelayed(1.5f); + lastCleanup = Time.time + 5f; // Avoid cleaning up before playing. + sources.Add(source); + return; + } - // Normal. - source.PlayOneShot(toPlay); - sources.Add(source); - } + // Normal. + source.PlayOneShot(toPlay); + sources.Add(source); + } } diff --git a/Assets/Scripts/desktop_app/ButtonUI.cs b/Assets/Scripts/desktop_app/ButtonUI.cs index 0ce6fc8b..6ac4e0b6 100644 --- a/Assets/Scripts/desktop_app/ButtonUI.cs +++ b/Assets/Scripts/desktop_app/ButtonUI.cs @@ -15,35 +15,40 @@ using UnityEngine; using UnityEngine.EventSystems; -namespace com.google.apps.peltzer.client.desktop_app { - /// - /// A class to manage hover behaviour for buttons in our desktop app. - /// - class ButtonUI : MonoBehaviour { - // A hover highlight, normally comprising an image around the button and a textual tip about the button. - private GameObject tip; +namespace com.google.apps.peltzer.client.desktop_app +{ + /// + /// A class to manage hover behaviour for buttons in our desktop app. + /// + class ButtonUI : MonoBehaviour + { + // A hover highlight, normally comprising an image around the button and a textual tip about the button. + private GameObject tip; - void Start() { - tip = transform.Find("Tip").gameObject; - tip.SetActive(false); + void Start() + { + tip = transform.Find("Tip").gameObject; + tip.SetActive(false); - EventTrigger trigger = GetComponent(); - EventTrigger.Entry entry = new EventTrigger.Entry(); - entry.eventID = EventTriggerType.PointerEnter; - entry.callback.AddListener((data) => { OnPointerEnter(); }); - trigger.triggers.Add(entry); - entry = new EventTrigger.Entry(); - entry.eventID = EventTriggerType.PointerExit; - entry.callback.AddListener((data) => { OnPointerExit(); }); - trigger.triggers.Add(entry); - } + EventTrigger trigger = GetComponent(); + EventTrigger.Entry entry = new EventTrigger.Entry(); + entry.eventID = EventTriggerType.PointerEnter; + entry.callback.AddListener((data) => { OnPointerEnter(); }); + trigger.triggers.Add(entry); + entry = new EventTrigger.Entry(); + entry.eventID = EventTriggerType.PointerExit; + entry.callback.AddListener((data) => { OnPointerExit(); }); + trigger.triggers.Add(entry); + } - public void OnPointerEnter() { - tip.SetActive(true); - } - - public void OnPointerExit() { - tip.SetActive(false); + public void OnPointerEnter() + { + tip.SetActive(true); + } + + public void OnPointerExit() + { + tip.SetActive(false); + } } - } } diff --git a/Assets/Scripts/desktop_app/DebugConsole.cs b/Assets/Scripts/desktop_app/DebugConsole.cs index 6678f5d9..fc2e92ad 100644 --- a/Assets/Scripts/desktop_app/DebugConsole.cs +++ b/Assets/Scripts/desktop_app/DebugConsole.cs @@ -30,826 +30,983 @@ using com.google.apps.peltzer.client.model.render; using com.google.apps.peltzer.client.app; -namespace com.google.apps.peltzer.client.desktop_app { - /// - /// Controls the debug console that appears on the desktop app, where the user can give commands. - /// - public class DebugConsole : MonoBehaviour { - private const string HELP_TEXT = "COMMANDS:\n" + - "dump\n dump debug logs/state to a file\n" + - "env\n change environment (background)\n" + - "flag\n lists/sets feature flags\n" + - "fuse\n fuses all selected meshes into a single mesh.\n" + - "help\n shows this help text\n" + - "insert\n insert primitives\n" + - "insertduration \n sets the mesh insert effect duration (e.g. 0.6).\n" + - "loadfile \n loads a model from the given file (use full path).\n" + - "loadres \n loads a model from the given resource file.\n" + - "minfo\n prints info about the selected meshes.\n" + - "movev\n moves vertices by a given delta.\n" + - "osq \n queries objects from the object store.\n" + - "osload \n loads the given search result# of the last osq command.\n" + - "publish\n saves & publishes the current scene.\n" + - "rest\n change restrictions.\n" + - "savefile \n saves model to the given file (use full path).\n" + - "setgid \n sets the group ID of selected mesh group.\n" + - "setmid \n sets the mesh ID of selected mesh.\n" + - "setmaxundo \n sets the maximum number of undos to store.\n" + - "tut\n tutorial-related commands.\n"; - - // Set from the Unity editor: - public GameObject consoleObject; - public Text consoleOutput; - public InputField consoleInput; - - private string lastCommand = ""; - - private Material originalSkybox; - - // Results of the last search, null if none. - ObjectStoreEntry[] objectStoreSearchResults; - - public void Start() { - consoleOutput.text = "DEBUG CONSOLE\n" + - "Blocks version: " + Config.Instance.version + "\n" + - "For a list of available commands, type 'help'." + - "Press ESC to close console."; - } +namespace com.google.apps.peltzer.client.desktop_app +{ + /// + /// Controls the debug console that appears on the desktop app, where the user can give commands. + /// + public class DebugConsole : MonoBehaviour + { + private const string HELP_TEXT = "COMMANDS:\n" + + "dump\n dump debug logs/state to a file\n" + + "env\n change environment (background)\n" + + "flag\n lists/sets feature flags\n" + + "fuse\n fuses all selected meshes into a single mesh.\n" + + "help\n shows this help text\n" + + "insert\n insert primitives\n" + + "insertduration \n sets the mesh insert effect duration (e.g. 0.6).\n" + + "loadfile \n loads a model from the given file (use full path).\n" + + "loadres \n loads a model from the given resource file.\n" + + "minfo\n prints info about the selected meshes.\n" + + "movev\n moves vertices by a given delta.\n" + + "osq \n queries objects from the object store.\n" + + "osload \n loads the given search result# of the last osq command.\n" + + "publish\n saves & publishes the current scene.\n" + + "rest\n change restrictions.\n" + + "savefile \n saves model to the given file (use full path).\n" + + "setgid \n sets the group ID of selected mesh group.\n" + + "setmid \n sets the mesh ID of selected mesh.\n" + + "setmaxundo \n sets the maximum number of undos to store.\n" + + "tut\n tutorial-related commands.\n"; + + // Set from the Unity editor: + public GameObject consoleObject; + public Text consoleOutput; + public InputField consoleInput; + + private string lastCommand = ""; + + private Material originalSkybox; + + // Results of the last search, null if none. + ObjectStoreEntry[] objectStoreSearchResults; + + public void Start() + { + consoleOutput.text = "DEBUG CONSOLE\n" + + "Blocks version: " + Config.Instance.version + "\n" + + "For a list of available commands, type 'help'." + + "Press ESC to close console."; + } - private void Update() { - // Key combination: Ctrl + D - bool keyComboPressed = Input.GetKeyDown(KeyCode.D) && Input.GetKey(KeyCode.LeftControl); - bool escPressed = Input.GetKeyDown(KeyCode.Escape); - - // To open the console, the user has to press the key combo. - // To close it, either ESC or the key combo are accepted. - if (!consoleObject.activeSelf && keyComboPressed) { - // Show console. - consoleObject.SetActive(true); - // Focus on the text field so the user can start typing right away. - consoleInput.ActivateInputField(); - consoleInput.Select(); - } else if (consoleObject.activeSelf && (keyComboPressed || escPressed)) { - // Hide console. - consoleObject.SetActive(false); - } - - if (!consoleObject.activeSelf) return; - - if (Input.GetKeyDown(KeyCode.Return)) { - // Run command. - RunCommand(consoleInput.text); - consoleInput.text = ""; - consoleInput.ActivateInputField(); - consoleInput.Select(); - } else if (Input.GetKeyDown(KeyCode.UpArrow)) { - // Recover last command and put it in the input text. - consoleInput.text = lastCommand; - consoleInput.ActivateInputField(); - consoleInput.Select(); - } - } + private void Update() + { + // Key combination: Ctrl + D + bool keyComboPressed = Input.GetKeyDown(KeyCode.D) && Input.GetKey(KeyCode.LeftControl); + bool escPressed = Input.GetKeyDown(KeyCode.Escape); + + // To open the console, the user has to press the key combo. + // To close it, either ESC or the key combo are accepted. + if (!consoleObject.activeSelf && keyComboPressed) + { + // Show console. + consoleObject.SetActive(true); + // Focus on the text field so the user can start typing right away. + consoleInput.ActivateInputField(); + consoleInput.Select(); + } + else if (consoleObject.activeSelf && (keyComboPressed || escPressed)) + { + // Hide console. + consoleObject.SetActive(false); + } - private void RunCommand(string command) { - lastCommand = command; - consoleOutput.text = ""; - string[] parts = command.Split(' '); - switch (parts[0]) { - case "dump": - CommandDump(parts); - break; - case "env": - CommandEnv(parts); - break; - case "flag": - CommandFlag(parts); - break; - case "fuse": - CommandFuse(parts); - break; - case "help": - PrintLn(HELP_TEXT); - break; - case "insert": - CommandInsert(parts); - break; - case "insertduration": - CommandInsertDuration(parts); - break; - case "loadfile": - CommandLoadFile(parts); - break; - case "loadres": - CommandLoadRes(parts); - break; - case "minfo": - CommandMInfo(parts); - break; - case "movev": - CommandMoveV(parts); - break; - case "osq": - CommandOsQ(parts); - break; - case "osload": - CommandOsLoad(parts); - break; - case "ospublish": - CommandOsPublish(parts); - break; - case "publish": - CommandPublish(parts); - break; - case "rest": - CommandRest(parts); - break; - case "savefile": - CommandSaveFile(parts); - break; - case "setgid": - CommandSetGid(parts); - break; - case "setmid": - CommandSetMid(parts); - break; - case "setmaxundo": - CommandSetMaxUndo(parts); - break; - case "tut": - CommandTut(parts); - break; - default: - PrintLn("Unrecognized command: " + command); - PrintLn("Type 'help' for a list of commands."); - break; - } - } + if (!consoleObject.activeSelf) return; - private void PrintLn(string message) { - consoleOutput.text += message + "\n"; - } + if (Input.GetKeyDown(KeyCode.Return)) + { + // Run command. + RunCommand(consoleInput.text); + consoleInput.text = ""; + consoleInput.ActivateInputField(); + consoleInput.Select(); + } + else if (Input.GetKeyDown(KeyCode.UpArrow)) + { + // Recover last command and put it in the input text. + consoleInput.text = lastCommand; + consoleInput.ActivateInputField(); + consoleInput.Select(); + } + } - private void CommandOsQ(string[] parts) { - if (parts.Length != 2) { - PrintLn("Syntax: osq "); - PrintLn(" Queries the object store with the given term or tag."); - PrintLn(" Examples:"); - PrintLn(" osq featured"); - PrintLn(" osq tea"); - return; - } - string query = parts[1]; - ObjectStoreClient objectStoreClient = new ObjectStoreClient(); - StringBuilder builder = new StringBuilder(ObjectStoreClient.OBJECT_STORE_BASE_URL); - builder.Append("/s?q=").Append(query); - PrintLn("Querying for '" + query + "'..."); - StartCoroutine(objectStoreClient.GetObjectStoreListings( - ObjectStoreClient.GetNewGetRequest(builder, "text/plain"), (ObjectStoreSearchResult result) => { - if (result.results != null && result.results.Length > 0) { - objectStoreSearchResults = result.results; - PrintLn(objectStoreSearchResults.Length + " result(s).\n"); - PrintLn("To load any of these, use 'osload '.\n"); - PrintLn("To publish any of these, use 'ospublish '.\n\n"); - for (int i = 0; i < objectStoreSearchResults.Length; i++) { - ObjectStoreEntry entry = objectStoreSearchResults[i]; - PrintLn(string.Format("{0}: '{1}' ({2})", i, entry.title, entry.id)); - } - } else { - objectStoreSearchResults = null; - PrintLn("No query results."); - return; - } - })); - } + private void RunCommand(string command) + { + lastCommand = command; + consoleOutput.text = ""; + string[] parts = command.Split(' '); + switch (parts[0]) + { + case "dump": + CommandDump(parts); + break; + case "env": + CommandEnv(parts); + break; + case "flag": + CommandFlag(parts); + break; + case "fuse": + CommandFuse(parts); + break; + case "help": + PrintLn(HELP_TEXT); + break; + case "insert": + CommandInsert(parts); + break; + case "insertduration": + CommandInsertDuration(parts); + break; + case "loadfile": + CommandLoadFile(parts); + break; + case "loadres": + CommandLoadRes(parts); + break; + case "minfo": + CommandMInfo(parts); + break; + case "movev": + CommandMoveV(parts); + break; + case "osq": + CommandOsQ(parts); + break; + case "osload": + CommandOsLoad(parts); + break; + case "ospublish": + CommandOsPublish(parts); + break; + case "publish": + CommandPublish(parts); + break; + case "rest": + CommandRest(parts); + break; + case "savefile": + CommandSaveFile(parts); + break; + case "setgid": + CommandSetGid(parts); + break; + case "setmid": + CommandSetMid(parts); + break; + case "setmaxundo": + CommandSetMaxUndo(parts); + break; + case "tut": + CommandTut(parts); + break; + default: + PrintLn("Unrecognized command: " + command); + PrintLn("Type 'help' for a list of commands."); + break; + } + } - private void CommandOsLoad(string[] parts) { - int index; - if (parts.Length != 2 || !int.TryParse(parts[1], out index)) { - PrintLn("Syntax: osload "); - PrintLn(" Loads the given search result (after calling osq)"); - return; - } - if (objectStoreSearchResults == null || index < 0 || index >= objectStoreSearchResults.Length) { - PrintLn("Invalid search result index. Must be one of the results produced by the osq command."); - return; - } - ObjectStoreClient objectStoreClient = new ObjectStoreClient(); - ObjectStoreEntry entry = objectStoreSearchResults[index]; - PrintLn(string.Format("Loading search result #{0}: {1} (id: {2})...", index, entry.title, entry.id)); - StartCoroutine(objectStoreClient.GetPeltzerFile(entry, (PeltzerFile peltzerFile) => { - PrintLn("Loaded successfully!"); - PeltzerMain.Instance.CreateNewModel(); - PeltzerMain.Instance.LoadPeltzerFileIntoModel(peltzerFile); - })); - } + private void PrintLn(string message) + { + consoleOutput.text += message + "\n"; + } - private void CommandOsPublish(string[] parts) { - int index; - if (parts.Length != 2 || !int.TryParse(parts[1], out index)) { - PrintLn("Syntax: publish "); - PrintLn(" Loads, saves, then opens the publish dialog for the given search result (after calling osq)"); - return; - } - if (objectStoreSearchResults == null || index < 0 || index >= objectStoreSearchResults.Length) { - PrintLn("Invalid search result index. Must be one of the results produced by the osq command."); - return; - } - ObjectStoreClient objectStoreClient = new ObjectStoreClient(); - ObjectStoreEntry entry = objectStoreSearchResults[index]; - PrintLn(string.Format("Publishing search result #{0}: {1} (id: {2})...", index, entry.title, entry.id)); - StartCoroutine(objectStoreClient.GetPeltzerFile(entry, (PeltzerFile peltzerFile) => { - PrintLn("Loaded successfully, now trying to save & publish\n."); - PrintLn("If no browser window opens after a minute or so, this might have failed."); - PeltzerMain.Instance.CreateNewModel(); - PeltzerMain.Instance.LoadPeltzerFileIntoModel(peltzerFile); - PeltzerMain.Instance.SaveCurrentModel(publish:true, saveSelected:false); - })); - } + private void CommandOsQ(string[] parts) + { + if (parts.Length != 2) + { + PrintLn("Syntax: osq "); + PrintLn(" Queries the object store with the given term or tag."); + PrintLn(" Examples:"); + PrintLn(" osq featured"); + PrintLn(" osq tea"); + return; + } + string query = parts[1]; + ObjectStoreClient objectStoreClient = new ObjectStoreClient(); + StringBuilder builder = new StringBuilder(ObjectStoreClient.OBJECT_STORE_BASE_URL); + builder.Append("/s?q=").Append(query); + PrintLn("Querying for '" + query + "'..."); + StartCoroutine(objectStoreClient.GetObjectStoreListings( + ObjectStoreClient.GetNewGetRequest(builder, "text/plain"), (ObjectStoreSearchResult result) => + { + if (result.results != null && result.results.Length > 0) + { + objectStoreSearchResults = result.results; + PrintLn(objectStoreSearchResults.Length + " result(s).\n"); + PrintLn("To load any of these, use 'osload '.\n"); + PrintLn("To publish any of these, use 'ospublish '.\n\n"); + for (int i = 0; i < objectStoreSearchResults.Length; i++) + { + ObjectStoreEntry entry = objectStoreSearchResults[i]; + PrintLn(string.Format("{0}: '{1}' ({2})", i, entry.title, entry.id)); + } + } + else + { + objectStoreSearchResults = null; + PrintLn("No query results."); + return; + } + })); + } - private void CommandPublish(string[] parts) { - int index; - if (parts.Length != 1) { - PrintLn("Syntax: publish"); - PrintLn("Publishes the current scene"); - return; - } - PeltzerMain.Instance.SaveCurrentModel(publish:true, saveSelected:false); - } + private void CommandOsLoad(string[] parts) + { + int index; + if (parts.Length != 2 || !int.TryParse(parts[1], out index)) + { + PrintLn("Syntax: osload "); + PrintLn(" Loads the given search result (after calling osq)"); + return; + } + if (objectStoreSearchResults == null || index < 0 || index >= objectStoreSearchResults.Length) + { + PrintLn("Invalid search result index. Must be one of the results produced by the osq command."); + return; + } + ObjectStoreClient objectStoreClient = new ObjectStoreClient(); + ObjectStoreEntry entry = objectStoreSearchResults[index]; + PrintLn(string.Format("Loading search result #{0}: {1} (id: {2})...", index, entry.title, entry.id)); + StartCoroutine(objectStoreClient.GetPeltzerFile(entry, (PeltzerFile peltzerFile) => + { + PrintLn("Loaded successfully!"); + PeltzerMain.Instance.CreateNewModel(); + PeltzerMain.Instance.LoadPeltzerFileIntoModel(peltzerFile); + })); + } - private void CommandFlag(string[] parts) { - string syntaxHelp = "Syntax:\n flag list\n flag set {true|false}"; - if (parts.Length < 2) { - PrintLn(syntaxHelp); - return; - } - - Dictionary fields = new Dictionary(); - foreach (FieldInfo fieldInfo in typeof(Features).GetFields(BindingFlags.Static | BindingFlags.Public)) { - // Only get fields that bool and not read-only. - if (fieldInfo.FieldType == typeof(bool) && fieldInfo.MemberType == MemberTypes.Field && - !fieldInfo.IsInitOnly) { - fields[fieldInfo.Name.ToLower()] = fieldInfo; - } - } - - if (parts[1] == "list") { - List keys = new List(fields.Keys); - keys.Sort(); - foreach (string fieldName in keys) { - PrintLn(fields[fieldName].Name + ": " + fields[fieldName].GetValue(null).ToString().ToLower()); - } - } else if (parts[1] == "set") { - if (parts.Length != 4) { - PrintLn(syntaxHelp); - return; - } - string flagName = parts[2]; - - if (!fields.ContainsKey(flagName.ToLower())) { - PrintLn("Unknown flag: " + flagName); - PrintLn("Use 'flag list' to list all flags."); - return; - } - - string flagValueString = parts[3].ToLower(); - bool flagValue; - if (flagValueString == "true") { - flagValue = true; - } else if (flagValueString == "false") { - flagValue = false; - } else { - PrintLn("Flag value must be 'true' or 'false'."); - return; - } - - // Set it. - fields[flagName.ToLower()].SetValue(null, flagValue); - PrintLn("Flag " + flagName + " set to " + flagValue.ToString().ToLower()); - } else { - PrintLn(syntaxHelp); - return; - } - } + private void CommandOsPublish(string[] parts) + { + int index; + if (parts.Length != 2 || !int.TryParse(parts[1], out index)) + { + PrintLn("Syntax: publish "); + PrintLn(" Loads, saves, then opens the publish dialog for the given search result (after calling osq)"); + return; + } + if (objectStoreSearchResults == null || index < 0 || index >= objectStoreSearchResults.Length) + { + PrintLn("Invalid search result index. Must be one of the results produced by the osq command."); + return; + } + ObjectStoreClient objectStoreClient = new ObjectStoreClient(); + ObjectStoreEntry entry = objectStoreSearchResults[index]; + PrintLn(string.Format("Publishing search result #{0}: {1} (id: {2})...", index, entry.title, entry.id)); + StartCoroutine(objectStoreClient.GetPeltzerFile(entry, (PeltzerFile peltzerFile) => + { + PrintLn("Loaded successfully, now trying to save & publish\n."); + PrintLn("If no browser window opens after a minute or so, this might have failed."); + PeltzerMain.Instance.CreateNewModel(); + PeltzerMain.Instance.LoadPeltzerFileIntoModel(peltzerFile); + PeltzerMain.Instance.SaveCurrentModel(publish: true, saveSelected: false); + })); + } - private void CommandRest(string[] parts) { - string helpText = "Syntax:\n" + - " rest clear\n" + - " Clears all restrictions.\n" + - " rest cmode ...\n" + - " Sets the allowed controller modes (modes names are as in the ControllerMode enum)\n"; - if (parts.Length < 2) { - PrintLn(helpText); - return; - } - if (parts[1] == "clear") { - PrintLn("Resetting restrictions."); - PeltzerMain.Instance.restrictionManager.AllowAll(); - } else if (parts[1] == "cmode") { - List allowedModes = new List(); - StringBuilder output = new StringBuilder(); - for (int i = 2; i < parts.Length; i++) { - try { - ControllerMode thisMode = (ControllerMode)Enum.Parse(typeof(ControllerMode), parts[i], - /* ignoreCase */ true); - allowedModes.Add(thisMode); - output.Append(" ").Append(thisMode.ToString()); - } catch (Exception) { - PrintLn("Failed to parse mode: " + parts[i]); - return; - } - } - PeltzerMain.Instance.restrictionManager.SetAllowedControllerModes(allowedModes); - PrintLn("Allowed modes set:" + output); - } else { - PrintLn(helpText); - } - } + private void CommandPublish(string[] parts) + { + int index; + if (parts.Length != 1) + { + PrintLn("Syntax: publish"); + PrintLn("Publishes the current scene"); + return; + } + PeltzerMain.Instance.SaveCurrentModel(publish: true, saveSelected: false); + } - private void CommandTut(string[] parts) { - string help = "Syntax:\n" + - " tut \n" + - " Plays tutorial lesson #number.\n" + - " tut exit\n" + - " Exits the current tutorial."; - if (parts.Length != 2) { - PrintLn(help); - return; - } - if (parts[1] == "exit") { - PrintLn("Exitting tutorial."); - PeltzerMain.Instance.tutorialManager.ExitTutorial(); - return; - } - int tutorialNumber; - if (parts.Length != 2 || !int.TryParse(parts[1], out tutorialNumber)) { - PrintLn(help); - return; - } - PrintLn("Starting tutorial #" + tutorialNumber); - PeltzerMain.Instance.tutorialManager.StartTutorial(tutorialNumber); - } + private void CommandFlag(string[] parts) + { + string syntaxHelp = "Syntax:\n flag list\n flag set {true|false}"; + if (parts.Length < 2) + { + PrintLn(syntaxHelp); + return; + } - private void CommandLoadRes(string[] parts) { - if (parts.Length != 2) { - PrintLn("Syntax: loadres "); - return; - } - PrintLn("Loading model from resource path: " + parts[1] + "..."); - try { - PeltzerMain.Instance.LoadPeltzerFileFromResources(parts[1]); - PrintLn("Loaded successfully."); - } catch (Exception e) { - PrintLn("Load failed (see logs)."); - throw e; - } - } + Dictionary fields = new Dictionary(); + foreach (FieldInfo fieldInfo in typeof(Features).GetFields(BindingFlags.Static | BindingFlags.Public)) + { + // Only get fields that bool and not read-only. + if (fieldInfo.FieldType == typeof(bool) && fieldInfo.MemberType == MemberTypes.Field && + !fieldInfo.IsInitOnly) + { + fields[fieldInfo.Name.ToLower()] = fieldInfo; + } + } - private void CommandSetMid(string[] parts) { - int newId; - PeltzerMain main = PeltzerMain.Instance; - if (parts.Length != 2 || !int.TryParse(parts[1], out newId) || newId <= 0) { - PrintLn("Syntax: setmid "); - PrintLn(" Sets the mesh ID of the selected mesh to the given ID."); - PrintLn(" Exactly one mesh must be selected for this to work."); - PrintLn(" The ID must be a positive integer."); - return; - } - Selector sel = main.GetSelector(); - List meshIds = new List(sel.SelectedOrHoveredMeshes()); - if (meshIds.Count != 1) { - PrintLn("Error: exactly one mesh must be selected."); - return; - } - int oldId = meshIds[0]; - - sel.DeselectAll(); - - // To ensure there are no collisions with the new ID, move the mesh that already had - // the ID newId to something else (if it happens to exist, which would be rare). - ChangeMeshId(newId, main.model.GenerateMeshId()); - // Now move oldId -> newId. - ChangeMeshId(oldId, newId); - - PrintLn("Successfully changed mesh ID " + oldId + " --> " + newId); - } + if (parts[1] == "list") + { + List keys = new List(fields.Keys); + keys.Sort(); + foreach (string fieldName in keys) + { + PrintLn(fields[fieldName].Name + ": " + fields[fieldName].GetValue(null).ToString().ToLower()); + } + } + else if (parts[1] == "set") + { + if (parts.Length != 4) + { + PrintLn(syntaxHelp); + return; + } + string flagName = parts[2]; + + if (!fields.ContainsKey(flagName.ToLower())) + { + PrintLn("Unknown flag: " + flagName); + PrintLn("Use 'flag list' to list all flags."); + return; + } + + string flagValueString = parts[3].ToLower(); + bool flagValue; + if (flagValueString == "true") + { + flagValue = true; + } + else if (flagValueString == "false") + { + flagValue = false; + } + else + { + PrintLn("Flag value must be 'true' or 'false'."); + return; + } + + // Set it. + fields[flagName.ToLower()].SetValue(null, flagValue); + PrintLn("Flag " + flagName + " set to " + flagValue.ToString().ToLower()); + } + else + { + PrintLn(syntaxHelp); + return; + } + } - private void CommandSetMaxUndo(string[] parts) { - int newMaxUndo; - if (parts.Length != 2 || !int.TryParse(parts[1], out newMaxUndo) || newMaxUndo < 5) { - PrintLn("Syntax: setmaxundo "); - PrintLn(" Sets the maximum size of the undo stack - minimum 5"); - return; - } - Model.SetMaxUndoStackSize(newMaxUndo); - } + private void CommandRest(string[] parts) + { + string helpText = "Syntax:\n" + + " rest clear\n" + + " Clears all restrictions.\n" + + " rest cmode ...\n" + + " Sets the allowed controller modes (modes names are as in the ControllerMode enum)\n"; + if (parts.Length < 2) + { + PrintLn(helpText); + return; + } + if (parts[1] == "clear") + { + PrintLn("Resetting restrictions."); + PeltzerMain.Instance.restrictionManager.AllowAll(); + } + else if (parts[1] == "cmode") + { + List allowedModes = new List(); + StringBuilder output = new StringBuilder(); + for (int i = 2; i < parts.Length; i++) + { + try + { + ControllerMode thisMode = (ControllerMode)Enum.Parse(typeof(ControllerMode), parts[i], + /* ignoreCase */ true); + allowedModes.Add(thisMode); + output.Append(" ").Append(thisMode.ToString()); + } + catch (Exception) + { + PrintLn("Failed to parse mode: " + parts[i]); + return; + } + } + PeltzerMain.Instance.restrictionManager.SetAllowedControllerModes(allowedModes); + PrintLn("Allowed modes set:" + output); + } + else + { + PrintLn(helpText); + } + } - private void ChangeMeshId(int oldId, int newId) { - Model model = PeltzerMain.Instance.model; - if (model.HasMesh(oldId)) { - model.AddMesh(model.GetMesh(oldId).CloneWithNewId(newId)); - model.DeleteMesh(oldId); - PeltzerMain.Instance.ModelChangedSinceLastSave = true; - } - } + private void CommandTut(string[] parts) + { + string help = "Syntax:\n" + + " tut \n" + + " Plays tutorial lesson #number.\n" + + " tut exit\n" + + " Exits the current tutorial."; + if (parts.Length != 2) + { + PrintLn(help); + return; + } + if (parts[1] == "exit") + { + PrintLn("Exitting tutorial."); + PeltzerMain.Instance.tutorialManager.ExitTutorial(); + return; + } + int tutorialNumber; + if (parts.Length != 2 || !int.TryParse(parts[1], out tutorialNumber)) + { + PrintLn(help); + return; + } + PrintLn("Starting tutorial #" + tutorialNumber); + PeltzerMain.Instance.tutorialManager.StartTutorial(tutorialNumber); + } - private void CommandSetGid(string[] parts) { - int newGroupId; - PeltzerMain main = PeltzerMain.Instance; - if (parts.Length != 2 || !int.TryParse(parts[1], out newGroupId) || newGroupId <= 0) { - PrintLn("Syntax: setgid "); - PrintLn(" Sets the group ID of the selected group to the given ID."); - PrintLn(" Exactly one group must be selected for this to work (group the meshes first)."); - PrintLn(" The ID must be a positive integer."); - return; - } - Selector sel = main.GetSelector(); - List meshIds = new List(sel.SelectedOrHoveredMeshes()); - if (meshIds.Count < 1) { - PrintLn("Error: nothing is selected. You must select a group."); - return; - } - - // Check that all selected meshes are part of the same group. - int oldGroupId = main.model.GetMesh(meshIds[0]).groupId; - if (oldGroupId == MMesh.GROUP_NONE) { - PrintLn("Error: the selected meshes must be grouped."); - return; - } - foreach (int id in meshIds) { - if (main.model.GetMesh(id).groupId != oldGroupId) { - PrintLn("Error: all selected meshes must belong to the same group."); - return; - } - } - - sel.DeselectAll(); - - // If there is already a group with ID newGroupId, first change its ID to something else. - ChangeGroupId(newGroupId, main.model.GenerateGroupId()); - // Now move oldGroupId -> newGroupId. - ChangeGroupId(oldGroupId, newGroupId); - - PrintLn("Successfully changed group ID " + oldGroupId + " --> " + newGroupId); - } + private void CommandLoadRes(string[] parts) + { + if (parts.Length != 2) + { + PrintLn("Syntax: loadres "); + return; + } + PrintLn("Loading model from resource path: " + parts[1] + "..."); + try + { + PeltzerMain.Instance.LoadPeltzerFileFromResources(parts[1]); + PrintLn("Loaded successfully."); + } + catch (Exception e) + { + PrintLn("Load failed (see logs)."); + throw e; + } + } + + private void CommandSetMid(string[] parts) + { + int newId; + PeltzerMain main = PeltzerMain.Instance; + if (parts.Length != 2 || !int.TryParse(parts[1], out newId) || newId <= 0) + { + PrintLn("Syntax: setmid "); + PrintLn(" Sets the mesh ID of the selected mesh to the given ID."); + PrintLn(" Exactly one mesh must be selected for this to work."); + PrintLn(" The ID must be a positive integer."); + return; + } + Selector sel = main.GetSelector(); + List meshIds = new List(sel.SelectedOrHoveredMeshes()); + if (meshIds.Count != 1) + { + PrintLn("Error: exactly one mesh must be selected."); + return; + } + int oldId = meshIds[0]; - private void ChangeGroupId(int oldGroupId, int newGroupId) { - Model model = PeltzerMain.Instance.model; - foreach (MMesh mesh in model.GetAllMeshes()) { - if (mesh.groupId == oldGroupId) { - model.SetMeshGroup(mesh.id, newGroupId); + sel.DeselectAll(); + + // To ensure there are no collisions with the new ID, move the mesh that already had + // the ID newId to something else (if it happens to exist, which would be rare). + ChangeMeshId(newId, main.model.GenerateMeshId()); + // Now move oldId -> newId. + ChangeMeshId(oldId, newId); + + PrintLn("Successfully changed mesh ID " + oldId + " --> " + newId); } - } - PeltzerMain.Instance.ModelChangedSinceLastSave = true; - } - private void CommandMInfo(string[] parts) { - Model model = PeltzerMain.Instance.model; - Selector selector = PeltzerMain.Instance.GetSelector(); - List meshIds = new List(selector.SelectedOrHoveredMeshes()); - List faceKeys = new List(selector.SelectedOrHoveredFaces()); - List edgeKeys = new List(selector.SelectedOrHoveredEdges()); - List vertexKeys = new List(selector.SelectedOrHoveredVertices()); - - if (meshIds.Count > 0) { - foreach (int meshId in meshIds) { - PrintLn(GetMeshInfo(PeltzerMain.Instance.model.GetMesh(meshId))); - } - } else if (faceKeys.Count > 0) { - foreach (FaceKey faceKey in faceKeys) { - MMesh mesh = model.GetMesh(faceKey.meshId); - PrintLn(GetFaceInfo(mesh, mesh.GetFace(faceKey.faceId))); - } - } else if (edgeKeys.Count > 0) { - foreach (EdgeKey edgeKey in edgeKeys) { - MMesh mesh = model.GetMesh(edgeKey.meshId); - PrintLn(GetEdgeInfo(mesh, edgeKey)); - } - } else if (vertexKeys.Count > 0) { - foreach (VertexKey vertexKey in vertexKeys) { - MMesh mesh = model.GetMesh(vertexKey.meshId); - PrintLn(GetVertexInfo(mesh, vertexKey.vertexId)); - } - } else { - PrintLn("Nothing selected. Model info:\n" + GetModelInfo()); - } - } + private void CommandSetMaxUndo(string[] parts) + { + int newMaxUndo; + if (parts.Length != 2 || !int.TryParse(parts[1], out newMaxUndo) || newMaxUndo < 5) + { + PrintLn("Syntax: setmaxundo "); + PrintLn(" Sets the maximum size of the undo stack - minimum 5"); + return; + } + Model.SetMaxUndoStackSize(newMaxUndo); + } - private string GetMeshInfo(MMesh mesh) { - StringBuilder sb = new StringBuilder() - .AppendFormat("MESH id: {0}", mesh.id).Append("\n") - .AppendFormat(" groupId: {0}", mesh.groupId).Append("\n") - .AppendFormat(" offset: {0}", DebugUtils.Vector3ToString(mesh.offset)).Append("\n") - .AppendFormat(" rotation: {0}", mesh.rotation).Append("\n") - .AppendFormat(" rotation (euler): {0}", mesh.rotation.eulerAngles).Append("\n") - .AppendFormat(" bounds: {0}", DebugUtils.BoundsToString(mesh.bounds)).Append("\n") - .AppendFormat(" #faces: {0}", mesh.faceCount).Append("\n") - .AppendFormat(" #vertices: {0}", mesh.vertexCount).Append("\n") - .AppendFormat(" remix IDs: {0}", - mesh.remixIds != null ? string.Join(",", new List(mesh.remixIds).ToArray()) : "NONE") - .AppendLine(); - - foreach (Face face in mesh.GetFaces()) { - sb.AppendLine(GetFaceInfo(mesh, face)); - } - - foreach (int vertexId in mesh.GetVertexIds()) { - sb.AppendLine(GetVertexInfo(mesh, vertexId)); - } - - return sb.ToString(); - } + private void ChangeMeshId(int oldId, int newId) + { + Model model = PeltzerMain.Instance.model; + if (model.HasMesh(oldId)) + { + model.AddMesh(model.GetMesh(oldId).CloneWithNewId(newId)); + model.DeleteMesh(oldId); + PeltzerMain.Instance.ModelChangedSinceLastSave = true; + } + } - private string GetModelInfo() { - Model model = PeltzerMain.Instance.model; - return new StringBuilder() - .AppendFormat("MODEL").Append("\n") - .AppendFormat(" #meshes: {0}", model.GetAllMeshes().Count).AppendLine() - .AppendFormat(" undo stack size: {0}", model.GetUndoStack().Count).AppendLine() - .AppendFormat(" redo stack size: {0}", model.GetRedoStack().Count).AppendLine() - .AppendFormat(" remix IDs: {0}", - string.Join(",", new List(model.GetAllRemixIds()).ToArray())).AppendLine() - .ToString(); - } + private void CommandSetGid(string[] parts) + { + int newGroupId; + PeltzerMain main = PeltzerMain.Instance; + if (parts.Length != 2 || !int.TryParse(parts[1], out newGroupId) || newGroupId <= 0) + { + PrintLn("Syntax: setgid "); + PrintLn(" Sets the group ID of the selected group to the given ID."); + PrintLn(" Exactly one group must be selected for this to work (group the meshes first)."); + PrintLn(" The ID must be a positive integer."); + return; + } + Selector sel = main.GetSelector(); + List meshIds = new List(sel.SelectedOrHoveredMeshes()); + if (meshIds.Count < 1) + { + PrintLn("Error: nothing is selected. You must select a group."); + return; + } - private static string GetFaceInfo(MMesh mesh, Face face) { - StringBuilder sb = new StringBuilder(); - sb.AppendFormat("FACE {0}, {1} vertices:", face.id, face.vertexIds.Count).AppendLine(); - foreach (int vertexId in face.vertexIds) { - sb.Append(" ").AppendLine(GetVertexInfo(mesh, vertexId)); - } - return sb.ToString(); - } + // Check that all selected meshes are part of the same group. + int oldGroupId = main.model.GetMesh(meshIds[0]).groupId; + if (oldGroupId == MMesh.GROUP_NONE) + { + PrintLn("Error: the selected meshes must be grouped."); + return; + } + foreach (int id in meshIds) + { + if (main.model.GetMesh(id).groupId != oldGroupId) + { + PrintLn("Error: all selected meshes must belong to the same group."); + return; + } + } - private static string GetEdgeInfo(MMesh mesh, EdgeKey edgeKey) { - return new StringBuilder() - .AppendFormat("EDGE {0} - {1}", edgeKey.vertexId1, edgeKey.vertexId2) - .AppendLine() - .Append(" From: ").AppendLine(GetVertexInfo(mesh, edgeKey.vertexId1)) - .Append(" To: ").AppendLine(GetVertexInfo(mesh, edgeKey.vertexId2)) - .ToString(); - } + sel.DeselectAll(); - private static string GetVertexInfo(MMesh mesh, int id) { - return string.Format("VERTEX {0}: {1} (model space: {2})", id, - DebugUtils.Vector3ToString(mesh.VertexPositionInMeshCoords(id)), - DebugUtils.Vector3ToString(mesh.VertexPositionInModelCoords(id))); - } + // If there is already a group with ID newGroupId, first change its ID to something else. + ChangeGroupId(newGroupId, main.model.GenerateGroupId()); + // Now move oldGroupId -> newGroupId. + ChangeGroupId(oldGroupId, newGroupId); + + PrintLn("Successfully changed group ID " + oldGroupId + " --> " + newGroupId); + } + + private void ChangeGroupId(int oldGroupId, int newGroupId) + { + Model model = PeltzerMain.Instance.model; + foreach (MMesh mesh in model.GetAllMeshes()) + { + if (mesh.groupId == oldGroupId) + { + model.SetMeshGroup(mesh.id, newGroupId); + } + } + PeltzerMain.Instance.ModelChangedSinceLastSave = true; + } + + private void CommandMInfo(string[] parts) + { + Model model = PeltzerMain.Instance.model; + Selector selector = PeltzerMain.Instance.GetSelector(); + List meshIds = new List(selector.SelectedOrHoveredMeshes()); + List faceKeys = new List(selector.SelectedOrHoveredFaces()); + List edgeKeys = new List(selector.SelectedOrHoveredEdges()); + List vertexKeys = new List(selector.SelectedOrHoveredVertices()); + + if (meshIds.Count > 0) + { + foreach (int meshId in meshIds) + { + PrintLn(GetMeshInfo(PeltzerMain.Instance.model.GetMesh(meshId))); + } + } + else if (faceKeys.Count > 0) + { + foreach (FaceKey faceKey in faceKeys) + { + MMesh mesh = model.GetMesh(faceKey.meshId); + PrintLn(GetFaceInfo(mesh, mesh.GetFace(faceKey.faceId))); + } + } + else if (edgeKeys.Count > 0) + { + foreach (EdgeKey edgeKey in edgeKeys) + { + MMesh mesh = model.GetMesh(edgeKey.meshId); + PrintLn(GetEdgeInfo(mesh, edgeKey)); + } + } + else if (vertexKeys.Count > 0) + { + foreach (VertexKey vertexKey in vertexKeys) + { + MMesh mesh = model.GetMesh(vertexKey.meshId); + PrintLn(GetVertexInfo(mesh, vertexKey.vertexId)); + } + } + else + { + PrintLn("Nothing selected. Model info:\n" + GetModelInfo()); + } + } - private void CommandDump(string[] unused) { - Debug.Log("=== DEBUG DUMP START ==="); + private string GetMeshInfo(MMesh mesh) + { + StringBuilder sb = new StringBuilder() + .AppendFormat("MESH id: {0}", mesh.id).Append("\n") + .AppendFormat(" groupId: {0}", mesh.groupId).Append("\n") + .AppendFormat(" offset: {0}", DebugUtils.Vector3ToString(mesh.offset)).Append("\n") + .AppendFormat(" rotation: {0}", mesh.rotation).Append("\n") + .AppendFormat(" rotation (euler): {0}", mesh.rotation.eulerAngles).Append("\n") + .AppendFormat(" bounds: {0}", DebugUtils.BoundsToString(mesh.bounds)).Append("\n") + .AppendFormat(" #faces: {0}", mesh.faceCount).Append("\n") + .AppendFormat(" #vertices: {0}", mesh.vertexCount).Append("\n") + .AppendFormat(" remix IDs: {0}", + mesh.remixIds != null ? string.Join(",", new List(mesh.remixIds).ToArray()) : "NONE") + .AppendLine(); + + foreach (Face face in mesh.GetFaces()) + { + sb.AppendLine(GetFaceInfo(mesh, face)); + } - string reportName = string.Format("BlocksDebugReport{0:yyyyMMdd-HHmmss}", DateTime.Now); - string path = Path.Combine(Path.Combine(PeltzerMain.Instance.userPath, "Reports"), reportName); - Directory.CreateDirectory(path); + foreach (int vertexId in mesh.GetVertexIds()) + { + sb.AppendLine(GetVertexInfo(mesh, vertexId)); + } - // Copy current log file to output file path. - string logFilePath = GetLogFilePath(); - File.Copy(logFilePath, Path.Combine(path, "output_log.txt")); + return sb.ToString(); + } - // Save a snapshot of the model to output file path. - File.WriteAllBytes(Path.Combine(path, "model.blocks"), - PeltzerFileHandler.PeltzerFileFromMeshes(PeltzerMain.Instance.model.GetAllMeshes())); + private string GetModelInfo() + { + Model model = PeltzerMain.Instance.model; + return new StringBuilder() + .AppendFormat("MODEL").Append("\n") + .AppendFormat(" #meshes: {0}", model.GetAllMeshes().Count).AppendLine() + .AppendFormat(" undo stack size: {0}", model.GetUndoStack().Count).AppendLine() + .AppendFormat(" redo stack size: {0}", model.GetRedoStack().Count).AppendLine() + .AppendFormat(" remix IDs: {0}", + string.Join(",", new List(model.GetAllRemixIds()).ToArray())).AppendLine() + .ToString(); + } - string modelDumpOutput = PeltzerMain.Instance.model.DebugConsoleDump(); - File.WriteAllBytes(Path.Combine(path, "model.dump"), Encoding.ASCII.GetBytes(modelDumpOutput)); - PrintLn("Debug dump generated: " + path); - } + private static string GetFaceInfo(MMesh mesh, Face face) + { + StringBuilder sb = new StringBuilder(); + sb.AppendFormat("FACE {0}, {1} vertices:", face.id, face.vertexIds.Count).AppendLine(); + foreach (int vertexId in face.vertexIds) + { + sb.Append(" ").AppendLine(GetVertexInfo(mesh, vertexId)); + } + return sb.ToString(); + } + + private static string GetEdgeInfo(MMesh mesh, EdgeKey edgeKey) + { + return new StringBuilder() + .AppendFormat("EDGE {0} - {1}", edgeKey.vertexId1, edgeKey.vertexId2) + .AppendLine() + .Append(" From: ").AppendLine(GetVertexInfo(mesh, edgeKey.vertexId1)) + .Append(" To: ").AppendLine(GetVertexInfo(mesh, edgeKey.vertexId2)) + .ToString(); + } + + private static string GetVertexInfo(MMesh mesh, int id) + { + return string.Format("VERTEX {0}: {1} (model space: {2})", id, + DebugUtils.Vector3ToString(mesh.VertexPositionInMeshCoords(id)), + DebugUtils.Vector3ToString(mesh.VertexPositionInModelCoords(id))); + } + + private void CommandDump(string[] unused) + { + Debug.Log("=== DEBUG DUMP START ==="); + + string reportName = string.Format("BlocksDebugReport{0:yyyyMMdd-HHmmss}", DateTime.Now); + string path = Path.Combine(Path.Combine(PeltzerMain.Instance.userPath, "Reports"), reportName); + Directory.CreateDirectory(path); + + // Copy current log file to output file path. + string logFilePath = GetLogFilePath(); + File.Copy(logFilePath, Path.Combine(path, "output_log.txt")); + + // Save a snapshot of the model to output file path. + File.WriteAllBytes(Path.Combine(path, "model.blocks"), + PeltzerFileHandler.PeltzerFileFromMeshes(PeltzerMain.Instance.model.GetAllMeshes())); + + string modelDumpOutput = PeltzerMain.Instance.model.DebugConsoleDump(); + File.WriteAllBytes(Path.Combine(path, "model.dump"), Encoding.ASCII.GetBytes(modelDumpOutput)); + PrintLn("Debug dump generated: " + path); + } - private static string GetLogFilePath() { + private static string GetLogFilePath() + { #if UNITY_EDITOR string localAppData = Environment.GetEnvironmentVariable("LOCALAPPDATA"); AssertOrThrow.NotNull(localAppData, "LOCALAPPDATA environment variable is not defined."); return localAppData + "\\Unity\\Editor\\Editor.log"; #else - return Application.dataPath + "\\output_log.txt"; + return Application.dataPath + "\\output_log.txt"; #endif - } + } - private void PrintInsertCommandHelp() { - PrintLn("Syntax: insert {cone|cube|cylinder|sphere|torus} [] []"); - PrintLn("Where and are expressed as x,y,z (without any spaces)."); - PrintLn("For example:"); - PrintLn(" insert cube 2.5,3.12,6.5 1.1,2.0,3.0"); - PrintLn(" defaults to 0,0,0 and defaults to 1,1,1."); - } + private void PrintInsertCommandHelp() + { + PrintLn("Syntax: insert {cone|cube|cylinder|sphere|torus} [] []"); + PrintLn("Where and are expressed as x,y,z (without any spaces)."); + PrintLn("For example:"); + PrintLn(" insert cube 2.5,3.12,6.5 1.1,2.0,3.0"); + PrintLn(" defaults to 0,0,0 and defaults to 1,1,1."); + } - private void PrintInsertDurationCommandHelp() { - PrintLn("Syntax: insertduration {time in seconds}"); - PrintLn("For example:"); - PrintLn(" insertduration 0.6"); - } + private void PrintInsertDurationCommandHelp() + { + PrintLn("Syntax: insertduration {time in seconds}"); + PrintLn("For example:"); + PrintLn(" insertduration 0.6"); + } - private void CommandInsert(string[] parts) { - if (parts.Length < 2) { - PrintInsertCommandHelp(); - return; - } - - // Parse the desired primitive type. - Primitives.Shape shape; - try { - shape = (Primitives.Shape)Enum.Parse(typeof(Primitives.Shape), parts[1], /* ignoreCase */ true); - } catch (Exception) { - PrintLn("Error: invalid primitive: " + parts[1]); - PrintInsertCommandHelp(); - return; - } - - Vector3 offset = Vector3.zero; - Vector3 scale = Vector3.one; - - // Parse the offset, if it was provided. - if (parts.Length >= 3 && !TryParseVector3(parts[2], out offset)) { - PrintInsertCommandHelp(); - return; - } - - // Parse the scale, if it was provided. - if (parts.Length >= 4 && !TryParseVector3(parts[3], out scale)) { - PrintInsertCommandHelp(); - return; - } - - int meshId = PeltzerMain.Instance.model.GenerateMeshId(); - MMesh mesh = Primitives.BuildPrimitive(shape, scale, offset, meshId, /* material */ 0); - PeltzerMain.Instance.model.AddMesh(mesh); - - PrintLn(string.Format("Inserted {0} at {1}, scale {2}, mesh ID {3}", shape, offset, scale, meshId)); - } + private void CommandInsert(string[] parts) + { + if (parts.Length < 2) + { + PrintInsertCommandHelp(); + return; + } - private void CommandInsertDuration(string[] parts) { - if (parts.Length != 2) { - PrintInsertDurationCommandHelp(); - return; - } + // Parse the desired primitive type. + Primitives.Shape shape; + try + { + shape = (Primitives.Shape)Enum.Parse(typeof(Primitives.Shape), parts[1], /* ignoreCase */ true); + } + catch (Exception) + { + PrintLn("Error: invalid primitive: " + parts[1]); + PrintInsertCommandHelp(); + return; + } - float newDuration; - if (!float.TryParse(parts[1], out newDuration)) { - PrintInsertDurationCommandHelp(); - return; - } + Vector3 offset = Vector3.zero; + Vector3 scale = Vector3.one; - MeshInsertEffect.DURATION_BASE = newDuration; + // Parse the offset, if it was provided. + if (parts.Length >= 3 && !TryParseVector3(parts[2], out offset)) + { + PrintInsertCommandHelp(); + return; + } - PrintLn(string.Format("Updated insert duration to {0}", newDuration)); - } + // Parse the scale, if it was provided. + if (parts.Length >= 4 && !TryParseVector3(parts[3], out scale)) + { + PrintInsertCommandHelp(); + return; + } - private void CommandMoveV(string[] parts) { - Vector3 delta; - if (parts.Length != 2 || !TryParseVector3(parts[1], out delta)) { - PrintLn("Syntax: move ,,"); - PrintLn(" Moves the selected vertices by the given delta in model space."); - PrintLn(""); - PrintLn(" IMPORTANT: do not use spaces between the coordinates."); - PrintLn(" Example: move 1.5,2.0,-3.1"); - return; - } - List updatedVerts = new List(); - int meshId = -1; - MMesh original = null; - foreach (VertexKey vkey in PeltzerMain.Instance.GetSelector().SelectedOrHoveredVertices()) { - if (meshId < 0) { - meshId = vkey.meshId; - original = PeltzerMain.Instance.model.GetMesh(meshId); - } else if (meshId != vkey.meshId) { - PrintLn("Selected vertices must belong to same mesh."); - return; - } - updatedVerts.Add(new Vertex(vkey.vertexId, original.VertexPositionInMeshCoords(vkey.vertexId) + delta)); - } - if (meshId < 0) { - PrintLn("No vertices selected."); - return; - } - - MMesh clone = original.Clone(); - if (!MeshFixer.MoveVerticesAndMutateMeshAndFix(original, clone, updatedVerts, /* forPreview */ false)) { - PrintLn("Failed to move vertices. Resulting mesh was invalid."); - return; - } - - PeltzerMain.Instance.model.ApplyCommand(new ReplaceMeshCommand(meshId, clone)); - PrintLn(string.Format("Mesh {0} successfully modified ({1} vertices displaced by {2})", - meshId, updatedVerts.Count, delta)); - } + int meshId = PeltzerMain.Instance.model.GenerateMeshId(); + MMesh mesh = Primitives.BuildPrimitive(shape, scale, offset, meshId, /* material */ 0); + PeltzerMain.Instance.model.AddMesh(mesh); - // Parses a string like "1.1,2.2,3.3" into a Vector3. - private static bool TryParseVector3(string s, out Vector3 result) { - result = Vector3.zero; - string[] coords = s.Split(','); - return (coords.Length == 3) && - float.TryParse(coords[0], out result.x) && - float.TryParse(coords[1], out result.y) && - float.TryParse(coords[2], out result.z); - } + PrintLn(string.Format("Inserted {0} at {1}, scale {2}, mesh ID {3}", shape, offset, scale, meshId)); + } - private void CommandEnv(string[] parts) { - string helpText = "env {reset|white|black|r,g,b}\n" + - " Sets/resets the environment (background).\n" + - " r,g,b must be in floating point with no spaces, example: 1.0,0.5,0.5\n"; - if (parts.Length < 2) { - PrintLn(helpText); - return; - } - - GameObject envObj = ObjectFinder.ObjectById("ID_Environment"); - GameObject terrain = ObjectFinder.ObjectById("ID_TerrainLift"); - GameObject terrainNoMountains = ObjectFinder.ObjectById("ID_TerrainNoMountains"); - Color bgColor; - Vector3 colorV; - - if (parts[1] == "reset") { - if (originalSkybox != null) { - RenderSettings.skybox = originalSkybox; - } - envObj.SetActive(true); - terrain.SetActive(true); - terrainNoMountains.SetActive(true); - PrintLn("Environment reset."); - return; - } else if (parts[1] == "white") { - bgColor = Color.white; - } else if (parts[1] == "black") { - bgColor = Color.black; - } else if (TryParseVector3(parts[1], out colorV)) { - bgColor = new Color(colorV.x, colorV.y, colorV.z); - } else { - PrintLn(helpText); - return; - } - - if (originalSkybox == null) { - originalSkybox = RenderSettings.skybox; - } - RenderSettings.skybox = new Material(Resources.Load("Materials/UnlitWhite")); - envObj.SetActive(false); - terrain.SetActive(false); - terrainNoMountains.SetActive(false); - RenderSettings.skybox.color = bgColor; - PrintLn("Environment color set to " + bgColor); - } + private void CommandInsertDuration(string[] parts) + { + if (parts.Length != 2) + { + PrintInsertDurationCommandHelp(); + return; + } - private void CommandLoadFile(string[] parts) { - if (parts.Length != 2) { - PrintLn("Syntax: loadfile "); - return; - } - string filePath = parts[1]; - if (!File.Exists(filePath)) { - PrintLn("Error: file does not exist: " + filePath); - return; - } - PrintLn("Loading model from file path: " + filePath + "..."); - try { - PeltzerFile peltzerFile; - byte[] fileBytes = File.ReadAllBytes(filePath); - if (!PeltzerFileHandler.PeltzerFileFromBytes(fileBytes, out peltzerFile)) { - PrintLn("Failed to load. Bad format?"); - return; - } - PeltzerMain.Instance.LoadPeltzerFileIntoModel(peltzerFile); - PrintLn("Loaded successfully: " + filePath); - } catch (Exception e) { - PrintLn("Load failed (see logs)."); - throw e; - } - } + float newDuration; + if (!float.TryParse(parts[1], out newDuration)) + { + PrintInsertDurationCommandHelp(); + return; + } - private void CommandSaveFile(string[] parts) { - if (parts.Length != 2) { - PrintLn("Syntax: savefile "); - return; - } - string filePath = parts[1]; - PrintLn("Saving model to file path: " + filePath + "..."); - try { - File.WriteAllBytes(filePath, PeltzerFileHandler.PeltzerFileFromMeshes(PeltzerMain.Instance.model.GetAllMeshes())); - PrintLn("Saved successfully: " + filePath); - } catch (Exception e) { - PrintLn("Save failed (see logs)."); - throw e; - } - } + MeshInsertEffect.DURATION_BASE = newDuration; + + PrintLn(string.Format("Updated insert duration to {0}", newDuration)); + } + + private void CommandMoveV(string[] parts) + { + Vector3 delta; + if (parts.Length != 2 || !TryParseVector3(parts[1], out delta)) + { + PrintLn("Syntax: move ,,"); + PrintLn(" Moves the selected vertices by the given delta in model space."); + PrintLn(""); + PrintLn(" IMPORTANT: do not use spaces between the coordinates."); + PrintLn(" Example: move 1.5,2.0,-3.1"); + return; + } + List updatedVerts = new List(); + int meshId = -1; + MMesh original = null; + foreach (VertexKey vkey in PeltzerMain.Instance.GetSelector().SelectedOrHoveredVertices()) + { + if (meshId < 0) + { + meshId = vkey.meshId; + original = PeltzerMain.Instance.model.GetMesh(meshId); + } + else if (meshId != vkey.meshId) + { + PrintLn("Selected vertices must belong to same mesh."); + return; + } + updatedVerts.Add(new Vertex(vkey.vertexId, original.VertexPositionInMeshCoords(vkey.vertexId) + delta)); + } + if (meshId < 0) + { + PrintLn("No vertices selected."); + return; + } + + MMesh clone = original.Clone(); + if (!MeshFixer.MoveVerticesAndMutateMeshAndFix(original, clone, updatedVerts, /* forPreview */ false)) + { + PrintLn("Failed to move vertices. Resulting mesh was invalid."); + return; + } + + PeltzerMain.Instance.model.ApplyCommand(new ReplaceMeshCommand(meshId, clone)); + PrintLn(string.Format("Mesh {0} successfully modified ({1} vertices displaced by {2})", + meshId, updatedVerts.Count, delta)); + } + + // Parses a string like "1.1,2.2,3.3" into a Vector3. + private static bool TryParseVector3(string s, out Vector3 result) + { + result = Vector3.zero; + string[] coords = s.Split(','); + return (coords.Length == 3) && + float.TryParse(coords[0], out result.x) && + float.TryParse(coords[1], out result.y) && + float.TryParse(coords[2], out result.z); + } + + private void CommandEnv(string[] parts) + { + string helpText = "env {reset|white|black|r,g,b}\n" + + " Sets/resets the environment (background).\n" + + " r,g,b must be in floating point with no spaces, example: 1.0,0.5,0.5\n"; + if (parts.Length < 2) + { + PrintLn(helpText); + return; + } + + GameObject envObj = ObjectFinder.ObjectById("ID_Environment"); + GameObject terrain = ObjectFinder.ObjectById("ID_TerrainLift"); + GameObject terrainNoMountains = ObjectFinder.ObjectById("ID_TerrainNoMountains"); + Color bgColor; + Vector3 colorV; + + if (parts[1] == "reset") + { + if (originalSkybox != null) + { + RenderSettings.skybox = originalSkybox; + } + envObj.SetActive(true); + terrain.SetActive(true); + terrainNoMountains.SetActive(true); + PrintLn("Environment reset."); + return; + } + else if (parts[1] == "white") + { + bgColor = Color.white; + } + else if (parts[1] == "black") + { + bgColor = Color.black; + } + else if (TryParseVector3(parts[1], out colorV)) + { + bgColor = new Color(colorV.x, colorV.y, colorV.z); + } + else + { + PrintLn(helpText); + return; + } - private void CommandFuse(string[] parts) { - HashSet meshIds = new HashSet(PeltzerMain.Instance.GetSelector().selectedMeshes); - if (meshIds.Count < 2) { - PrintLn("Select at least 2 meshes to fuse."); - return; - } - List meshes = new List(); - foreach (int meshId in meshIds) { - meshes.Add(PeltzerMain.Instance.model.GetMesh(meshId)); - } - int newId = PeltzerMain.Instance.model.GenerateMeshId(); - PeltzerMain.Instance.model.AddMesh(Fuser.FuseMeshes(meshes, newId)); - - PeltzerMain.Instance.GetSelector().DeselectAll(); - foreach (int meshId in meshIds) { - PeltzerMain.Instance.model.DeleteMesh(meshId); - } - - PrintLn(string.Format("Created fused mesh from {0} meshes.", meshIds.Count)); + if (originalSkybox == null) + { + originalSkybox = RenderSettings.skybox; + } + RenderSettings.skybox = new Material(Resources.Load("Materials/UnlitWhite")); + envObj.SetActive(false); + terrain.SetActive(false); + terrainNoMountains.SetActive(false); + RenderSettings.skybox.color = bgColor; + PrintLn("Environment color set to " + bgColor); + } + + private void CommandLoadFile(string[] parts) + { + if (parts.Length != 2) + { + PrintLn("Syntax: loadfile "); + return; + } + string filePath = parts[1]; + if (!File.Exists(filePath)) + { + PrintLn("Error: file does not exist: " + filePath); + return; + } + PrintLn("Loading model from file path: " + filePath + "..."); + try + { + PeltzerFile peltzerFile; + byte[] fileBytes = File.ReadAllBytes(filePath); + if (!PeltzerFileHandler.PeltzerFileFromBytes(fileBytes, out peltzerFile)) + { + PrintLn("Failed to load. Bad format?"); + return; + } + PeltzerMain.Instance.LoadPeltzerFileIntoModel(peltzerFile); + PrintLn("Loaded successfully: " + filePath); + } + catch (Exception e) + { + PrintLn("Load failed (see logs)."); + throw e; + } + } + + private void CommandSaveFile(string[] parts) + { + if (parts.Length != 2) + { + PrintLn("Syntax: savefile "); + return; + } + string filePath = parts[1]; + PrintLn("Saving model to file path: " + filePath + "..."); + try + { + File.WriteAllBytes(filePath, PeltzerFileHandler.PeltzerFileFromMeshes(PeltzerMain.Instance.model.GetAllMeshes())); + PrintLn("Saved successfully: " + filePath); + } + catch (Exception e) + { + PrintLn("Save failed (see logs)."); + throw e; + } + } + + private void CommandFuse(string[] parts) + { + HashSet meshIds = new HashSet(PeltzerMain.Instance.GetSelector().selectedMeshes); + if (meshIds.Count < 2) + { + PrintLn("Select at least 2 meshes to fuse."); + return; + } + List meshes = new List(); + foreach (int meshId in meshIds) + { + meshes.Add(PeltzerMain.Instance.model.GetMesh(meshId)); + } + int newId = PeltzerMain.Instance.model.GenerateMeshId(); + PeltzerMain.Instance.model.AddMesh(Fuser.FuseMeshes(meshes, newId)); + + PeltzerMain.Instance.GetSelector().DeselectAll(); + foreach (int meshId in meshIds) + { + PeltzerMain.Instance.model.DeleteMesh(meshId); + } + + PrintLn(string.Format("Created fused mesh from {0} meshes.", meshIds.Count)); + } } - } } diff --git a/Assets/Scripts/desktop_app/DesktopMain.cs b/Assets/Scripts/desktop_app/DesktopMain.cs index 80350bd1..40e29f97 100644 --- a/Assets/Scripts/desktop_app/DesktopMain.cs +++ b/Assets/Scripts/desktop_app/DesktopMain.cs @@ -18,272 +18,313 @@ using UnityEngine; using UnityEngine.UI; -namespace com.google.apps.peltzer.client.desktop_app { - /// - /// Establishes the desktop app. - /// - public class DesktopMain : MonoBehaviour { +namespace com.google.apps.peltzer.client.desktop_app +{ /// - /// A default string to show as the user's name if the Plus Client fails to get their actual name. + /// Establishes the desktop app. /// - private const string DEFAULT_DISPLAY_NAME = "Blocks User"; - /// - /// Whether the user has signed in. - /// - private bool isSignedIn; - /// - /// The GameObject for the add reference image button. - /// - private GameObject addReferenceButton; - /// - /// The GameObject for the sign in button. - /// - private GameObject signInButton; - /// - /// The GameObject for the menu. - /// - private GameObject menu; - /// - /// The GameObject for the menu button. - /// - private GameObject menuButton; - /// - /// The GameObject for the "Your Models" button on the menu. - /// - private GameObject menuYourModelsButton; - /// - /// The GameObject for the about poly button on the menu. - /// - private GameObject menuAboutPolyButton; - /// - /// The GameObject for the sign out button on the menu. - /// - private GameObject menuSignOutButton; - /// - /// The sprite displaying the menu icon which is the user's avatar. - /// - private Sprite defaultMenuIcon; - /// - /// The image where the users avatar or default icon is shown. - /// - private Image avatarImage; - /// - /// The text where the users name or "Sign In" is shown. - /// - private Text displayNameOrPrompt; - /// - /// The default message to sign in. This is replaced with the user display name on signIn. - /// - private string defaultSignInPrompt; - - /// - /// URL user is taken to when selecting 'About Blocks' from menu - /// - private const string ABOUT_BLOCKS_URL = "https://vr.google.com/blocks"; - - public void Setup() { - SetupSigninButton(); - SetupReferenceImage(); - - SetupMenu(); - // The menu has to be active on start to set it up. Now that it is setup disable it. - if (menu != null) { - menu.SetActive(false); - } - - // Relies on menu, must be called after SetupMenu(). - SetupMenuButton(); - } - - /// - /// Setup the sign in button. - /// - private void SetupSigninButton() { - signInButton = transform.Find("Header/User/sign_in").gameObject; - - if (signInButton != null) { - displayNameOrPrompt = signInButton.GetComponent(); - defaultSignInPrompt = signInButton.GetComponent().text; - - HoverableButton signInButtonHoverable = signInButton.AddComponent(); - signInButtonHoverable.SetOnClickAction(() => { - CloseMenu(); - // OnClick of hoverable button start the authentication process. - PeltzerMain.Instance.InvokeMenuAction(MenuAction.SIGN_IN); - }); - } - } - - /// - /// Reset the sign in button and updates the menu when a user logs out. - /// - public void SignOut() { - if (signInButton == null) - return; - - // Replace the users name with the sign in text. - displayNameOrPrompt.text = defaultSignInPrompt; - - // Replace the users avatar with the default. - if (menuButton != null) { - avatarImage.sprite = defaultMenuIcon; - avatarImage.color = new Color(0f, 0f, 0f, 220/255f); - } - - if (signInButton.GetComponent() == null) { - // Re-add the hoverable button that was removed when the user signed in. - HoverableButton signInButtonHoverable = signInButton.AddComponent(); - signInButtonHoverable.SetOnClickAction(() => { - CloseMenu(); - // OnClick of hoverable button start the authentication process. - PeltzerMain.Instance.InvokeMenuAction(MenuAction.SIGN_IN); - }); - } else { - signInButton.transform.Find("hover").gameObject.SetActive(false); - } - - // Modify the menu if it is open to not show the sign out button. - if (menu.activeInHierarchy && menuSignOutButton != null) { - menuSignOutButton.SetActive(false); - } - - isSignedIn = false; - } + public class DesktopMain : MonoBehaviour + { + /// + /// A default string to show as the user's name if the Plus Client fails to get their actual name. + /// + private const string DEFAULT_DISPLAY_NAME = "Blocks User"; + /// + /// Whether the user has signed in. + /// + private bool isSignedIn; + /// + /// The GameObject for the add reference image button. + /// + private GameObject addReferenceButton; + /// + /// The GameObject for the sign in button. + /// + private GameObject signInButton; + /// + /// The GameObject for the menu. + /// + private GameObject menu; + /// + /// The GameObject for the menu button. + /// + private GameObject menuButton; + /// + /// The GameObject for the "Your Models" button on the menu. + /// + private GameObject menuYourModelsButton; + /// + /// The GameObject for the about poly button on the menu. + /// + private GameObject menuAboutPolyButton; + /// + /// The GameObject for the sign out button on the menu. + /// + private GameObject menuSignOutButton; + /// + /// The sprite displaying the menu icon which is the user's avatar. + /// + private Sprite defaultMenuIcon; + /// + /// The image where the users avatar or default icon is shown. + /// + private Image avatarImage; + /// + /// The text where the users name or "Sign In" is shown. + /// + private Text displayNameOrPrompt; + /// + /// The default message to sign in. This is replaced with the user display name on signIn. + /// + private string defaultSignInPrompt; + + /// + /// URL user is taken to when selecting 'About Blocks' from menu + /// + private const string ABOUT_BLOCKS_URL = "https://vr.google.com/blocks"; + + public void Setup() + { + SetupSigninButton(); + SetupReferenceImage(); + + SetupMenu(); + // The menu has to be active on start to set it up. Now that it is setup disable it. + if (menu != null) + { + menu.SetActive(false); + } + + // Relies on menu, must be called after SetupMenu(). + SetupMenuButton(); + } - /// - /// Hides the sign in button and updates the menu when the user logs in. - /// - /// The user's avatar to display in the UI. - /// The user's name to display in the UI. - public void SignIn(Sprite avatarIcon, string displayName) { - if (signInButton != null) { - // Disabled the hover object and delete the HoverableButton component. - signInButton.transform.Find("hover").gameObject.SetActive(false); - Destroy(signInButton.GetComponent()); - } - - // Modify the menu if it is open to show the sign out button. - if (menu.activeInHierarchy && menuSignOutButton != null) { - menuSignOutButton.SetActive(true); - } - - // Change the UI elements to display the user's icon and name. - displayNameOrPrompt.text = displayName != null ? displayName : DEFAULT_DISPLAY_NAME; - avatarImage.sprite = avatarIcon != null ? avatarIcon : defaultMenuIcon; - avatarImage.color = avatarIcon != null ? Color.white : new Color(0f, 0f, 0f, 220 / 255f); - - isSignedIn = true; - } + /// + /// Setup the sign in button. + /// + private void SetupSigninButton() + { + signInButton = transform.Find("Header/User/sign_in").gameObject; + + if (signInButton != null) + { + displayNameOrPrompt = signInButton.GetComponent(); + defaultSignInPrompt = signInButton.GetComponent().text; + + HoverableButton signInButtonHoverable = signInButton.AddComponent(); + signInButtonHoverable.SetOnClickAction(() => + { + CloseMenu(); + // OnClick of hoverable button start the authentication process. + PeltzerMain.Instance.InvokeMenuAction(MenuAction.SIGN_IN); + }); + } + } - /// - /// Setup the add reference image button. - /// - private void SetupReferenceImage() { - addReferenceButton = transform.Find("Header/AddImage").gameObject; - - if (addReferenceButton != null) { - HoverableButton addReferenceButtonHoverable = addReferenceButton.AddComponent(); - addReferenceButtonHoverable.SetOnClickAction(() => { - if (PeltzerMain.Instance.GetPreviewController() != null) { - // OnClick of hoverable button start open the image dialog. - PeltzerMain.Instance.GetPreviewController().SelectPreviewImage(); - CloseMenu(); - } - }); - } - } + /// + /// Reset the sign in button and updates the menu when a user logs out. + /// + public void SignOut() + { + if (signInButton == null) + return; + + // Replace the users name with the sign in text. + displayNameOrPrompt.text = defaultSignInPrompt; + + // Replace the users avatar with the default. + if (menuButton != null) + { + avatarImage.sprite = defaultMenuIcon; + avatarImage.color = new Color(0f, 0f, 0f, 220 / 255f); + } + + if (signInButton.GetComponent() == null) + { + // Re-add the hoverable button that was removed when the user signed in. + HoverableButton signInButtonHoverable = signInButton.AddComponent(); + signInButtonHoverable.SetOnClickAction(() => + { + CloseMenu(); + // OnClick of hoverable button start the authentication process. + PeltzerMain.Instance.InvokeMenuAction(MenuAction.SIGN_IN); + }); + } + else + { + signInButton.transform.Find("hover").gameObject.SetActive(false); + } + + // Modify the menu if it is open to not show the sign out button. + if (menu.activeInHierarchy && menuSignOutButton != null) + { + menuSignOutButton.SetActive(false); + } + + isSignedIn = false; + } - /// - /// Setup the clickable menu button. - /// - private void SetupMenuButton() { - menuButton = transform.Find("Header/User/avatar_menu").gameObject; - - - // Find the default menu icon that is displayed when the user is not signed in. - if (menuButton != null) { - avatarImage = menuButton.GetComponent(); - defaultMenuIcon = menuButton.GetComponent().sprite; - } - - if (menuButton != null) { - HoverableButton menuButtonHoverable = menuButton.AddComponent(); - menuButtonHoverable.SetOnClickAction(() => { - // OnClick of hoverable button toggle the menu open and close. - ToggleMenu(); - }); - } - } + /// + /// Hides the sign in button and updates the menu when the user logs in. + /// + /// The user's avatar to display in the UI. + /// The user's name to display in the UI. + public void SignIn(Sprite avatarIcon, string displayName) + { + if (signInButton != null) + { + // Disabled the hover object and delete the HoverableButton component. + signInButton.transform.Find("hover").gameObject.SetActive(false); + Destroy(signInButton.GetComponent()); + } + + // Modify the menu if it is open to show the sign out button. + if (menu.activeInHierarchy && menuSignOutButton != null) + { + menuSignOutButton.SetActive(true); + } + + // Change the UI elements to display the user's icon and name. + displayNameOrPrompt.text = displayName != null ? displayName : DEFAULT_DISPLAY_NAME; + avatarImage.sprite = avatarIcon != null ? avatarIcon : defaultMenuIcon; + avatarImage.color = avatarIcon != null ? Color.white : new Color(0f, 0f, 0f, 220 / 255f); + + isSignedIn = true; + } - /// - /// Setup the menu. - /// - private void SetupMenu() { - menu = transform.Find("Header/Menu").gameObject; - - // Setup each button on the menu. - if (menu != null) { - menuYourModelsButton = menu.transform.Find("your_models_button").gameObject; - if (menuYourModelsButton != null) { - HoverableButton menuFeedbackHoverable = menuYourModelsButton.AddComponent(); - menuFeedbackHoverable.SetOnClickAction(() => { - // OnClick of Poly Library hoverable button opens the Your Models URL in the web browser. - Application.OpenURL(AssetsServiceClient.SaveUrl()); - CloseMenu(); - }); + /// + /// Setup the add reference image button. + /// + private void SetupReferenceImage() + { + addReferenceButton = transform.Find("Header/AddImage").gameObject; + + if (addReferenceButton != null) + { + HoverableButton addReferenceButtonHoverable = addReferenceButton.AddComponent(); + addReferenceButtonHoverable.SetOnClickAction(() => + { + if (PeltzerMain.Instance.GetPreviewController() != null) + { + // OnClick of hoverable button start open the image dialog. + PeltzerMain.Instance.GetPreviewController().SelectPreviewImage(); + CloseMenu(); + } + }); + } } - menuAboutPolyButton = menu.transform.Find("about_poly_button").gameObject; - if (menuAboutPolyButton != null) { - HoverableButton menuAboutPolyHoverable = menuAboutPolyButton.AddComponent(); - menuAboutPolyHoverable.SetOnClickAction(() => { - // OnClick of Poly Library hoverable button opens the "About Poly" page in the web browser. - Application.OpenURL(ABOUT_BLOCKS_URL); - CloseMenu(); - }); + /// + /// Setup the clickable menu button. + /// + private void SetupMenuButton() + { + menuButton = transform.Find("Header/User/avatar_menu").gameObject; + + + // Find the default menu icon that is displayed when the user is not signed in. + if (menuButton != null) + { + avatarImage = menuButton.GetComponent(); + defaultMenuIcon = menuButton.GetComponent().sprite; + } + + if (menuButton != null) + { + HoverableButton menuButtonHoverable = menuButton.AddComponent(); + menuButtonHoverable.SetOnClickAction(() => + { + // OnClick of hoverable button toggle the menu open and close. + ToggleMenu(); + }); + } } - menuSignOutButton = menu.transform.Find("sign_out_button").gameObject; - if (menuSignOutButton != null) { - HoverableButton menuSignOutHoverable = menuSignOutButton.AddComponent(); - menuSignOutHoverable.SetOnClickAction(() => { - CloseMenu(); - // OnClick of sign out hoverable deauthenticate and reset the user name and avatar. - PeltzerMain.Instance.InvokeMenuAction(MenuAction.SIGN_OUT); - }); + /// + /// Setup the menu. + /// + private void SetupMenu() + { + menu = transform.Find("Header/Menu").gameObject; + + // Setup each button on the menu. + if (menu != null) + { + menuYourModelsButton = menu.transform.Find("your_models_button").gameObject; + if (menuYourModelsButton != null) + { + HoverableButton menuFeedbackHoverable = menuYourModelsButton.AddComponent(); + menuFeedbackHoverable.SetOnClickAction(() => + { + // OnClick of Poly Library hoverable button opens the Your Models URL in the web browser. + Application.OpenURL(AssetsServiceClient.SaveUrl()); + CloseMenu(); + }); + } + + menuAboutPolyButton = menu.transform.Find("about_poly_button").gameObject; + if (menuAboutPolyButton != null) + { + HoverableButton menuAboutPolyHoverable = menuAboutPolyButton.AddComponent(); + menuAboutPolyHoverable.SetOnClickAction(() => + { + // OnClick of Poly Library hoverable button opens the "About Poly" page in the web browser. + Application.OpenURL(ABOUT_BLOCKS_URL); + CloseMenu(); + }); + } + + menuSignOutButton = menu.transform.Find("sign_out_button").gameObject; + if (menuSignOutButton != null) + { + HoverableButton menuSignOutHoverable = menuSignOutButton.AddComponent(); + menuSignOutHoverable.SetOnClickAction(() => + { + CloseMenu(); + // OnClick of sign out hoverable deauthenticate and reset the user name and avatar. + PeltzerMain.Instance.InvokeMenuAction(MenuAction.SIGN_OUT); + }); + } + } } - } - } - /// - /// Closes the menu. - /// - public void CloseMenu() { - if (menu == null) { - return; - } - - if (menu.activeInHierarchy) { - menu.SetActive(false); - } - } + /// + /// Closes the menu. + /// + public void CloseMenu() + { + if (menu == null) + { + return; + } + + if (menu.activeInHierarchy) + { + menu.SetActive(false); + } + } - /// - /// Opens and closes the menu. - /// - private void ToggleMenu() { - if (menu == null) { - return; - } - - // Close the menu. - if (menu.activeInHierarchy) { - menu.SetActive(false); - } else { - menu.SetActive(true); - // Show the sign out button if the user is signed in. - menuSignOutButton.SetActive(isSignedIn); - } + /// + /// Opens and closes the menu. + /// + private void ToggleMenu() + { + if (menu == null) + { + return; + } + + // Close the menu. + if (menu.activeInHierarchy) + { + menu.SetActive(false); + } + else + { + menu.SetActive(true); + // Show the sign out button if the user is signed in. + menuSignOutButton.SetActive(isSignedIn); + } + } } - } } diff --git a/Assets/Scripts/desktop_app/HeaderOptionsController.cs b/Assets/Scripts/desktop_app/HeaderOptionsController.cs index 3c25214f..289fa22a 100644 --- a/Assets/Scripts/desktop_app/HeaderOptionsController.cs +++ b/Assets/Scripts/desktop_app/HeaderOptionsController.cs @@ -18,36 +18,45 @@ using com.google.apps.peltzer.client.model.export; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.desktop_app { - public class HeaderOptionsController : MonoBehaviour { - - private Rect localBounds; - - void Start() { - HoverableButton signOut = transform.Find("SignOutOption").gameObject.AddComponent(); - signOut.SetOnClickAction(() => { - PeltzerMain.Instance.InvokeMenuAction(MenuAction.SIGN_OUT); - }); - } - - void LateUpdate() { - if (Input.GetMouseButtonDown(0) && !localBounds.Contains(Input.mousePosition)) { - Close(); - } - } - - void OnEnable() { - Vector2 size = GetComponent().sizeDelta; - Vector2 position = GetComponent().position; - localBounds = new Rect(position.x - size.x / 2.0f, position.y - size.y / 2.0f, size.x, size.y); - } - - public void Open() { - gameObject.SetActive(true); - } - - public void Close() { - gameObject.SetActive(false); +namespace com.google.apps.peltzer.client.desktop_app +{ + public class HeaderOptionsController : MonoBehaviour + { + + private Rect localBounds; + + void Start() + { + HoverableButton signOut = transform.Find("SignOutOption").gameObject.AddComponent(); + signOut.SetOnClickAction(() => + { + PeltzerMain.Instance.InvokeMenuAction(MenuAction.SIGN_OUT); + }); + } + + void LateUpdate() + { + if (Input.GetMouseButtonDown(0) && !localBounds.Contains(Input.mousePosition)) + { + Close(); + } + } + + void OnEnable() + { + Vector2 size = GetComponent().sizeDelta; + Vector2 position = GetComponent().position; + localBounds = new Rect(position.x - size.x / 2.0f, position.y - size.y / 2.0f, size.x, size.y); + } + + public void Open() + { + gameObject.SetActive(true); + } + + public void Close() + { + gameObject.SetActive(false); + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/desktop_app/HoverableButton.cs b/Assets/Scripts/desktop_app/HoverableButton.cs index 19250020..f3517386 100644 --- a/Assets/Scripts/desktop_app/HoverableButton.cs +++ b/Assets/Scripts/desktop_app/HoverableButton.cs @@ -16,43 +16,53 @@ using UnityEngine.UI; using UnityEngine.EventSystems; -namespace com.google.apps.peltzer.client.desktop_app { - /// - /// A button with a background component called 'hover' that has an associated action. - /// - public class HoverableButton : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler, - IPointerExitHandler, IPointerDownHandler, IPointerUpHandler { - private GameObject hover; - private System.Action onClick; - - void Awake() { - hover = transform.Find("hover").gameObject; - hover.SetActive(false); - } +namespace com.google.apps.peltzer.client.desktop_app +{ + /// + /// A button with a background component called 'hover' that has an associated action. + /// + public class HoverableButton : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler, + IPointerExitHandler, IPointerDownHandler, IPointerUpHandler + { + private GameObject hover; + private System.Action onClick; - public void OnPointerClick(PointerEventData eventData) { - if (onClick != null) { - onClick(); - hover.SetActive(false); - } - } + void Awake() + { + hover = transform.Find("hover").gameObject; + hover.SetActive(false); + } - public void OnPointerEnter(PointerEventData eventData) { - hover.SetActive(true); - } + public void OnPointerClick(PointerEventData eventData) + { + if (onClick != null) + { + onClick(); + hover.SetActive(false); + } + } - public void OnPointerExit(PointerEventData eventData) { - hover.SetActive(false); - } + public void OnPointerEnter(PointerEventData eventData) + { + hover.SetActive(true); + } - public void OnPointerDown(PointerEventData eventData) { - } + public void OnPointerExit(PointerEventData eventData) + { + hover.SetActive(false); + } - public void OnPointerUp(PointerEventData eventData) { - } + public void OnPointerDown(PointerEventData eventData) + { + } + + public void OnPointerUp(PointerEventData eventData) + { + } - public void SetOnClickAction(System.Action onClick) { - this.onClick = onClick; + public void SetOnClickAction(System.Action onClick) + { + this.onClick = onClick; + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/desktop_app/MoveableReferenceImage.cs b/Assets/Scripts/desktop_app/MoveableReferenceImage.cs index 17778669..b3382ecf 100644 --- a/Assets/Scripts/desktop_app/MoveableReferenceImage.cs +++ b/Assets/Scripts/desktop_app/MoveableReferenceImage.cs @@ -22,211 +22,223 @@ using com.google.apps.peltzer.client.tools.utils; using com.google.apps.peltzer.client.tools; -namespace com.google.apps.peltzer.client.desktop_app { - /// - /// A reference image is an image that's on the scene to help the user create their model. The user creates these - /// by clicking the Add Reference Image button on the desktop app. A reference image exists in MODEL SPACE, so it's - /// "moves" with the model when the user pans/zooms/rotates. - /// - /// The reference image can be moved, scaled and deleted like a Poly mesh, even though it isn't really Poly mesh. - /// - /// The image is NOT part of the model and doesn't get saved with it. It only exists for reference during the - /// session. - /// - public class MoveableReferenceImage : MoveableObject { - public static readonly string REFERENCE_IMAGE_NAME_PREFIX = "Reference Image "; - - /// - /// Parameters indicating how to create a reference image. - /// - public struct SetupParams { - /// - /// ID of the reference image. - /// - public int refImageId; - /// - /// Texture that represents the reference image. - /// - public Texture2D texture; - /// - /// If true, the image moves with the controller (starts in the grabbed state) - /// until the user clicks the trigger to place it down. - /// - public bool attachToController; - /// - /// Position of the image in model space. - /// - public Vector3 positionModelSpace; - /// - /// Rotation of the image in model space. - /// - public Quaternion rotationModelSpace; - /// - /// Scale of the image in model space. - /// - public Vector3 scaleModelSpace; - /// - /// Whether this is the initial insertion of the image (and hence undo should reattach it to the controller) - /// - public bool initialInsertion; - } - - /// - /// Minimum scale of the reference image. - /// - private const float SCALE_MIN = 0.2f; - - /// - /// Maximum scale of the reference image. - /// - private const float SCALE_MAX = 4.0f; - - /// - /// Scale increment (by how much the scale changes at every click). - /// - private const float SCALE_INCREMENT = 0.1f; - +namespace com.google.apps.peltzer.client.desktop_app +{ /// - /// Texture that represents the image. + /// A reference image is an image that's on the scene to help the user create their model. The user creates these + /// by clicking the Add Reference Image button on the desktop app. A reference image exists in MODEL SPACE, so it's + /// "moves" with the model when the user pans/zooms/rotates. + /// + /// The reference image can be moved, scaled and deleted like a Poly mesh, even though it isn't really Poly mesh. + /// + /// The image is NOT part of the model and doesn't get saved with it. It only exists for reference during the + /// session. /// - public Texture2D referenceImageTexture { get; set; } - - /// - /// ID of the reference image. Reference images have unique IDs. - /// - public int referenceImageId { get; set; } - - private bool initialInsertion; - - public void Setup(SetupParams setupParams) { - base.Setup(); - referenceImageTexture = setupParams.texture; - referenceImageId = setupParams.refImageId; - float halfAspect = (setupParams.texture.width / (float)setupParams.texture.height) * .5f; - initialInsertion = setupParams.initialInsertion; - gameObject.name = REFERENCE_IMAGE_NAME_PREFIX + setupParams.refImageId; - - mesh = GetComponent().mesh; - // Unity has a somewhat odd default vertex order, so this code is: - // * Resizing the quad so it has the same aspect ratio as the image. - // * Reordering the vertices so that they are clockwise and can be used in our mesh math. - mesh.SetVertices(new List() { + public class MoveableReferenceImage : MoveableObject + { + public static readonly string REFERENCE_IMAGE_NAME_PREFIX = "Reference Image "; + + /// + /// Parameters indicating how to create a reference image. + /// + public struct SetupParams + { + /// + /// ID of the reference image. + /// + public int refImageId; + /// + /// Texture that represents the reference image. + /// + public Texture2D texture; + /// + /// If true, the image moves with the controller (starts in the grabbed state) + /// until the user clicks the trigger to place it down. + /// + public bool attachToController; + /// + /// Position of the image in model space. + /// + public Vector3 positionModelSpace; + /// + /// Rotation of the image in model space. + /// + public Quaternion rotationModelSpace; + /// + /// Scale of the image in model space. + /// + public Vector3 scaleModelSpace; + /// + /// Whether this is the initial insertion of the image (and hence undo should reattach it to the controller) + /// + public bool initialInsertion; + } + + /// + /// Minimum scale of the reference image. + /// + private const float SCALE_MIN = 0.2f; + + /// + /// Maximum scale of the reference image. + /// + private const float SCALE_MAX = 4.0f; + + /// + /// Scale increment (by how much the scale changes at every click). + /// + private const float SCALE_INCREMENT = 0.1f; + + /// + /// Texture that represents the image. + /// + public Texture2D referenceImageTexture { get; set; } + + /// + /// ID of the reference image. Reference images have unique IDs. + /// + public int referenceImageId { get; set; } + + private bool initialInsertion; + + public void Setup(SetupParams setupParams) + { + base.Setup(); + referenceImageTexture = setupParams.texture; + referenceImageId = setupParams.refImageId; + float halfAspect = (setupParams.texture.width / (float)setupParams.texture.height) * .5f; + initialInsertion = setupParams.initialInsertion; + gameObject.name = REFERENCE_IMAGE_NAME_PREFIX + setupParams.refImageId; + + mesh = GetComponent().mesh; + // Unity has a somewhat odd default vertex order, so this code is: + // * Resizing the quad so it has the same aspect ratio as the image. + // * Reordering the vertices so that they are clockwise and can be used in our mesh math. + mesh.SetVertices(new List() { new Vector3(-halfAspect, -.5f), new Vector3(-halfAspect, .5f), new Vector3(halfAspect, .5f), new Vector3(halfAspect, -.5f) }); - mesh.SetUVs(0, new List() { + mesh.SetUVs(0, new List() { new Vector2(0, 0), new Vector2(0, 1.0f), new Vector2(1.0f, 1.0f), new Vector2(1.0f, 0) }); - // Add both forward-facing and backward-facing triangles so the reference image can be seen from both sides. - mesh.SetTriangles(new int[] { + // Add both forward-facing and backward-facing triangles so the reference image can be seen from both sides. + mesh.SetTriangles(new int[] { // Front-facing triangles: 0, 1, 2, 0, 2, 3, // Back-facing triangles: 2, 1, 0, 3, 2, 0}, 0); - mesh.RecalculateBounds(); - mesh.RecalculateNormals(); - - // Prepare the material that we will use to render the reference image. We clone the default - // Unity material and set the shader to Unlit/Texture. - material = new Material(gameObject.GetComponent().material); - material.shader = Shader.Find("Unlit/UnlitTransparentWithColor"); - material.mainTexture = setupParams.texture; - - // We won't be using the default MeshRenderer component. We'll handle rendering on our own. - GameObject.Destroy(gameObject.GetComponent()); - - if (setupParams.attachToController) { - // Have the image start attached to the controller and right ahead of it. - positionModelSpace = PeltzerMain.Instance.peltzerController.LastPositionModel + - PeltzerMain.Instance.peltzerController.LastRotationModel * Vector3.forward * HOVER_DISTANCE; - rotationModelSpace = PeltzerMain.Instance.peltzerController.LastRotationModel; - } else { - positionModelSpace = setupParams.positionModelSpace; - rotationModelSpace = setupParams.rotationModelSpace; - } - transform.position = PeltzerMain.Instance.worldSpace.ModelToWorld(positionModelSpace); - transform.rotation = PeltzerMain.Instance.worldSpace.ModelOrientationToWorld(rotationModelSpace); - - scaleModelSpace = setupParams.scaleModelSpace; - - RecalculateVerticesAndNormal(); - - if (setupParams.attachToController) { - Grab(); - } - } - - internal override void Delete() { - base.Delete(); - - SetupParams setupParams = new SetupParams(); - setupParams.positionModelSpace = positionModelSpace; - setupParams.rotationModelSpace = rotationModelSpace; - setupParams.scaleModelSpace = scaleModelSpace; - setupParams.texture = referenceImageTexture; - setupParams.refImageId = referenceImageId; - PeltzerMain.Instance.GetModel().ApplyCommand(new DeleteReferenceImageCommand(setupParams)); - } - - internal override void Release() { - base.Release(); - - // Force an update to get the latest position and rotation. - UpdatePosition(); - - SetupParams oldParams = new SetupParams(); - oldParams.positionModelSpace = positionAtStartOfMove; - oldParams.rotationModelSpace = rotationAtStartOfMove; - oldParams.scaleModelSpace = scaleAtStartOfMove; - oldParams.texture = referenceImageTexture; - oldParams.refImageId = referenceImageId; - - SetupParams newParams = oldParams; - newParams.positionModelSpace = positionModelSpace; - newParams.rotationModelSpace = rotationModelSpace; - newParams.scaleModelSpace = scaleModelSpace; - - oldParams.initialInsertion = initialInsertion; - // Delete the old image and add a new one with the updated position/rotation/scale. - base.Delete(); - PeltzerMain.Instance.GetModel().ApplyCommand(new CompositeCommand(new List() { + mesh.RecalculateBounds(); + mesh.RecalculateNormals(); + + // Prepare the material that we will use to render the reference image. We clone the default + // Unity material and set the shader to Unlit/Texture. + material = new Material(gameObject.GetComponent().material); + material.shader = Shader.Find("Unlit/UnlitTransparentWithColor"); + material.mainTexture = setupParams.texture; + + // We won't be using the default MeshRenderer component. We'll handle rendering on our own. + GameObject.Destroy(gameObject.GetComponent()); + + if (setupParams.attachToController) + { + // Have the image start attached to the controller and right ahead of it. + positionModelSpace = PeltzerMain.Instance.peltzerController.LastPositionModel + + PeltzerMain.Instance.peltzerController.LastRotationModel * Vector3.forward * HOVER_DISTANCE; + rotationModelSpace = PeltzerMain.Instance.peltzerController.LastRotationModel; + } + else + { + positionModelSpace = setupParams.positionModelSpace; + rotationModelSpace = setupParams.rotationModelSpace; + } + transform.position = PeltzerMain.Instance.worldSpace.ModelToWorld(positionModelSpace); + transform.rotation = PeltzerMain.Instance.worldSpace.ModelOrientationToWorld(rotationModelSpace); + + scaleModelSpace = setupParams.scaleModelSpace; + + RecalculateVerticesAndNormal(); + + if (setupParams.attachToController) + { + Grab(); + } + } + + internal override void Delete() + { + base.Delete(); + + SetupParams setupParams = new SetupParams(); + setupParams.positionModelSpace = positionModelSpace; + setupParams.rotationModelSpace = rotationModelSpace; + setupParams.scaleModelSpace = scaleModelSpace; + setupParams.texture = referenceImageTexture; + setupParams.refImageId = referenceImageId; + PeltzerMain.Instance.GetModel().ApplyCommand(new DeleteReferenceImageCommand(setupParams)); + } + + internal override void Release() + { + base.Release(); + + // Force an update to get the latest position and rotation. + UpdatePosition(); + + SetupParams oldParams = new SetupParams(); + oldParams.positionModelSpace = positionAtStartOfMove; + oldParams.rotationModelSpace = rotationAtStartOfMove; + oldParams.scaleModelSpace = scaleAtStartOfMove; + oldParams.texture = referenceImageTexture; + oldParams.refImageId = referenceImageId; + + SetupParams newParams = oldParams; + newParams.positionModelSpace = positionModelSpace; + newParams.rotationModelSpace = rotationModelSpace; + newParams.scaleModelSpace = scaleModelSpace; + + oldParams.initialInsertion = initialInsertion; + // Delete the old image and add a new one with the updated position/rotation/scale. + base.Delete(); + PeltzerMain.Instance.GetModel().ApplyCommand(new CompositeCommand(new List() { new DeleteReferenceImageCommand(oldParams), new AddReferenceImageCommand(newParams) })); - } - - internal override void Scale(bool scaleUp) { - Vector3 oldScale = scaleModelSpace; - float increment = scaleUp ? SCALE_INCREMENT : -SCALE_INCREMENT; - float newScaleFactor = Mathf.Clamp(scaleModelSpace.x + increment, SCALE_MIN, SCALE_MAX); - scaleModelSpace = Vector3.one * newScaleFactor; - RecalculateVerticesAndNormal(); - - // If the image is grabbed, we will apply the scale at the end of the move, so we don't need to worry about - // that here. However, if it's not grabbed, we have to apply the command right now. - if (!grabbed) { - SetupParams oldParams = new SetupParams(); - oldParams.positionModelSpace = positionModelSpace; - oldParams.rotationModelSpace = rotationModelSpace; - oldParams.scaleModelSpace = oldScale; - oldParams.texture = referenceImageTexture; - oldParams.refImageId = referenceImageId; - - SetupParams newParams = oldParams; - newParams.scaleModelSpace = scaleModelSpace; - - base.Delete(); - PeltzerMain.Instance.GetModel().ApplyCommand(new CompositeCommand(new List() { + } + + internal override void Scale(bool scaleUp) + { + Vector3 oldScale = scaleModelSpace; + float increment = scaleUp ? SCALE_INCREMENT : -SCALE_INCREMENT; + float newScaleFactor = Mathf.Clamp(scaleModelSpace.x + increment, SCALE_MIN, SCALE_MAX); + scaleModelSpace = Vector3.one * newScaleFactor; + RecalculateVerticesAndNormal(); + + // If the image is grabbed, we will apply the scale at the end of the move, so we don't need to worry about + // that here. However, if it's not grabbed, we have to apply the command right now. + if (!grabbed) + { + SetupParams oldParams = new SetupParams(); + oldParams.positionModelSpace = positionModelSpace; + oldParams.rotationModelSpace = rotationModelSpace; + oldParams.scaleModelSpace = oldScale; + oldParams.texture = referenceImageTexture; + oldParams.refImageId = referenceImageId; + + SetupParams newParams = oldParams; + newParams.scaleModelSpace = scaleModelSpace; + + base.Delete(); + PeltzerMain.Instance.GetModel().ApplyCommand(new CompositeCommand(new List() { new DeleteReferenceImageCommand(oldParams), new AddReferenceImageCommand(newParams) })); - } + } + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/desktop_app/MyCreationMoreButtonOptionHandler.cs b/Assets/Scripts/desktop_app/MyCreationMoreButtonOptionHandler.cs index 746390d6..cdfc8b95 100644 --- a/Assets/Scripts/desktop_app/MyCreationMoreButtonOptionHandler.cs +++ b/Assets/Scripts/desktop_app/MyCreationMoreButtonOptionHandler.cs @@ -17,45 +17,56 @@ using UnityEngine.EventSystems; using System; -namespace com.google.apps.peltzer.client.desktop_app { - /// - /// This class handles the hovering and selection of individual items in the "more options" menu. - /// - public class MyCreationMoreButtonOptionHandler : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler, IPointerDownHandler, IPointerUpHandler { - private GameObject hover; - private Action clickAction; - - void Start() { - EventSystem eventSystem = EventSystem.current; - hover = transform.Find("hover").gameObject; - } +namespace com.google.apps.peltzer.client.desktop_app +{ + /// + /// This class handles the hovering and selection of individual items in the "more options" menu. + /// + public class MyCreationMoreButtonOptionHandler : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler, IPointerDownHandler, IPointerUpHandler + { + private GameObject hover; + private Action clickAction; - public void OnPointerClick(PointerEventData eventData) { - if (clickAction != null) { - clickAction(); - } - } + void Start() + { + EventSystem eventSystem = EventSystem.current; + hover = transform.Find("hover").gameObject; + } - public void OnPointerEnter(PointerEventData eventData) { - hover.SetActive(true); - } + public void OnPointerClick(PointerEventData eventData) + { + if (clickAction != null) + { + clickAction(); + } + } - public void OnPointerExit(PointerEventData eventData) { - hover.SetActive(false); - } + public void OnPointerEnter(PointerEventData eventData) + { + hover.SetActive(true); + } - public void OnPointerDown(PointerEventData eventData) { - } + public void OnPointerExit(PointerEventData eventData) + { + hover.SetActive(false); + } - public void OnPointerUp(PointerEventData eventData) { - } + public void OnPointerDown(PointerEventData eventData) + { + } - public void SetClickAction(Action clickAction) { - this.clickAction = clickAction; - } + public void OnPointerUp(PointerEventData eventData) + { + } + + public void SetClickAction(Action clickAction) + { + this.clickAction = clickAction; + } - public void RemoveHover() { - hover.SetActive(false); + public void RemoveHover() + { + hover.SetActive(false); + } } - } } diff --git a/Assets/Scripts/desktop_app/ObjImportController.cs b/Assets/Scripts/desktop_app/ObjImportController.cs index d225f973..c857504d 100644 --- a/Assets/Scripts/desktop_app/ObjImportController.cs +++ b/Assets/Scripts/desktop_app/ObjImportController.cs @@ -22,108 +22,122 @@ using com.google.apps.peltzer.client.model.core; using com.google.apps.peltzer.client.model.export; -namespace com.google.apps.peltzer.client.desktop_app { - /// - /// Responsible for handling the button-click to import obj files from the desktop app, and - /// loading them into the model. - /// - public class ObjImportController : MonoBehaviour { +namespace com.google.apps.peltzer.client.desktop_app +{ /// - /// When an OBJ file is imported, this indicates the minimum distance at which it will appear in front of - /// the user, to ensure the user is not too close to (or inside!) of the imported geometry. + /// Responsible for handling the button-click to import obj files from the desktop app, and + /// loading them into the model. /// - private const float MIN_IMPORTED_OBJ_DISTANCE_FROM_USER = 2.0f; + public class ObjImportController : MonoBehaviour + { + /// + /// When an OBJ file is imported, this indicates the minimum distance at which it will appear in front of + /// the user, to ensure the user is not too close to (or inside!) of the imported geometry. + /// + private const float MIN_IMPORTED_OBJ_DISTANCE_FROM_USER = 2.0f; - /// - /// Handles the button-click to import an obj. Opens up a dialog and in the background, waits for the - /// user to hit 'ok' with two files selected. - /// - public void SelectObjToImport() { - Model model = PeltzerMain.Instance.GetModel(); - BackgroundWork openDialog = new OpenFileDialogAndLoadObj(model); - PeltzerMain.Instance.DoPolyMenuBackgroundWork(openDialog); - } + /// + /// Handles the button-click to import an obj. Opens up a dialog and in the background, waits for the + /// user to hit 'ok' with two files selected. + /// + public void SelectObjToImport() + { + Model model = PeltzerMain.Instance.GetModel(); + BackgroundWork openDialog = new OpenFileDialogAndLoadObj(model); + PeltzerMain.Instance.DoPolyMenuBackgroundWork(openDialog); + } - /// - /// Reads an entire file into a string. - /// Faster than File.ReadAllLines(): http://cc.davelozinski.com/c-sharp/fastest-way-to-read-text-files - /// - /// The file to read. - /// The file as a string, with newlines preserved. - private static string FileToString(string filename) { - StringBuilder stringBuilder = new StringBuilder(); - using (FileStream fileStream = File.Open(filename, FileMode.Open)) - using (BufferedStream bufferedStream = new BufferedStream(fileStream)) - using (StreamReader streamReader = new StreamReader(bufferedStream)) { - string line; - while ((line = streamReader.ReadLine()) != null) { - stringBuilder.AppendLine(line); + /// + /// Reads an entire file into a string. + /// Faster than File.ReadAllLines(): http://cc.davelozinski.com/c-sharp/fastest-way-to-read-text-files + /// + /// The file to read. + /// The file as a string, with newlines preserved. + private static string FileToString(string filename) + { + StringBuilder stringBuilder = new StringBuilder(); + using (FileStream fileStream = File.Open(filename, FileMode.Open)) + using (BufferedStream bufferedStream = new BufferedStream(fileStream)) + using (StreamReader streamReader = new StreamReader(bufferedStream)) + { + string line; + while ((line = streamReader.ReadLine()) != null) + { + stringBuilder.AppendLine(line); + } + return stringBuilder.ToString(); + } } - return stringBuilder.ToString(); - } - } - class OpenFileDialogAndLoadObj : BackgroundWork { - // A reference to the model. - private readonly Model model; - // File contents to be passed from a background thread to a foreground thread. - string mtlFileContents; - string objFileContents; - PeltzerFile peltzerFile; + class OpenFileDialogAndLoadObj : BackgroundWork + { + // A reference to the model. + private readonly Model model; + // File contents to be passed from a background thread to a foreground thread. + string mtlFileContents; + string objFileContents; + PeltzerFile peltzerFile; - public OpenFileDialogAndLoadObj(Model model) { - this.model = model; - } + public OpenFileDialogAndLoadObj(Model model) + { + this.model = model; + } - // In the background we perform all the File I/O to get file contents. There are no graceful failures here, - // and there is no feedback to the user in case of failure. - public void BackgroundWork() { - // OpenFileDialog dialog = new OpenFileDialog(); - // dialog.Multiselect = true; - // Expect that the user selected two files, one .obj and one .mtl - // if (dialog.ShowDialog() == DialogResult.OK) { - // if (dialog.FileNames.Length == 1) { - // if (dialog.FileNames[0].EndsWith(".peltzer") || dialog.FileNames[0].EndsWith(".poly") - // || dialog.FileNames[0].EndsWith(".blocks")) { - // byte[] peltzerFileBytes = File.ReadAllBytes(dialog.FileNames[0]); - // PeltzerFileHandler.PeltzerFileFromBytes(peltzerFileBytes, out peltzerFile); - // } else if (dialog.FileNames[0].EndsWith(".obj")) { - // objFileContents = FileToString(dialog.FileNames[0]); - // } else { - // Debug.Log("When selecting only one file for OBJ import, it must have a .obj extension"); - // } - // } else if (dialog.FileNames.Length == 2) { - // string objFile = dialog.FileNames[0].EndsWith(".obj") ? dialog.FileNames[0] : dialog.FileNames[1]; - // string mtlFile = dialog.FileNames[0].EndsWith(".mtl") ? dialog.FileNames[0] : dialog.FileNames[1]; - // if (!objFile.EndsWith(".obj") || !mtlFile.EndsWith(".mtl")) { - // Debug.Log("When selecting two files for OBJ import, one must be .obj and the other .mtl"); - // } + // In the background we perform all the File I/O to get file contents. There are no graceful failures here, + // and there is no feedback to the user in case of failure. + public void BackgroundWork() + { + // OpenFileDialog dialog = new OpenFileDialog(); + // dialog.Multiselect = true; + // Expect that the user selected two files, one .obj and one .mtl + // if (dialog.ShowDialog() == DialogResult.OK) { + // if (dialog.FileNames.Length == 1) { + // if (dialog.FileNames[0].EndsWith(".peltzer") || dialog.FileNames[0].EndsWith(".poly") + // || dialog.FileNames[0].EndsWith(".blocks")) { + // byte[] peltzerFileBytes = File.ReadAllBytes(dialog.FileNames[0]); + // PeltzerFileHandler.PeltzerFileFromBytes(peltzerFileBytes, out peltzerFile); + // } else if (dialog.FileNames[0].EndsWith(".obj")) { + // objFileContents = FileToString(dialog.FileNames[0]); + // } else { + // Debug.Log("When selecting only one file for OBJ import, it must have a .obj extension"); + // } + // } else if (dialog.FileNames.Length == 2) { + // string objFile = dialog.FileNames[0].EndsWith(".obj") ? dialog.FileNames[0] : dialog.FileNames[1]; + // string mtlFile = dialog.FileNames[0].EndsWith(".mtl") ? dialog.FileNames[0] : dialog.FileNames[1]; + // if (!objFile.EndsWith(".obj") || !mtlFile.EndsWith(".mtl")) { + // Debug.Log("When selecting two files for OBJ import, one must be .obj and the other .mtl"); + // } - // objFileContents = FileToString(objFile); - // mtlFileContents = FileToString(mtlFile); - // } else { - // Debug.Log("Exactly one .obj file or a pair of .obj and .mtl files must be selected for OBJ import"); - // } - // } - } + // objFileContents = FileToString(objFile); + // mtlFileContents = FileToString(mtlFile); + // } else { + // Debug.Log("Exactly one .obj file or a pair of .obj and .mtl files must be selected for OBJ import"); + // } + // } + } - // In the foreground we add the mesh to the model. - public void PostWork() { - if (peltzerFile != null) { - foreach (MMesh mesh in peltzerFile.meshes) { - mesh.ChangeId(model.GenerateMeshId()); - AssertOrThrow.True(model.AddMesh(mesh), "Attempted to load an invalid mesh"); - } - } else { - Vector3 headInModelSpace = PeltzerMain.Instance.worldSpace.WorldToModel( - PeltzerMain.Instance.hmd.transform.position); - Vector3 headForward = PeltzerMain.Instance.hmd.transform.forward; - // Request that the OBJ file's geometry be positioned reasonably, so that it appears in front - // of the user at the given minimum distance. - model.AddMeshFromObjAndMtl(objFileContents, mtlFileContents, headInModelSpace, headForward, - MIN_IMPORTED_OBJ_DISTANCE_FROM_USER); + // In the foreground we add the mesh to the model. + public void PostWork() + { + if (peltzerFile != null) + { + foreach (MMesh mesh in peltzerFile.meshes) + { + mesh.ChangeId(model.GenerateMeshId()); + AssertOrThrow.True(model.AddMesh(mesh), "Attempted to load an invalid mesh"); + } + } + else + { + Vector3 headInModelSpace = PeltzerMain.Instance.worldSpace.WorldToModel( + PeltzerMain.Instance.hmd.transform.position); + Vector3 headForward = PeltzerMain.Instance.hmd.transform.forward; + // Request that the OBJ file's geometry be positioned reasonably, so that it appears in front + // of the user at the given minimum distance. + model.AddMeshFromObjAndMtl(objFileContents, mtlFileContents, headInModelSpace, headForward, + MIN_IMPORTED_OBJ_DISTANCE_FROM_USER); + } + } } - } } - } } diff --git a/Assets/Scripts/desktop_app/PreviewController.cs b/Assets/Scripts/desktop_app/PreviewController.cs index e755dabe..c360008b 100644 --- a/Assets/Scripts/desktop_app/PreviewController.cs +++ b/Assets/Scripts/desktop_app/PreviewController.cs @@ -17,20 +17,25 @@ using com.google.apps.peltzer.client.model.util; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.desktop_app { - public class PreviewController : MonoBehaviour { - class OpenFileDialogAndCreatePreview : BackgroundWork { - private const string DIALOG_TITLE = "Choose a Reference Image"; +namespace com.google.apps.peltzer.client.desktop_app +{ + public class PreviewController : MonoBehaviour + { + class OpenFileDialogAndCreatePreview : BackgroundWork + { + private const string DIALOG_TITLE = "Choose a Reference Image"; - private readonly PreviewController previewController; + private readonly PreviewController previewController; - private bool userCancelled; + private bool userCancelled; - public OpenFileDialogAndCreatePreview(PreviewController controller) { - previewController = controller; - } + public OpenFileDialogAndCreatePreview(PreviewController controller) + { + previewController = controller; + } - public void BackgroundWork() { + public void BackgroundWork() + { #if UNITY_STANDALONE_WIN string selectedPath; if (Win32FileDialog.ShowWin32FileDialog(DIALOG_TITLE, @@ -41,42 +46,48 @@ public void BackgroundWork() { userCancelled = true; } #else - Debug.LogError("Open file dialog not available in this platform."); + Debug.LogError("Open file dialog not available in this platform."); #endif - } + } - public void PostWork() { - if (userCancelled) { - previewController.ChangeMenuPrompt(showClickToInsert: true); + public void PostWork() + { + if (userCancelled) + { + previewController.ChangeMenuPrompt(showClickToInsert: true); + } + } } - } - } - bool loadNewPreviewImage = false; - string previewImagePath = null; + bool loadNewPreviewImage = false; + string previewImagePath = null; - void Update() { - if (loadNewPreviewImage && previewImagePath != null) { - PeltzerMain.Instance.referenceImageManager.InsertNewReferenceImage(previewImagePath); - loadNewPreviewImage = false; - previewImagePath = null; - ChangeMenuPrompt(showClickToInsert: true); - } - } + void Update() + { + if (loadNewPreviewImage && previewImagePath != null) + { + PeltzerMain.Instance.referenceImageManager.InsertNewReferenceImage(previewImagePath); + loadNewPreviewImage = false; + previewImagePath = null; + ChangeMenuPrompt(showClickToInsert: true); + } + } - public void SelectPreviewImage() { - BackgroundWork openDialog = new OpenFileDialogAndCreatePreview(this); - PeltzerMain.Instance.DoFilePickerBackgroundWork(openDialog); - ChangeMenuPrompt(showClickToInsert: false); - } + public void SelectPreviewImage() + { + BackgroundWork openDialog = new OpenFileDialogAndCreatePreview(this); + PeltzerMain.Instance.DoFilePickerBackgroundWork(openDialog); + ChangeMenuPrompt(showClickToInsert: false); + } - /// - /// Switches the menu prompt between telling a user to click to insert a reference image, or to - /// take off their headset to complete adding one. - /// - private void ChangeMenuPrompt(bool showClickToInsert) { - ObjectFinder.ObjectById("ID_add_ref_image").SetActive(showClickToInsert); - ObjectFinder.ObjectById("ID_take_off_headset_for_ref_image").SetActive(!showClickToInsert); + /// + /// Switches the menu prompt between telling a user to click to insert a reference image, or to + /// take off their headset to complete adding one. + /// + private void ChangeMenuPrompt(bool showClickToInsert) + { + ObjectFinder.ObjectById("ID_add_ref_image").SetActive(showClickToInsert); + ObjectFinder.ObjectById("ID_take_off_headset_for_ref_image").SetActive(!showClickToInsert); + } } - } } diff --git a/Assets/Scripts/desktop_app/ReferenceImageManager.cs b/Assets/Scripts/desktop_app/ReferenceImageManager.cs index 9ed2a63f..dbd0c8d5 100644 --- a/Assets/Scripts/desktop_app/ReferenceImageManager.cs +++ b/Assets/Scripts/desktop_app/ReferenceImageManager.cs @@ -21,110 +21,125 @@ using com.google.apps.peltzer.client.model.util; using com.google.apps.peltzer.client.tools; -namespace com.google.apps.peltzer.client.desktop_app { - /// - /// Manages the reference images in the scene. - /// - public class ReferenceImageManager : MonoBehaviour { +namespace com.google.apps.peltzer.client.desktop_app +{ /// - /// Next free reference image ID to assign. + /// Manages the reference images in the scene. /// - private int nextId = 0; + public class ReferenceImageManager : MonoBehaviour + { + /// + /// Next free reference image ID to assign. + /// + private int nextId = 0; - /// - /// Reference images currently in the scene. Keyed by ID. - /// - private Dictionary referenceImages = new Dictionary(); + /// + /// Reference images currently in the scene. Keyed by ID. + /// + private Dictionary referenceImages = new Dictionary(); - private Queue pendingReferenceImageCommands = new Queue(); + private Queue pendingReferenceImageCommands = new Queue(); - public void Update() { - if (PeltzerMain.Instance.restrictionManager.insertingReferenceImagesAllowed) { - // If we have reference images waiting to be added and we aren't currently in the middle of inserting one we - // will add one. - if (pendingReferenceImageCommands.Count > 0 && !HasGrabbedReferenceImage()) { - // Change to the "grab" tool so the user can click to place the new reference image in the scene. - if (PeltzerMain.Instance.peltzerController.mode != ControllerMode.move) { - PeltzerMain.Instance.peltzerController - .ChangeMode(ControllerMode.move, ObjectFinder.ObjectById("ID_ToolGrab")); - } + public void Update() + { + if (PeltzerMain.Instance.restrictionManager.insertingReferenceImagesAllowed) + { + // If we have reference images waiting to be added and we aren't currently in the middle of inserting one we + // will add one. + if (pendingReferenceImageCommands.Count > 0 && !HasGrabbedReferenceImage()) + { + // Change to the "grab" tool so the user can click to place the new reference image in the scene. + if (PeltzerMain.Instance.peltzerController.mode != ControllerMode.move) + { + PeltzerMain.Instance.peltzerController + .ChangeMode(ControllerMode.move, ObjectFinder.ObjectById("ID_ToolGrab")); + } - // Do this as an AddReferenceImageCommmand so the user can undo this operation. - PeltzerMain.Instance.GetModel().ApplyCommand(pendingReferenceImageCommands.Dequeue()); + // Do this as an AddReferenceImageCommmand so the user can undo this operation. + PeltzerMain.Instance.GetModel().ApplyCommand(pendingReferenceImageCommands.Dequeue()); + } + } } - } - } - /// - /// Creates a new reference image. It will be initially stuck to the controller until the user presses - /// the trigger button to place it on the scene. If there is already a reference image being dragged - /// by the controller, it will be replaced by the new one. - /// - /// - public void InsertNewReferenceImage(string previewImagePath) { - WWW www = new WWW("file:///" + System.Uri.EscapeUriString(previewImagePath)); - Texture2D texture = new Texture2D(500, 500); - www.LoadImageIntoTexture(texture); + /// + /// Creates a new reference image. It will be initially stuck to the controller until the user presses + /// the trigger button to place it on the scene. If there is already a reference image being dragged + /// by the controller, it will be replaced by the new one. + /// + /// + public void InsertNewReferenceImage(string previewImagePath) + { + WWW www = new WWW("file:///" + System.Uri.EscapeUriString(previewImagePath)); + Texture2D texture = new Texture2D(500, 500); + www.LoadImageIntoTexture(texture); - MoveableReferenceImage.SetupParams setupParams = new MoveableReferenceImage.SetupParams(); - setupParams.attachToController = true; - // Have the image start attached to the controller and right ahead of it. - setupParams.positionModelSpace = PeltzerMain.Instance.peltzerController.LastPositionModel + - PeltzerMain.Instance.peltzerController.LastRotationModel * Vector3.forward * MoveableObject.HOVER_DISTANCE; - setupParams.rotationModelSpace = PeltzerMain.Instance.peltzerController.LastRotationModel; - setupParams.scaleModelSpace = Vector3.one * 0.5f; - setupParams.texture = texture; - setupParams.refImageId = nextId++; - setupParams.initialInsertion = true; + MoveableReferenceImage.SetupParams setupParams = new MoveableReferenceImage.SetupParams(); + setupParams.attachToController = true; + // Have the image start attached to the controller and right ahead of it. + setupParams.positionModelSpace = PeltzerMain.Instance.peltzerController.LastPositionModel + + PeltzerMain.Instance.peltzerController.LastRotationModel * Vector3.forward * MoveableObject.HOVER_DISTANCE; + setupParams.rotationModelSpace = PeltzerMain.Instance.peltzerController.LastRotationModel; + setupParams.scaleModelSpace = Vector3.one * 0.5f; + setupParams.texture = texture; + setupParams.refImageId = nextId++; + setupParams.initialInsertion = true; - pendingReferenceImageCommands.Enqueue(new AddReferenceImageCommand(setupParams)); - } + pendingReferenceImageCommands.Enqueue(new AddReferenceImageCommand(setupParams)); + } - /// - /// Creates a reference image with the specified parameters. - /// - /// - public void CreateReferenceImage(MoveableReferenceImage.SetupParams setupParams) { - AssertOrThrow.True(!referenceImages.ContainsKey(setupParams.refImageId), - "Duplicate reference image ID: " + setupParams.refImageId); - GameObject imageObject = GameObject.CreatePrimitive(PrimitiveType.Quad); - imageObject.GetComponent().enabled = false; - referenceImages[setupParams.refImageId] = imageObject.AddComponent(); - referenceImages[setupParams.refImageId].Setup(setupParams); - } + /// + /// Creates a reference image with the specified parameters. + /// + /// + public void CreateReferenceImage(MoveableReferenceImage.SetupParams setupParams) + { + AssertOrThrow.True(!referenceImages.ContainsKey(setupParams.refImageId), + "Duplicate reference image ID: " + setupParams.refImageId); + GameObject imageObject = GameObject.CreatePrimitive(PrimitiveType.Quad); + imageObject.GetComponent().enabled = false; + referenceImages[setupParams.refImageId] = imageObject.AddComponent(); + referenceImages[setupParams.refImageId].Setup(setupParams); + } - /// - /// Deletes the given reference image. - /// - /// - public void DeleteReferenceImage(int refImageId) { - MoveableReferenceImage refImage; - if (referenceImages.TryGetValue(refImageId, out refImage)) { - referenceImages.Remove(refImageId); - refImage.Destroy(); - } - } + /// + /// Deletes the given reference image. + /// + /// + public void DeleteReferenceImage(int refImageId) + { + MoveableReferenceImage refImage; + if (referenceImages.TryGetValue(refImageId, out refImage)) + { + referenceImages.Remove(refImageId); + refImage.Destroy(); + } + } - /// - /// Returns whether or not there is a grabbed reference image. There will be a grabbed image if the user is - /// in the process of moving one around, either as a result of grabbing it directly, or because they have - /// just inserted one and haven't placed it in the scene yet. - /// - /// True if there is a grabbed reference image, false if not. - public bool HasGrabbedReferenceImage() { - foreach (MoveableReferenceImage refImage in referenceImages.Values) { - if (refImage.grabbed) return true; - } - return false; - } + /// + /// Returns whether or not there is a grabbed reference image. There will be a grabbed image if the user is + /// in the process of moving one around, either as a result of grabbing it directly, or because they have + /// just inserted one and haven't placed it in the scene yet. + /// + /// True if there is a grabbed reference image, false if not. + public bool HasGrabbedReferenceImage() + { + foreach (MoveableReferenceImage refImage in referenceImages.Values) + { + if (refImage.grabbed) return true; + } + return false; + } - private void DeleteAllGrabbedReferenceImages() { - List ids = new List(referenceImages.Keys); - foreach (int id in ids) { - if (referenceImages[id].grabbed) { - DeleteReferenceImage(id); + private void DeleteAllGrabbedReferenceImages() + { + List ids = new List(referenceImages.Keys); + foreach (int id in ids) + { + if (referenceImages[id].grabbed) + { + DeleteReferenceImage(id); + } + } } - } } - } } diff --git a/Assets/Scripts/desktop_app/WriteFrameToPreview.cs b/Assets/Scripts/desktop_app/WriteFrameToPreview.cs index 42603217..58f2e51e 100644 --- a/Assets/Scripts/desktop_app/WriteFrameToPreview.cs +++ b/Assets/Scripts/desktop_app/WriteFrameToPreview.cs @@ -15,46 +15,57 @@ using UnityEngine; using UnityEngine.UI; -namespace com.google.apps.peltzer.client.desktop_app { - /// - /// This is attached to the camera which renders previews. It controls setting the proper - /// render texture and active render texture before rendering, and writing the rendered - /// frame to a texture post rendering. - /// - public class WriteFrameToPreview : MonoBehaviour { - private Camera previewCam; - private RenderTexture renderTexture; - private Image previewImage; +namespace com.google.apps.peltzer.client.desktop_app +{ + /// + /// This is attached to the camera which renders previews. It controls setting the proper + /// render texture and active render texture before rendering, and writing the rendered + /// frame to a texture post rendering. + /// + public class WriteFrameToPreview : MonoBehaviour + { + private Camera previewCam; + private RenderTexture renderTexture; + private Image previewImage; - public void Setup(RenderTexture renderTexture, Image previewImage) { - this.renderTexture = renderTexture; - this.previewImage = previewImage; - } + public void Setup(RenderTexture renderTexture, Image previewImage) + { + this.renderTexture = renderTexture; + this.previewImage = previewImage; + } - void Awake() { - previewCam = gameObject.GetComponent(); - } + void Awake() + { + previewCam = gameObject.GetComponent(); + } - void OnPreRender() { - if (renderTexture != null && previewImage != null) { - previewCam.targetTexture = renderTexture; - RenderTexture.active = renderTexture; - } - } + void OnPreRender() + { + if (renderTexture != null && previewImage != null) + { + previewCam.targetTexture = renderTexture; + RenderTexture.active = renderTexture; + } + } - void OnPostRender() { - if (renderTexture != null && previewImage != null) { - if (previewImage.sprite != null && previewImage.sprite.texture != null) { - Texture2D previewTex = previewImage.sprite.texture; - previewTex.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0); - previewTex.Apply(); - } else { - Texture2D previewTex = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.RGB24, false); - previewTex.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0); - previewTex.Apply(); - previewImage.sprite = Sprite.Create(previewTex, new Rect(0, 0, renderTexture.width, renderTexture.height), new Vector2(.5f, .5f)); + void OnPostRender() + { + if (renderTexture != null && previewImage != null) + { + if (previewImage.sprite != null && previewImage.sprite.texture != null) + { + Texture2D previewTex = previewImage.sprite.texture; + previewTex.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0); + previewTex.Apply(); + } + else + { + Texture2D previewTex = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.RGB24, false); + previewTex.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0); + previewTex.Apply(); + previewImage.sprite = Sprite.Create(previewTex, new Rect(0, 0, renderTexture.width, renderTexture.height), new Vector2(.5f, .5f)); + } + } } - } } - } } \ No newline at end of file diff --git a/Assets/Scripts/entitlement/OAuth2Identity.cs b/Assets/Scripts/entitlement/OAuth2Identity.cs index eb48fb67..a925b5e4 100644 --- a/Assets/Scripts/entitlement/OAuth2Identity.cs +++ b/Assets/Scripts/entitlement/OAuth2Identity.cs @@ -27,454 +27,548 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.api_clients.assets_service_client; -namespace com.google.apps.peltzer.client.entitlement { - - /// Handle accessing OAuth2 based web services. There are known issues with non-square avatars. - - public class OAuth2Identity : MonoBehaviour { - public class UserInfo { - public string id; - public string name; - public string email; - public string location; - public Sprite icon; - } +namespace com.google.apps.peltzer.client.entitlement +{ + + /// Handle accessing OAuth2 based web services. There are known issues with non-square avatars. + + public class OAuth2Identity : MonoBehaviour + { + public class UserInfo + { + public string id; + public string name; + public string email; + public string location; + public Sprite icon; + } - private const string m_ServiceName = "Google"; - private const string m_ClientId = "TODO"; - private const string m_ClientSecret = "TODO"; - private const string m_RequestTokenUri = "https://accounts.google.com/o/oauth2/auth"; - private const string m_AccessTokenUri = "https://accounts.google.com/o/oauth2/token"; - private const string m_UserInfoUri = "[Removed]"; - private const string m_OAuthScope = "profile email " + - "https://www.googleapis.com/auth/plus.me " + - "https://www.googleapis.com/auth/plus.peopleapi.readwrite"; - private const string m_CallbackPath = "/callback"; - private const string m_ReplaceHeadset = "ReplaceHeadset"; - private string m_CallbackFailedMessage = "Sorry!"; - - // User avatar pixel density. This is the number of pixels that correspond to one unit in world space. - // Larger values will make a smaller (more dense) avatar. Smaller values will make it larger (less dense). - private const int USER_AVATAR_PIXELS_PER_UNIT = 30; - - private static Color UI_BACKGROUND_COLOR = Color.clear; - - public static OAuth2Identity Instance; - public static event Action OnProfileUpdated { - add { - if (Instance != null) { - value(); // Call the event once for the current profile. + private const string m_ServiceName = "Google"; + private const string m_ClientId = "TODO"; + private const string m_ClientSecret = "TODO"; + private const string m_RequestTokenUri = "https://accounts.google.com/o/oauth2/auth"; + private const string m_AccessTokenUri = "https://accounts.google.com/o/oauth2/token"; + private const string m_UserInfoUri = "[Removed]"; + private const string m_OAuthScope = "profile email " + + "https://www.googleapis.com/auth/plus.me " + + "https://www.googleapis.com/auth/plus.peopleapi.readwrite"; + private const string m_CallbackPath = "/callback"; + private const string m_ReplaceHeadset = "ReplaceHeadset"; + private string m_CallbackFailedMessage = "Sorry!"; + + // User avatar pixel density. This is the number of pixels that correspond to one unit in world space. + // Larger values will make a smaller (more dense) avatar. Smaller values will make it larger (less dense). + private const int USER_AVATAR_PIXELS_PER_UNIT = 30; + + private static Color UI_BACKGROUND_COLOR = Color.clear; + + public static OAuth2Identity Instance; + public static event Action OnProfileUpdated + { + add + { + if (Instance != null) + { + value(); // Call the event once for the current profile. + } + m_OnProfileUpdated += value; + } + remove + { + m_OnProfileUpdated -= value; + } } - m_OnProfileUpdated += value; - } - remove { - m_OnProfileUpdated -= value; - } - } - private static event Action m_OnProfileUpdated; - private static string PLAYER_PREF_REFRESH_KEY_SUFFIX = "BlocksOAuthRefreshKey"; - private string m_PlayerPrefRefreshKey; - private const string kIconSizeSuffix = "?sz=128"; - - private string m_AccessToken; - private string m_RefreshToken; - private UserInfo m_User = null; - - private HttpListener m_HttpListener; - private int m_HttpPort; - private bool m_WaitingOnAuthorization; - private string m_VerificationCode; - private Boolean m_VerificationError; - - public UserInfo Profile { - get { return m_User; } - set { - m_User = value; - if (m_OnProfileUpdated != null) { - m_OnProfileUpdated(); + private static event Action m_OnProfileUpdated; + private static string PLAYER_PREF_REFRESH_KEY_SUFFIX = "BlocksOAuthRefreshKey"; + private string m_PlayerPrefRefreshKey; + private const string kIconSizeSuffix = "?sz=128"; + + private string m_AccessToken; + private string m_RefreshToken; + private UserInfo m_User = null; + + private HttpListener m_HttpListener; + private int m_HttpPort; + private bool m_WaitingOnAuthorization; + private string m_VerificationCode; + private Boolean m_VerificationError; + + public UserInfo Profile + { + get { return m_User; } + set + { + m_User = value; + if (m_OnProfileUpdated != null) + { + m_OnProfileUpdated(); + } + } } - } - } - public bool LoggedIn { - // We don't consider us logged in until we have the UserInfo - get { return m_RefreshToken != null && Profile != null; } - } + public bool LoggedIn + { + // We don't consider us logged in until we have the UserInfo + get { return m_RefreshToken != null && Profile != null; } + } - public bool HasAccessToken { - get { return m_AccessToken != null; } - } + public bool HasAccessToken + { + get { return m_AccessToken != null; } + } - void Awake() { - Instance = this; - m_PlayerPrefRefreshKey = String.Format("{0}{1}", m_ServiceName, PLAYER_PREF_REFRESH_KEY_SUFFIX); + void Awake() + { + Instance = this; + m_PlayerPrefRefreshKey = String.Format("{0}{1}", m_ServiceName, PLAYER_PREF_REFRESH_KEY_SUFFIX); - if (PlayerPrefs.HasKey(m_PlayerPrefRefreshKey)) { - m_RefreshToken = PlayerPrefs.GetString(m_PlayerPrefRefreshKey); - } - } + if (PlayerPrefs.HasKey(m_PlayerPrefRefreshKey)) + { + m_RefreshToken = PlayerPrefs.GetString(m_PlayerPrefRefreshKey); + } + } - // Use Google Account Chooser to open a url with the current account. - public void OpenURL(string url) { - if (LoggedIn) { - url = string.Format("https://accounts.google.com/AccountChooser?Email={0}&continue={1}", - Profile.email, url); - } - Application.OpenURL(url); - } + // Use Google Account Chooser to open a url with the current account. + public void OpenURL(string url) + { + if (LoggedIn) + { + url = string.Format("https://accounts.google.com/AccountChooser?Email={0}&continue={1}", + Profile.email, url); + } + Application.OpenURL(url); + } - public void Login(System.Action onSuccess, System.Action onFailure, bool promptUserIfNoToken) { - StartCoroutine(Authorize(onSuccess, onFailure, promptUserIfNoToken)); - } + public void Login(System.Action onSuccess, System.Action onFailure, bool promptUserIfNoToken) + { + StartCoroutine(Authorize(onSuccess, onFailure, promptUserIfNoToken)); + } - public void Logout() { - if (m_RefreshToken != null) { - // Not sure if it's possible for m_User to be null here. - if (Profile != null) { - Debug.Log(Profile.name + " logged out."); - } else { - Debug.Log("Logged out."); + public void Logout() + { + if (m_RefreshToken != null) + { + // Not sure if it's possible for m_User to be null here. + if (Profile != null) + { + Debug.Log(Profile.name + " logged out."); + } + else + { + Debug.Log("Logged out."); + } + m_RefreshToken = null; + m_AccessToken = null; + Profile = null; + PlayerPrefs.DeleteKey(m_PlayerPrefRefreshKey); + } } - m_RefreshToken = null; - m_AccessToken = null; - Profile = null; - PlayerPrefs.DeleteKey(m_PlayerPrefRefreshKey); - } - } - /// Sign an outgoing request. - public void Authenticate(UnityWebRequest www) { - www.SetRequestHeader("Authorization", String.Format("Bearer {0}", m_AccessToken)); - } + /// Sign an outgoing request. + public void Authenticate(UnityWebRequest www) + { + www.SetRequestHeader("Authorization", String.Format("Bearer {0}", m_AccessToken)); + } - private static string UserInfoRequestUri() { - return String.Format("{0}&key={1}", m_UserInfoUri, AssetsServiceClient.POLY_KEY); - } + private static string UserInfoRequestUri() + { + return String.Format("{0}&key={1}", m_UserInfoUri, AssetsServiceClient.POLY_KEY); + } - private IEnumerator GetUserInfo() { - if (String.IsNullOrEmpty(m_RefreshToken)) { - yield break; - } - - UserInfo user = new UserInfo(); - for (int i = 0; i < 2; i++) { - using (UnityWebRequest www = UnityWebRequest.Get(UserInfoRequestUri())) { - Authenticate(www); - yield return www.Send(); - if (www.responseCode == 200) { - JObject json = JObject.Parse(www.downloadHandler.text); - user.id = json["resourceName"].ToString(); - user.name = json["names"][0]["displayName"].ToString(); - string iconUri = json["photos"][0]["url"].ToString(); - if (json["residences"] != null) { - user.location = json["residences"][0]["value"].ToString(); + private IEnumerator GetUserInfo() + { + if (String.IsNullOrEmpty(m_RefreshToken)) + { + yield break; } - if (json["emailAddresses"] != null) { - foreach (var email in json["emailAddresses"]) { - var primary = email["metadata"]["primary"]; - if (primary != null && primary.Value()) { - user.email = email["value"].ToString(); - break; + + UserInfo user = new UserInfo(); + for (int i = 0; i < 2; i++) + { + using (UnityWebRequest www = UnityWebRequest.Get(UserInfoRequestUri())) + { + Authenticate(www); + yield return www.Send(); + if (www.responseCode == 200) + { + JObject json = JObject.Parse(www.downloadHandler.text); + user.id = json["resourceName"].ToString(); + user.name = json["names"][0]["displayName"].ToString(); + string iconUri = json["photos"][0]["url"].ToString(); + if (json["residences"] != null) + { + user.location = json["residences"][0]["value"].ToString(); + } + if (json["emailAddresses"] != null) + { + foreach (var email in json["emailAddresses"]) + { + var primary = email["metadata"]["primary"]; + if (primary != null && primary.Value()) + { + user.email = email["value"].ToString(); + break; + } + } + } + Profile = user; + yield return LoadProfileIcon(iconUri); + + Debug.Log(Profile.name + " logged in."); + yield break; + } + else if (www.responseCode == 401) + { + yield return Reauthorize(); + } + else + { + Debug.Log(www.responseCode); + Debug.Log(www.error); + Debug.Log(www.downloadHandler.text); + } } - } } - Profile = user; - yield return LoadProfileIcon(iconUri); - - Debug.Log(Profile.name + " logged in."); - yield break; - } else if (www.responseCode == 401) { - yield return Reauthorize(); - } else { - Debug.Log(www.responseCode); - Debug.Log(www.error); - Debug.Log(www.downloadHandler.text); - } + Profile = null; } - } - Profile = null; - } - // I have a refresh token, I need an access token. - public IEnumerator Reauthorize() { - m_AccessToken = null; - if (!String.IsNullOrEmpty(m_RefreshToken)) { - Dictionary parameters = new Dictionary(); - parameters.Add("client_id", m_ClientId); - parameters.Add("client_secret", m_ClientSecret); - parameters.Add("refresh_token", m_RefreshToken); - parameters.Add("grant_type", "refresh_token"); - using (UnityWebRequest www = UnityWebRequest.Post(m_AccessTokenUri, parameters)) { - yield return www.Send(); - if (www.isNetworkError) { - Debug.LogError("Network error"); - yield break; - } - - if (www.responseCode == 400 || www.responseCode == 401) { - // Refresh token revoked or expired - forget it - m_RefreshToken = null; - PlayerPrefs.DeleteKey(m_PlayerPrefRefreshKey); - } else { - JObject json = JObject.Parse(www.downloadHandler.text); - m_AccessToken = json["access_token"].ToString(); - } + // I have a refresh token, I need an access token. + public IEnumerator Reauthorize() + { + m_AccessToken = null; + if (!String.IsNullOrEmpty(m_RefreshToken)) + { + Dictionary parameters = new Dictionary(); + parameters.Add("client_id", m_ClientId); + parameters.Add("client_secret", m_ClientSecret); + parameters.Add("refresh_token", m_RefreshToken); + parameters.Add("grant_type", "refresh_token"); + using (UnityWebRequest www = UnityWebRequest.Post(m_AccessTokenUri, parameters)) + { + yield return www.Send(); + if (www.isNetworkError) + { + Debug.LogError("Network error"); + yield break; + } + + if (www.responseCode == 400 || www.responseCode == 401) + { + // Refresh token revoked or expired - forget it + m_RefreshToken = null; + PlayerPrefs.DeleteKey(m_PlayerPrefRefreshKey); + } + else + { + JObject json = JObject.Parse(www.downloadHandler.text); + m_AccessToken = json["access_token"].ToString(); + } + } + } } - } - } - /// - /// Attempt to authorise the user via a refresh token, or by giving them a browser window - /// to authorize permissions then get refresh and access tokens. - /// - /// Callback on success - /// Callback on failure - /// - /// If true, will prompt the user to sign in via a browser if no refresh token found. - /// - public IEnumerator Authorize(System.Action onSuccess, System.Action onFailure, bool promptUserIfNoToken) { - if (String.IsNullOrEmpty(m_RefreshToken) && promptUserIfNoToken) { - int port = m_HttpPort != 0 ? m_HttpPort : StartHttpListener(); - string redirectURI = string.Format("http://localhost:{0}/", port); - - if (port == 0) { - // Failed to start HTTP server. - onFailure(); - yield break; - } + /// + /// Attempt to authorise the user via a refresh token, or by giving them a browser window + /// to authorize permissions then get refresh and access tokens. + /// + /// Callback on success + /// Callback on failure + /// + /// If true, will prompt the user to sign in via a browser if no refresh token found. + /// + public IEnumerator Authorize(System.Action onSuccess, System.Action onFailure, bool promptUserIfNoToken) + { + if (String.IsNullOrEmpty(m_RefreshToken) && promptUserIfNoToken) + { + int port = m_HttpPort != 0 ? m_HttpPort : StartHttpListener(); + string redirectURI = string.Format("http://localhost:{0}/", port); + + if (port == 0) + { + // Failed to start HTTP server. + onFailure(); + yield break; + } - StringBuilder sb = new StringBuilder(m_RequestTokenUri) - .Append("?client_id=").Append(Uri.EscapeDataString(m_ClientId)) - .Append("&redirect_uri=").Append("http://localhost:").Append(port).Append(m_CallbackPath) - .Append("&response_type=code") - .Append("&scope=").Append(m_OAuthScope); - - // Something about the url makes OpenURL() not work on OSX, so use a workaround - if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer) { - System.Diagnostics.Process.Start(sb.ToString()); - } else { - Application.OpenURL(sb.ToString()); - } + StringBuilder sb = new StringBuilder(m_RequestTokenUri) + .Append("?client_id=").Append(Uri.EscapeDataString(m_ClientId)) + .Append("&redirect_uri=").Append("http://localhost:").Append(port).Append(m_CallbackPath) + .Append("&response_type=code") + .Append("&scope=").Append(m_OAuthScope); - if (m_WaitingOnAuthorization) { - // A previous attempt is already waiting - yield break; - } - m_WaitingOnAuthorization = true; - m_VerificationCode = null; - m_VerificationError = false; + // Something about the url makes OpenURL() not work on OSX, so use a workaround + if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer) + { + System.Diagnostics.Process.Start(sb.ToString()); + } + else + { + Application.OpenURL(sb.ToString()); + } - // Wait for verification - while (m_VerificationCode == null || m_VerificationError) { - yield return null; - } + if (m_WaitingOnAuthorization) + { + // A previous attempt is already waiting + yield break; + } + m_WaitingOnAuthorization = true; + m_VerificationCode = null; + m_VerificationError = false; + + // Wait for verification + while (m_VerificationCode == null || m_VerificationError) + { + yield return null; + } - if (m_VerificationError) { - Debug.LogError("Account verification failed"); - Debug.LogFormat("Verification error {0}", m_VerificationCode); - m_WaitingOnAuthorization = false; - yield break; - } + if (m_VerificationError) + { + Debug.LogError("Account verification failed"); + Debug.LogFormat("Verification error {0}", m_VerificationCode); + m_WaitingOnAuthorization = false; + yield break; + } - // Exchange for tokens - var parameters = new Dictionary(); - parameters.Add("code", m_VerificationCode); - parameters.Add("client_id", m_ClientId); - parameters.Add("client_secret", m_ClientSecret); - parameters.Add("redirect_uri", String.Format("http://localhost:{0}{1}", port, m_CallbackPath)); - parameters.Add("grant_type", "authorization_code"); - - UnityWebRequest www = UnityWebRequest.Post(m_AccessTokenUri, parameters); - - yield return www.Send(); - if (www.isNetworkError) { - Debug.LogError("Network error"); - m_WaitingOnAuthorization = false; - yield break; - } else if (www.responseCode >= 400) { - Debug.LogError("Authorization failed"); - Debug.LogFormat("Authorization error {0}", www.downloadHandler.text); - m_WaitingOnAuthorization = false; - yield break; - } + // Exchange for tokens + var parameters = new Dictionary(); + parameters.Add("code", m_VerificationCode); + parameters.Add("client_id", m_ClientId); + parameters.Add("client_secret", m_ClientSecret); + parameters.Add("redirect_uri", String.Format("http://localhost:{0}{1}", port, m_CallbackPath)); + parameters.Add("grant_type", "authorization_code"); + + UnityWebRequest www = UnityWebRequest.Post(m_AccessTokenUri, parameters); + + yield return www.Send(); + if (www.isNetworkError) + { + Debug.LogError("Network error"); + m_WaitingOnAuthorization = false; + yield break; + } + else if (www.responseCode >= 400) + { + Debug.LogError("Authorization failed"); + Debug.LogFormat("Authorization error {0}", www.downloadHandler.text); + m_WaitingOnAuthorization = false; + yield break; + } - JObject json = JObject.Parse(www.downloadHandler.text); - if (json != null) { - m_AccessToken = json["access_token"].ToString(); - m_RefreshToken = json["refresh_token"].ToString(); - PlayerPrefs.SetString(m_PlayerPrefRefreshKey, m_RefreshToken); - } - m_WaitingOnAuthorization = false; - } + JObject json = JObject.Parse(www.downloadHandler.text); + if (json != null) + { + m_AccessToken = json["access_token"].ToString(); + m_RefreshToken = json["refresh_token"].ToString(); + PlayerPrefs.SetString(m_PlayerPrefRefreshKey, m_RefreshToken); + } + m_WaitingOnAuthorization = false; + } - yield return GetUserInfo(); + yield return GetUserInfo(); - if (LoggedIn) { - onSuccess(); - } else { - onFailure(); - } - } + if (LoggedIn) + { + onSuccess(); + } + else + { + onFailure(); + } + } - private int StartHttpListener() { - // Get a free port - TcpListener tcpListener = new TcpListener(IPAddress.Loopback, 0); - tcpListener.Start(); - m_HttpPort = ((IPEndPoint)tcpListener.LocalEndpoint).Port; - tcpListener.Stop(); - - // There is a small possibility of someone else grabbing the port here, but I'm not aware - // of any other way to do this with HttpListener. - - // We can't load resources in the listener so do it here. - string responseText = Resources.Load("Text/headset").text; - - try { - m_HttpListener = new HttpListener(); - m_HttpListener.Prefixes.Add(String.Format("http://localhost:{0}/", m_HttpPort)); - m_HttpListener.Start(); - ThreadPool.QueueUserWorkItem((o) => { - while (m_HttpListener.IsListening) { - ThreadPool.QueueUserWorkItem((c) => { - var ctx = c as HttpListenerContext; - try { - string response = HttpRequestCallback(ctx.Request, responseText); - byte[] buf = System.Text.Encoding.UTF8.GetBytes(response); - ctx.Response.ContentLength64 = buf.Length; - ctx.Response.OutputStream.Write(buf, 0, buf.Length); - } finally { - ctx.Response.Close(); - } - }, m_HttpListener.GetContext()); - } - }); - } catch (System.Net.Sockets.SocketException e) { - Debug.LogFormat("HttpListener failed to start\n{0}", e); - m_HttpListener = null; - m_HttpPort = 0; - } - - return m_HttpPort; - } + private int StartHttpListener() + { + // Get a free port + TcpListener tcpListener = new TcpListener(IPAddress.Loopback, 0); + tcpListener.Start(); + m_HttpPort = ((IPEndPoint)tcpListener.LocalEndpoint).Port; + tcpListener.Stop(); + + // There is a small possibility of someone else grabbing the port here, but I'm not aware + // of any other way to do this with HttpListener. + + // We can't load resources in the listener so do it here. + string responseText = Resources.Load("Text/headset").text; + + try + { + m_HttpListener = new HttpListener(); + m_HttpListener.Prefixes.Add(String.Format("http://localhost:{0}/", m_HttpPort)); + m_HttpListener.Start(); + ThreadPool.QueueUserWorkItem((o) => + { + while (m_HttpListener.IsListening) + { + ThreadPool.QueueUserWorkItem((c) => + { + var ctx = c as HttpListenerContext; + try + { + string response = HttpRequestCallback(ctx.Request, responseText); + byte[] buf = System.Text.Encoding.UTF8.GetBytes(response); + ctx.Response.ContentLength64 = buf.Length; + ctx.Response.OutputStream.Write(buf, 0, buf.Length); + } + finally + { + ctx.Response.Close(); + } + }, m_HttpListener.GetContext()); + } + }); + } + catch (System.Net.Sockets.SocketException e) + { + Debug.LogFormat("HttpListener failed to start\n{0}", e); + m_HttpListener = null; + m_HttpPort = 0; + } - private void StopHttpListener() { - if (m_HttpListener != null) { - m_HttpListener.Abort(); - m_HttpListener = null; - m_HttpPort = 0; - } - } + return m_HttpPort; + } - private string HttpRequestCallback(HttpListenerRequest request, string message) { - if (request.Url.AbsolutePath == m_CallbackPath) { - if (request.Url.Query.StartsWith("?code=")) { - m_VerificationCode = request.Url.Query.Substring(6); - m_VerificationError = false; - } else if (request.Url.Query.StartsWith("#error=")) { - m_VerificationError = true; - m_VerificationCode = request.Url.Query.Substring(7); - } else { - m_VerificationError = true; - m_VerificationCode = null; + private void StopHttpListener() + { + if (m_HttpListener != null) + { + m_HttpListener.Abort(); + m_HttpListener = null; + m_HttpPort = 0; + } } - } - return m_VerificationError ? m_CallbackFailedMessage : message; - } - // Unity doesn't do this correctly so we do it ourselves - // https://fogbugz.unity3d.com/default.asp?846309_0391asaijk4j3vnt - static public byte[] serializeMultipartForm(string filepath, byte[] boundary, string contentType) { - Debug.Assert(File.Exists(filepath), filepath); - FileInfo fileInfo = new FileInfo(filepath); - long filesize = fileInfo.Length; - - FileStream stream = new FileStream(filepath, FileMode.Open, FileAccess.Read); - byte[] buffer = new byte[filesize]; - stream.Read(buffer, 0, (int)filesize); - stream.Close(); - return serializeMultipartForm(buffer, boundary, Path.GetFileName(filepath), contentType); - } + private string HttpRequestCallback(HttpListenerRequest request, string message) + { + if (request.Url.AbsolutePath == m_CallbackPath) + { + if (request.Url.Query.StartsWith("?code=")) + { + m_VerificationCode = request.Url.Query.Substring(6); + m_VerificationError = false; + } + else if (request.Url.Query.StartsWith("#error=")) + { + m_VerificationError = true; + m_VerificationCode = request.Url.Query.Substring(7); + } + else + { + m_VerificationError = true; + m_VerificationCode = null; + } + } + return m_VerificationError ? m_CallbackFailedMessage : message; + } - static public byte[] serializeMultipartForm(byte[] buffer, byte[] boundary, string filename, string contentType) { - MemoryStream ms = new MemoryStream(); - - const byte dash = 0x2d; - ms.WriteByte(dash); - ms.WriteByte(dash); - ms.Write(boundary, 0, boundary.Length); - string header = String.Format("\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{0}\"\r\nContent-Type: {1}\r\n\r\n", - filename, contentType); - byte[] headerBytes = System.Text.Encoding.ASCII.GetBytes(header); - ms.Write(headerBytes, 0, headerBytes.Length); - - ms.Write(buffer, 0, buffer.Length); - - ms.WriteByte(0x0d); - ms.WriteByte(0x0a); - ms.WriteByte(dash); - ms.WriteByte(dash); - ms.Write(boundary, 0, boundary.Length); - ms.WriteByte(0x0d); - ms.WriteByte(0x0a); - - return ms.ToArray(); - } + // Unity doesn't do this correctly so we do it ourselves + // https://fogbugz.unity3d.com/default.asp?846309_0391asaijk4j3vnt + static public byte[] serializeMultipartForm(string filepath, byte[] boundary, string contentType) + { + Debug.Assert(File.Exists(filepath), filepath); + FileInfo fileInfo = new FileInfo(filepath); + long filesize = fileInfo.Length; + + FileStream stream = new FileStream(filepath, FileMode.Open, FileAccess.Read); + byte[] buffer = new byte[filesize]; + stream.Read(buffer, 0, (int)filesize); + stream.Close(); + return serializeMultipartForm(buffer, boundary, Path.GetFileName(filepath), contentType); + } - private IEnumerator LoadProfileIcon(string uri) { - if (Profile == null) { - yield break; - } - using (UnityWebRequest www = UnityWebRequestTexture.GetTexture(uri + kIconSizeSuffix)) { - yield return www.Send(); - if (www.isNetworkError || www.responseCode >= 400) { - Debug.LogErrorFormat("Error downloading {0}, error {1}", uri, www.responseCode); - Profile.icon = null; - } else { - // Convert the texture to a circle and set it as the user's avatar in the UI and the PolyMenu. - Texture2D profileImage = DownloadHandlerTexture.GetContent(www); - Profile.icon = Sprite.Create(CropSquareTextureToCircle(profileImage), - new Rect(0, 0, profileImage.width, profileImage.height), new Vector2(0.5f, 0.5f), - USER_AVATAR_PIXELS_PER_UNIT); + static public byte[] serializeMultipartForm(byte[] buffer, byte[] boundary, string filename, string contentType) + { + MemoryStream ms = new MemoryStream(); + + const byte dash = 0x2d; + ms.WriteByte(dash); + ms.WriteByte(dash); + ms.Write(boundary, 0, boundary.Length); + string header = String.Format("\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{0}\"\r\nContent-Type: {1}\r\n\r\n", + filename, contentType); + byte[] headerBytes = System.Text.Encoding.ASCII.GetBytes(header); + ms.Write(headerBytes, 0, headerBytes.Length); + + ms.Write(buffer, 0, buffer.Length); + + ms.WriteByte(0x0d); + ms.WriteByte(0x0a); + ms.WriteByte(dash); + ms.WriteByte(dash); + ms.Write(boundary, 0, boundary.Length); + ms.WriteByte(0x0d); + ms.WriteByte(0x0a); + + return ms.ToArray(); } - if (m_OnProfileUpdated != null) { - m_OnProfileUpdated(); + + private IEnumerator LoadProfileIcon(string uri) + { + if (Profile == null) + { + yield break; + } + using (UnityWebRequest www = UnityWebRequestTexture.GetTexture(uri + kIconSizeSuffix)) + { + yield return www.Send(); + if (www.isNetworkError || www.responseCode >= 400) + { + Debug.LogErrorFormat("Error downloading {0}, error {1}", uri, www.responseCode); + Profile.icon = null; + } + else + { + // Convert the texture to a circle and set it as the user's avatar in the UI and the PolyMenu. + Texture2D profileImage = DownloadHandlerTexture.GetContent(www); + Profile.icon = Sprite.Create(CropSquareTextureToCircle(profileImage), + new Rect(0, 0, profileImage.width, profileImage.height), new Vector2(0.5f, 0.5f), + USER_AVATAR_PIXELS_PER_UNIT); + } + if (m_OnProfileUpdated != null) + { + m_OnProfileUpdated(); + } + } } - } - } - /// - /// Gets a free port on the local machine to use for the local redirect HttpListener. - /// (see http://stackoverflow.com/a/3978040) - /// - /// A free port on the local machine. - private static int GetRandomUnusedPort() { - TcpListener listener = new TcpListener(IPAddress.Loopback, 0); - listener.Start(); - int port = ((IPEndPoint)listener.LocalEndpoint).Port; - listener.Stop(); - return port; - } + /// + /// Gets a free port on the local machine to use for the local redirect HttpListener. + /// (see http://stackoverflow.com/a/3978040) + /// + /// A free port on the local machine. + private static int GetRandomUnusedPort() + { + TcpListener listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + int port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } - private Texture2D CropSquareTextureToCircle(Texture2D squareTexture) { - float width = squareTexture.width; - float height = squareTexture.height; - float radius = width / 2; - float centerX = squareTexture.width / 2; - float centerY = squareTexture.height / 2; - Color[] c = squareTexture.GetPixels(0, 0, (int)width, (int)height); - Texture2D circleTexture = new Texture2D((int)height, (int)width); - for (int i = 0; i < height * width; i++) { - int y = Mathf.FloorToInt(i / width); - int x = Mathf.FloorToInt(i - (y * width)); - if (radius * radius >= (x - centerX) * (x - centerX) + (y - centerY) * (y - centerY)) { - circleTexture.SetPixel(x, y, c[i]); - } else { - circleTexture.SetPixel(x, y, UI_BACKGROUND_COLOR); + private Texture2D CropSquareTextureToCircle(Texture2D squareTexture) + { + float width = squareTexture.width; + float height = squareTexture.height; + float radius = width / 2; + float centerX = squareTexture.width / 2; + float centerY = squareTexture.height / 2; + Color[] c = squareTexture.GetPixels(0, 0, (int)width, (int)height); + Texture2D circleTexture = new Texture2D((int)height, (int)width); + for (int i = 0; i < height * width; i++) + { + int y = Mathf.FloorToInt(i / width); + int x = Mathf.FloorToInt(i - (y * width)); + if (radius * radius >= (x - centerX) * (x - centerX) + (y - centerY) * (y - centerY)) + { + circleTexture.SetPixel(x, y, c[i]); + } + else + { + circleTexture.SetPixel(x, y, UI_BACKGROUND_COLOR); + } + } + circleTexture.Apply(); + return circleTexture; } - } - circleTexture.Apply(); - return circleTexture; } - } } diff --git a/Assets/Scripts/entitlement/OculusAuth.cs b/Assets/Scripts/entitlement/OculusAuth.cs index 94ca84f9..b445b86a 100644 --- a/Assets/Scripts/entitlement/OculusAuth.cs +++ b/Assets/Scripts/entitlement/OculusAuth.cs @@ -14,45 +14,49 @@ using UnityEngine; -namespace com.google.apps.peltzer.client.entitlement { - public class OculusAuth : MonoBehaviour { - private bool userIsEntitled = false; - // The App ID is a public identifier for the Blocks app on the Oculus platform. It is - // analogous to Apple's App ID, which shows up in URLs related to the app. - private const string OCULUS_APP_ID = "[Removed]]"; +namespace com.google.apps.peltzer.client.entitlement +{ + public class OculusAuth : MonoBehaviour + { + private bool userIsEntitled = false; + // The App ID is a public identifier for the Blocks app on the Oculus platform. It is + // analogous to Apple's App ID, which shows up in URLs related to the app. + private const string OCULUS_APP_ID = "[Removed]]"; - private void Awake() { - // TODO AB - // Oculus.Platform.Core.Initialize(OCULUS_APP_ID); - // Oculus.Platform.Entitlements.IsUserEntitledToApplication().OnComplete(EntitlementCallback); - } + private void Awake() + { + // TODO AB + // Oculus.Platform.Core.Initialize(OCULUS_APP_ID); + // Oculus.Platform.Entitlements.IsUserEntitledToApplication().OnComplete(EntitlementCallback); + } - public void Update() { - // Oculus.Platform.Request.RunCallbacks(); - } + public void Update() + { + // Oculus.Platform.Request.RunCallbacks(); + } - // TODO AB - // private void EntitlementCallback(Oculus.Platform.Message response) { - // string message; - // if (response.IsError) { - // if (response.GetError() != null) { - // message = response.GetError().Message; - // } else { - // message = "Authentication failed"; - // } - // } else { - // userIsEntitled = true; - // message = ""; - // } - // - // if (message != string.Empty) { - // Debug.Log(message, this); - // } - // - // if (!userIsEntitled) { - // Debug.Log("User not authenticated! You must be logged in to continue."); - // Application.Quit(); - // } - // } - } + // TODO AB + // private void EntitlementCallback(Oculus.Platform.Message response) { + // string message; + // if (response.IsError) { + // if (response.GetError() != null) { + // message = response.GetError().Message; + // } else { + // message = "Authentication failed"; + // } + // } else { + // userIsEntitled = true; + // message = ""; + // } + // + // if (message != string.Empty) { + // Debug.Log(message, this); + // } + // + // if (!userIsEntitled) { + // Debug.Log("User not authenticated! You must be logged in to continue."); + // Application.Quit(); + // } + // } + } } diff --git a/Assets/Scripts/gif_creation/GifEncodeTask.cs b/Assets/Scripts/gif_creation/GifEncodeTask.cs index ecfc0adf..bbf040ae 100644 --- a/Assets/Scripts/gif_creation/GifEncodeTask.cs +++ b/Assets/Scripts/gif_creation/GifEncodeTask.cs @@ -19,205 +19,234 @@ using System.Threading; using UnityEngine; -namespace TiltBrush { - -public class GifEncodeTask { - readonly int m_GifWidth; - readonly int m_GifHeight; - readonly string m_GifName; - // readonly bool m_Symmetric; Not actually needed at the moment - readonly int m_FrameDelayMs; - readonly bool m_bSavePngs = false; - readonly bool m_bPalettePerFrame = false; - readonly float m_DitherStrength; - - private List m_Frames; - - private float m_CreationPercent; - private Thread m_Thread; - private string m_ErrorMessage; - - /// If true, the output file has been written. - public bool IsDone { get { return !m_Thread.IsAlive; } } - - public string GifName { get { return m_GifName; } } - - /// An error message, or null if no error. - public string Error { - get { - if (! IsDone) { - throw new InvalidOperationException("Not done"); - } - return m_ErrorMessage; - } - } - - /// A number in [0, 1] representing the state of the encode. - public float CreationPercent { get { return m_CreationPercent; } } - - /// gifName: A path to the output file (either full, or relative to cwd) - /// dither: A number from 0 to 1 (powers of 2 are best) - /// TODO(pld): when frameWidth == 500 and frameHeight == 500, the palettes seem to be - /// created incorrectly. - public GifEncodeTask( - List frames, int frameDelayMs, - int frameWidth, int frameHeight, string gifName, - float ditherStrength=1f/8, bool palettePerFrame = false) { - m_DitherStrength = ditherStrength; - // Browsers (and monitors) won't show more than 60Hz - m_Frames = frames; - // Gif delay is expressed in 100ths of a second - m_FrameDelayMs = (int)Mathf.Round(frameDelayMs / 10f) * 10; - m_GifWidth = frameWidth; - m_GifHeight = frameHeight; - m_GifName = gifName; - m_bPalettePerFrame = palettePerFrame; - // m_Symmetric = IsSymmetric(frames); - - m_CreationPercent = 0.0f; - } - - public void Start() { - m_Thread = new Thread(Run); - m_Thread.Start(); - } - - private void WriteFrameAsPng(DirectoryInfo di, int i) { - Color32[] frame = m_Frames[i]; - string filename = Path.Combine(di.FullName, string.Format("{0:00}.png", i)); - ImageInfo imi = new ImageInfo(m_GifWidth, m_GifHeight, 8, false); - PngWriter png = FileHelper.CreatePngWriter(filename, imi, true); - - byte[] row = new byte[m_GifWidth * 3]; - for (int iRow = 0; iRow < m_GifHeight; ++iRow) { - int iStartPixel = (m_GifHeight-1 - iRow) * m_GifWidth; - for (int iCol = 0; iCol < m_GifWidth; ++iCol) { - Color32 c = frame[iStartPixel + iCol]; - row[iCol*3 + 0] = c.r; - row[iCol*3 + 1] = c.g; - row[iCol*3 + 2] = c.b; - } - png.WriteRowByte(row, iRow); - } - - png.End(); - } +namespace TiltBrush +{ - private void Run() { - m_ErrorMessage = null; - try { - RunLow(); - } - catch (IOException e) { - m_ErrorMessage = e.Message; - } - catch (UnauthorizedAccessException e) { - m_ErrorMessage = e.Message; - } - catch (Exception e) { - m_ErrorMessage = "Unexpected error: " + e.Message; - } - } - - private void RunLow() { - if (m_bSavePngs) { - DirectoryInfo di; - try { - string dirname = m_GifName.Substring(0, m_GifName.Length-4) + "_raw"; - di = Directory.CreateDirectory(dirname); - } catch (IOException) { - di = null; - } - if (di != null) { - for (int i = 0; i < m_Frames.Count; ++i) { - WriteFrameAsPng(di, i); - } - } - } + public class GifEncodeTask + { + readonly int m_GifWidth; + readonly int m_GifHeight; + readonly string m_GifName; + // readonly bool m_Symmetric; Not actually needed at the moment + readonly int m_FrameDelayMs; + readonly bool m_bSavePngs = false; + readonly bool m_bPalettePerFrame = false; + readonly float m_DitherStrength; + + private List m_Frames; + + private float m_CreationPercent; + private Thread m_Thread; + private string m_ErrorMessage; + + /// If true, the output file has been written. + public bool IsDone { get { return !m_Thread.IsAlive; } } + + public string GifName { get { return m_GifName; } } + + /// An error message, or null if no error. + public string Error + { + get + { + if (!IsDone) + { + throw new InvalidOperationException("Not done"); + } + return m_ErrorMessage; + } + } - Directory.CreateDirectory(Path.GetDirectoryName(m_GifName)); - using (var encoder = new AnimatedGifEncoder32()) { - encoder.Start(m_GifName); - encoder.SetSize(m_GifWidth, m_GifHeight); - encoder.SetTransparent(new Color32(0,0,0,255), false); - encoder.SetDelay(m_FrameDelayMs); - encoder.SetRepeat(0); // infinite repeat - encoder.DitherStrength = m_DitherStrength; + /// A number in [0, 1] representing the state of the encode. + public float CreationPercent { get { return m_CreationPercent; } } + + /// gifName: A path to the output file (either full, or relative to cwd) + /// dither: A number from 0 to 1 (powers of 2 are best) + /// TODO(pld): when frameWidth == 500 and frameHeight == 500, the palettes seem to be + /// created incorrectly. + public GifEncodeTask( + List frames, int frameDelayMs, + int frameWidth, int frameHeight, string gifName, + float ditherStrength = 1f / 8, bool palettePerFrame = false) + { + m_DitherStrength = ditherStrength; + // Browsers (and monitors) won't show more than 60Hz + m_Frames = frames; + // Gif delay is expressed in 100ths of a second + m_FrameDelayMs = (int)Mathf.Round(frameDelayMs / 10f) * 10; + m_GifWidth = frameWidth; + m_GifHeight = frameHeight; + m_GifName = gifName; + m_bPalettePerFrame = palettePerFrame; + // m_Symmetric = IsSymmetric(frames); + + m_CreationPercent = 0.0f; + } - int nAdded = 0; - int nTotal = m_Frames.Count; + public void Start() + { + m_Thread = new Thread(Run); + m_Thread.Start(); + } - if (!m_bPalettePerFrame) { - nTotal *= 2; + private void WriteFrameAsPng(DirectoryInfo di, int i) + { + Color32[] frame = m_Frames[i]; + string filename = Path.Combine(di.FullName, string.Format("{0:00}.png", i)); + ImageInfo imi = new ImageInfo(m_GifWidth, m_GifHeight, 8, false); + PngWriter png = FileHelper.CreatePngWriter(filename, imi, true); + + byte[] row = new byte[m_GifWidth * 3]; + for (int iRow = 0; iRow < m_GifHeight; ++iRow) + { + int iStartPixel = (m_GifHeight - 1 - iRow) * m_GifWidth; + for (int iCol = 0; iCol < m_GifWidth; ++iCol) + { + Color32 c = frame[iStartPixel + iCol]; + row[iCol * 3 + 0] = c.r; + row[iCol * 3 + 1] = c.g; + row[iCol * 3 + 2] = c.b; + } + png.WriteRowByte(row, iRow); + } + + png.End(); + } - for (int i = 0; i < m_Frames.Count; ++i) { - encoder.PreAnalyzeFrame(m_Frames[i], m_GifWidth, m_GifHeight); - m_CreationPercent = (float)(++nAdded) / nTotal; + private void Run() + { + m_ErrorMessage = null; + try + { + RunLow(); + } + catch (IOException e) + { + m_ErrorMessage = e.Message; + } + catch (UnauthorizedAccessException e) + { + m_ErrorMessage = e.Message; + } + catch (Exception e) + { + m_ErrorMessage = "Unexpected error: " + e.Message; + } } - } - for (int i = 0; i < m_Frames.Count; ++i) { - encoder.AddFrame(m_Frames[i], m_GifWidth, m_GifHeight); - m_CreationPercent = (float)(++nAdded) / nTotal; - } + private void RunLow() + { + if (m_bSavePngs) + { + DirectoryInfo di; + try + { + string dirname = m_GifName.Substring(0, m_GifName.Length - 4) + "_raw"; + di = Directory.CreateDirectory(dirname); + } + catch (IOException) + { + di = null; + } + if (di != null) + { + for (int i = 0; i < m_Frames.Count; ++i) + { + WriteFrameAsPng(di, i); + } + } + } + + Directory.CreateDirectory(Path.GetDirectoryName(m_GifName)); + using (var encoder = new AnimatedGifEncoder32()) + { + encoder.Start(m_GifName); + encoder.SetSize(m_GifWidth, m_GifHeight); + encoder.SetTransparent(new Color32(0, 0, 0, 255), false); + encoder.SetDelay(m_FrameDelayMs); + encoder.SetRepeat(0); // infinite repeat + encoder.DitherStrength = m_DitherStrength; + + int nAdded = 0; + int nTotal = m_Frames.Count; + + if (!m_bPalettePerFrame) + { + nTotal *= 2; + + for (int i = 0; i < m_Frames.Count; ++i) + { + encoder.PreAnalyzeFrame(m_Frames[i], m_GifWidth, m_GifHeight); + m_CreationPercent = (float)(++nAdded) / nTotal; + } + } + + for (int i = 0; i < m_Frames.Count; ++i) + { + encoder.AddFrame(m_Frames[i], m_GifWidth, m_GifHeight); + m_CreationPercent = (float)(++nAdded) / nTotal; + } + + encoder.Finish(); + } + } - encoder.Finish(); - } - } - - // Helper function - // Return true if frames[i] == frames[Count-i] - static bool IsSymmetric(List frames) { - // Frame 0 would be symmetric with frames[Count], which doesn't exist. - for (int i = 1; i <= frames.Count / 2; ++i) { - var a = frames[i]; - var b = frames[frames.Count - i]; - if (a != b) { - return false; - } + // Helper function + // Return true if frames[i] == frames[Count-i] + static bool IsSymmetric(List frames) + { + // Frame 0 would be symmetric with frames[Count], which doesn't exist. + for (int i = 1; i <= frames.Count / 2; ++i) + { + var a = frames[i]; + var b = frames[frames.Count - i]; + if (a != b) + { + return false; + } + } + return true; + } } - return true; - } -} -internal class AnimatedGifEncoder32 : IDisposable -{ - public void Dispose() - { - throw new NotImplementedException(); - } - public void Start(string mGifName) - { - throw new NotImplementedException(); - } - public void SetSize(int mGifWidth, int mGifHeight) - { - throw new NotImplementedException(); - } - public void SetTransparent(Color32 color32, bool b) - { - throw new NotImplementedException(); - } - public void SetDelay(int mFrameDelayMs) - { - throw new NotImplementedException(); - } - public void SetRepeat(int i) - { - throw new NotImplementedException(); - } - public float DitherStrength { get; set; } - public void PreAnalyzeFrame(Color32[] mFrame, int mGifWidth, int mGifHeight) - { - throw new NotImplementedException(); - } - public void AddFrame(Color32[] mFrame, int mGifWidth, int mGifHeight) - { - throw new NotImplementedException(); - } - public void Finish() - { - throw new NotImplementedException(); - } -} + internal class AnimatedGifEncoder32 : IDisposable + { + public void Dispose() + { + throw new NotImplementedException(); + } + public void Start(string mGifName) + { + throw new NotImplementedException(); + } + public void SetSize(int mGifWidth, int mGifHeight) + { + throw new NotImplementedException(); + } + public void SetTransparent(Color32 color32, bool b) + { + throw new NotImplementedException(); + } + public void SetDelay(int mFrameDelayMs) + { + throw new NotImplementedException(); + } + public void SetRepeat(int i) + { + throw new NotImplementedException(); + } + public float DitherStrength { get; set; } + public void PreAnalyzeFrame(Color32[] mFrame, int mGifWidth, int mGifHeight) + { + throw new NotImplementedException(); + } + public void AddFrame(Color32[] mFrame, int mGifWidth, int mGifHeight) + { + throw new NotImplementedException(); + } + public void Finish() + { + throw new NotImplementedException(); + } + } } // namespace TiltBrush \ No newline at end of file diff --git a/Assets/Scripts/gif_creation/NeuQuant.cs b/Assets/Scripts/gif_creation/NeuQuant.cs index e3cfff42..97aa056f 100644 --- a/Assets/Scripts/gif_creation/NeuQuant.cs +++ b/Assets/Scripts/gif_creation/NeuQuant.cs @@ -15,425 +15,483 @@ using UnityEngine; using System; -namespace TiltBrush { - -/// Usage: -/// var nq = new NeuQuant(); -/// nq.Learn(image); // Call one or more times -/// nq.FinishLearning(); -/// index = nq.Map(color); // Call one or more times -/// byte[] map = nq.ColorMap(); -public class NeuQuant { - struct Pixel { - public int b, g, r; - /// Palette index - public int c; - } - - const int kNetSize = 256; /* number of colours used */ - /* four primes near 500 - assume no image has a length so large */ - /* that it is divisible by all four primes */ - const int kPrime1 = 499; - const int kPrime2 = 491; - const int kPrime3 = 487; - const int kPrime4 = 503; - const int kMinPixels = kPrime4; - - - /* net Definitions - ------------------- */ - const int kMaxNetPos = (kNetSize - 1); - const int kNetBiasShift = 4; /* bias for colour values */ - const int kNumCycles = 100; /* no. of learning cycles */ - - /* defs for freq and bias */ - const int kIntBiasShift = 16; /* bias for fractions */ - const int kIntBias = (((int)1) << kIntBiasShift); - const int kGammaShift = 10; /* gamma = 1024 */ - const int kGamma = (((int)1) << kGammaShift); - const int kBetaShift = 10; - const int kBeta = (kIntBias >> kBetaShift); /* kBeta = 1/1024 */ - const int kBetaGamma = (kIntBias << (kGammaShift - kBetaShift)); - - /* defs for decreasing radius factor */ - const int kInitRad = (kNetSize >> 3); /* for 256 cols, radius starts */ - const int kRadiusBiasShift = 6; /* at 32.0 biased by 6 bits */ - const int kRadiusBias = (((int)1) << kRadiusBiasShift); - const int kInitRadius = (kInitRad * kRadiusBias); /* and decreases by a */ - const int kRadiuscDec = 30; /* factor of 1/30 each cycle */ - - /* defs for decreasing alpha factor */ - const int kAlphaBiasShift = 10; /* alpha starts at 1.0 */ - const int kInitAlpha = (((int)1) << kAlphaBiasShift); - - /* kRadBias and kAlphaRadBias used for m_radpower calculation */ - const int kRadBiasShift = 8; - const int kRadBias = (((int)1) << kRadBiasShift); - const int kAlphaRadBShift = (kAlphaBiasShift + kRadBiasShift); - const int kAlphaRadBias = (((int)1) << kAlphaRadBShift); - - // Preallocate for avoiding garbage - int[] m_radPowerBuffer = new int[kInitRad]; - - Pixel[] m_net = new Pixel[kNetSize]; - - // For each green level, returns the index of the Pixel closest to that green. - // Pixels will be sorted by green - int[] m_netindex = new int[256]; - - /* bias and freq arrays for learning */ - int[] m_bias = new int[kNetSize]; - int[] m_freq = new int[kNetSize]; - - bool m_learning; - - public bool IsLearning { - get { return m_learning; } - } - - /* Initialise m_net in range (0,0,0) to (255,255,255) and set parameters - ----------------------------------------------------------------------- */ - public NeuQuant() { - int i; - - for (i = 0; i < kNetSize; i++) { - int val = (i << (kNetBiasShift + 8)) / kNetSize; - m_net[i] = new Pixel { b=val, g=val, r=val, c=-1 }; - - m_freq[i] = kIntBias / kNetSize; /* 1/kNetSize */ - m_bias[i] = 0; - } - m_learning = true; - } - - /// Transition from analyzing data to performing color map lookup. - /// After this, Learn() may not be called. - /// After this, Map() may be called. - public void FinishLearning() { - Debug.Assert(m_learning); - UnbiasNet(); - BuildIndex(); - m_learning = false; - } - - /// Create and return a color map, in "RGB" order the way Gif wants it - public byte[] ColorMap() { - if (m_learning) { - FinishLearning(); - } +namespace TiltBrush +{ + + /// Usage: + /// var nq = new NeuQuant(); + /// nq.Learn(image); // Call one or more times + /// nq.FinishLearning(); + /// index = nq.Map(color); // Call one or more times + /// byte[] map = nq.ColorMap(); + public class NeuQuant + { + struct Pixel + { + public int b, g, r; + /// Palette index + public int c; + } - int[] index = new int[kNetSize]; - for (int i = 0; i < kNetSize; i++) { - index[m_net[i].c] = i; - } + const int kNetSize = 256; /* number of colours used */ + /* four primes near 500 - assume no image has a length so large */ + /* that it is divisible by all four primes */ + const int kPrime1 = 499; + const int kPrime2 = 491; + const int kPrime3 = 487; + const int kPrime4 = 503; + const int kMinPixels = kPrime4; + + + /* net Definitions + ------------------- */ + const int kMaxNetPos = (kNetSize - 1); + const int kNetBiasShift = 4; /* bias for colour values */ + const int kNumCycles = 100; /* no. of learning cycles */ + + /* defs for freq and bias */ + const int kIntBiasShift = 16; /* bias for fractions */ + const int kIntBias = (((int)1) << kIntBiasShift); + const int kGammaShift = 10; /* gamma = 1024 */ + const int kGamma = (((int)1) << kGammaShift); + const int kBetaShift = 10; + const int kBeta = (kIntBias >> kBetaShift); /* kBeta = 1/1024 */ + const int kBetaGamma = (kIntBias << (kGammaShift - kBetaShift)); + + /* defs for decreasing radius factor */ + const int kInitRad = (kNetSize >> 3); /* for 256 cols, radius starts */ + const int kRadiusBiasShift = 6; /* at 32.0 biased by 6 bits */ + const int kRadiusBias = (((int)1) << kRadiusBiasShift); + const int kInitRadius = (kInitRad * kRadiusBias); /* and decreases by a */ + const int kRadiuscDec = 30; /* factor of 1/30 each cycle */ + + /* defs for decreasing alpha factor */ + const int kAlphaBiasShift = 10; /* alpha starts at 1.0 */ + const int kInitAlpha = (((int)1) << kAlphaBiasShift); + + /* kRadBias and kAlphaRadBias used for m_radpower calculation */ + const int kRadBiasShift = 8; + const int kRadBias = (((int)1) << kRadBiasShift); + const int kAlphaRadBShift = (kAlphaBiasShift + kRadBiasShift); + const int kAlphaRadBias = (((int)1) << kAlphaRadBShift); + + // Preallocate for avoiding garbage + int[] m_radPowerBuffer = new int[kInitRad]; + + Pixel[] m_net = new Pixel[kNetSize]; + + // For each green level, returns the index of the Pixel closest to that green. + // Pixels will be sorted by green + int[] m_netindex = new int[256]; + + /* bias and freq arrays for learning */ + int[] m_bias = new int[kNetSize]; + int[] m_freq = new int[kNetSize]; + + bool m_learning; + + public bool IsLearning + { + get { return m_learning; } + } - int k = 0; - byte[] map = new byte[3 * kNetSize]; - for (int i = 0; i < kNetSize; i++) { - int j = index[i]; - map[k++] = (byte)(m_net[j].r); - map[k++] = (byte)(m_net[j].g); - map[k++] = (byte)(m_net[j].b); - } + /* Initialise m_net in range (0,0,0) to (255,255,255) and set parameters + ----------------------------------------------------------------------- */ + public NeuQuant() + { + int i; - return map; - } - - /* Unbias m_net to give byte values 0..255 and record position i to prepare for sort - ----------------------------------------------------------------------------------- */ - void UnbiasNet() { - for (int i = 0; i < kNetSize; i++) { - m_net[i].b >>= kNetBiasShift; - m_net[i].g >>= kNetBiasShift; - m_net[i].r >>= kNetBiasShift; - m_net[i].c = i; /* record colour no */ - } - } - - /* Insertion sort of m_net and building of m_netindex[0..255] (to do after unbias) - ------------------------------------------------------------------------------- */ - void BuildIndex() { - int previouscol = 0; - int startpos = 0; - - unsafe { - fixed (Pixel* aPixel = m_net) { - for (int i = 0; i < kNetSize; i++) { - Pixel* p = &aPixel[i]; - int smallpos = i; - int smallval = p->g; /* index on g */ - /* find smallest in i..kNetSize-1 */ - for (int j = i + 1; j < kNetSize; j++) { - Pixel* q = &aPixel[j]; - if (q->g < smallval) { /* index on g */ - smallpos = j; - smallval = q->g; /* index on g */ + for (i = 0; i < kNetSize; i++) + { + int val = (i << (kNetBiasShift + 8)) / kNetSize; + m_net[i] = new Pixel { b = val, g = val, r = val, c = -1 }; + + m_freq[i] = kIntBias / kNetSize; /* 1/kNetSize */ + m_bias[i] = 0; + } + m_learning = true; + } + + /// Transition from analyzing data to performing color map lookup. + /// After this, Learn() may not be called. + /// After this, Map() may be called. + public void FinishLearning() + { + Debug.Assert(m_learning); + UnbiasNet(); + BuildIndex(); + m_learning = false; + } + + /// Create and return a color map, in "RGB" order the way Gif wants it + public byte[] ColorMap() + { + if (m_learning) + { + FinishLearning(); + } + + int[] index = new int[kNetSize]; + for (int i = 0; i < kNetSize; i++) + { + index[m_net[i].c] = i; } - } - /* swap p (i) and q (smallpos) entries */ - if (i != smallpos) { - Pixel* q = &aPixel[smallpos]; - Pixel oldq = *q; - *q = *p; - *p = oldq; - } - /* smallval entry is now in position i */ - if (smallval != previouscol) { - m_netindex[previouscol] = (startpos + i) >> 1; - for (int j = previouscol + 1; j < smallval; j++) { - m_netindex[j] = i; + + int k = 0; + byte[] map = new byte[3 * kNetSize]; + for (int i = 0; i < kNetSize; i++) + { + int j = index[i]; + map[k++] = (byte)(m_net[j].r); + map[k++] = (byte)(m_net[j].g); + map[k++] = (byte)(m_net[j].b); } - previouscol = smallval; - startpos = i; - } + + return map; } - } - } - m_netindex[previouscol] = (startpos + kMaxNetPos) >> 1; - for (int j = previouscol + 1; j < 256; j++) { - m_netindex[j] = kMaxNetPos; /* really 256 */ - } - } - /* Main Learning Loop - ------------------ */ + /* Unbias m_net to give byte values 0..255 and record position i to prepare for sort + ----------------------------------------------------------------------------------- */ + void UnbiasNet() + { + for (int i = 0; i < kNetSize; i++) + { + m_net[i].b >>= kNetBiasShift; + m_net[i].g >>= kNetBiasShift; + m_net[i].r >>= kNetBiasShift; + m_net[i].c = i; /* record colour no */ + } + } - void radiusAlphaToRad(int radius, int alpha, out int rad, int[] radpower) { - rad = radius >> kRadiusBiasShift; - if (rad <= 1) { - rad = 0; - } - for (int i = 0; i < rad; i++) { - radpower[i] = alpha * (((rad * rad - i * i) * kRadBias) / (rad * rad)); - } - } - - // Returns number that is coprime to i (might be 1) - // Tries to return a number near 500 (why? cache effects?) - static int FindCoprime(int i) { - if ((i % kPrime1) != 0) { return kPrime1; } - if ((i % kPrime2) != 0) { return kPrime2; } - if ((i % kPrime3) != 0) { return kPrime3; } - if ((i % kPrime4) != 0) { return kPrime4; } - return 1; - } - - /// Sample factor is in [1, 30] - /// Higher values reduce the number of pixels sampled. - public void Learn(Color32[] pixels, int sampleFactor) { - Debug.Assert(m_learning); - int nPixels = pixels.Length; - sampleFactor = (nPixels < kMinPixels) ? 1 : sampleFactor; - sampleFactor = Mathf.Clamp(sampleFactor, 1, 30); - - int nSamplePixels = nPixels / sampleFactor; - int alphaDec = 30 + ((sampleFactor - 1) / 3); - int pixelsPerCycle = Mathf.Max(1, nSamplePixels / kNumCycles); - - // mutable state - int alpha = kInitAlpha; - int radius = kInitRadius; - int rad; - int[] radpower = m_radPowerBuffer; - radiusAlphaToRad(radius, alpha, out rad, radpower); - - int step = (nPixels < kMinPixels) ? 1 : FindCoprime(nPixels); - for (int i = 0, iPixel = 0; i < nSamplePixels; ) { - int b = pixels[iPixel].b << kNetBiasShift; - int g = pixels[iPixel].g << kNetBiasShift; - int r = pixels[iPixel].r << kNetBiasShift; - int neuron = Contest(b, g, r); - - AlterSingle(alpha, neuron, b, g, r); - if (rad != 0) { - AlterNeighbors(rad, radpower, neuron, b, g, r); - } - - iPixel = (iPixel + step) % nPixels; - i++; - - if (i % pixelsPerCycle == 0) { - alpha -= alpha / alphaDec; - radius -= radius / kRadiuscDec; - radiusAlphaToRad(radius, alpha, out rad, radpower); - } - } - } - - /// Convert color value to color index - public int Map(Color32 c) { - int b = c.b; - int g = c.g; - int r = c.r; - - // TODO: this uses manhattan distance; maybe it should use euclidean? - // TODO: KD tree instead? - - int dist, a, bestd; - int best; - - bestd = 1000; /* biggest possible dist is 256 * 3 */ - best = -1; - - int i = m_netindex[g]; // moves forward to end - int j = i - 1; // moves backward to beginning - - while ((i < kNetSize) || (j >= 0)) { - if (i < kNetSize) { - Pixel p = m_net[i]; - dist = p.g - g; /* inx key */ - if (dist >= bestd) { - i = kNetSize; /* stop iter */ - } else { - i++; - if (dist < 0) - dist = -dist; - a = p.b - b; - if (a < 0) - a = -a; - dist += a; - if (dist < bestd) { - a = p.r - r; - if (a < 0) - a = -a; - dist += a; - if (dist < bestd) { - bestd = dist; - best = p.c; + /* Insertion sort of m_net and building of m_netindex[0..255] (to do after unbias) + ------------------------------------------------------------------------------- */ + void BuildIndex() + { + int previouscol = 0; + int startpos = 0; + + unsafe + { + fixed (Pixel* aPixel = m_net) + { + for (int i = 0; i < kNetSize; i++) + { + Pixel* p = &aPixel[i]; + int smallpos = i; + int smallval = p->g; /* index on g */ + /* find smallest in i..kNetSize-1 */ + for (int j = i + 1; j < kNetSize; j++) + { + Pixel* q = &aPixel[j]; + if (q->g < smallval) + { /* index on g */ + smallpos = j; + smallval = q->g; /* index on g */ + } + } + /* swap p (i) and q (smallpos) entries */ + if (i != smallpos) + { + Pixel* q = &aPixel[smallpos]; + Pixel oldq = *q; + *q = *p; + *p = oldq; + } + /* smallval entry is now in position i */ + if (smallval != previouscol) + { + m_netindex[previouscol] = (startpos + i) >> 1; + for (int j = previouscol + 1; j < smallval; j++) + { + m_netindex[j] = i; + } + previouscol = smallval; + startpos = i; + } + } + } + } + m_netindex[previouscol] = (startpos + kMaxNetPos) >> 1; + for (int j = previouscol + 1; j < 256; j++) + { + m_netindex[j] = kMaxNetPos; /* really 256 */ } - } } - } - - if (j >= 0) { - Pixel p = m_net[j]; - dist = g - p.g; /* inx key - reverse dif */ - if (dist >= bestd) - j = -1; /* stop iter */ - else { - j--; - if (dist < 0) - dist = -dist; - a = p.b - b; - if (a < 0) - a = -a; - dist += a; - if (dist < bestd) { - a = p.r - r; - if (a < 0) - a = -a; - dist += a; - if (dist < bestd) { - bestd = dist; - best = p.c; + + /* Main Learning Loop + ------------------ */ + + void radiusAlphaToRad(int radius, int alpha, out int rad, int[] radpower) + { + rad = radius >> kRadiusBiasShift; + if (rad <= 1) + { + rad = 0; + } + for (int i = 0; i < rad; i++) + { + radpower[i] = alpha * (((rad * rad - i * i) * kRadBias) / (rad * rad)); } - } } - } - } - return (best); - } - - /* Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in m_radpower[|i-j|] - --------------------------------------------------------------------------------- */ - void AlterNeighbors(int rad, int[] radpower, int i, int b, int g, int r) { - - int j, k, lo, hi, a, m; - - lo = i - rad; - if (lo < -1) - lo = -1; - hi = i + rad; - if (hi > kNetSize) - hi = kNetSize; - - j = i + 1; - k = i - 1; - m = 1; - while ((j < hi) || (k > lo)) { - a = radpower[m++]; - if (j < hi) { - Pixel p = m_net[j]; - try { - p.b -= (a * (p.b - b)) / kAlphaRadBias; - p.g -= (a * (p.g - g)) / kAlphaRadBias; - p.r -= (a * (p.r - r)) / kAlphaRadBias; - } catch (Exception e) { - Debug.Log(e); + + // Returns number that is coprime to i (might be 1) + // Tries to return a number near 500 (why? cache effects?) + static int FindCoprime(int i) + { + if ((i % kPrime1) != 0) { return kPrime1; } + if ((i % kPrime2) != 0) { return kPrime2; } + if ((i % kPrime3) != 0) { return kPrime3; } + if ((i % kPrime4) != 0) { return kPrime4; } + return 1; } - m_net[j++] = p; - } - if (k > lo) { - Pixel p = m_net[k]; - try { - p.b -= (a * (p.b - b)) / kAlphaRadBias; - p.g -= (a * (p.g - g)) / kAlphaRadBias; - p.r -= (a * (p.r - r)) / kAlphaRadBias; - } catch (Exception e) { - Debug.Log(e); + + /// Sample factor is in [1, 30] + /// Higher values reduce the number of pixels sampled. + public void Learn(Color32[] pixels, int sampleFactor) + { + Debug.Assert(m_learning); + int nPixels = pixels.Length; + sampleFactor = (nPixels < kMinPixels) ? 1 : sampleFactor; + sampleFactor = Mathf.Clamp(sampleFactor, 1, 30); + + int nSamplePixels = nPixels / sampleFactor; + int alphaDec = 30 + ((sampleFactor - 1) / 3); + int pixelsPerCycle = Mathf.Max(1, nSamplePixels / kNumCycles); + + // mutable state + int alpha = kInitAlpha; + int radius = kInitRadius; + int rad; + int[] radpower = m_radPowerBuffer; + radiusAlphaToRad(radius, alpha, out rad, radpower); + + int step = (nPixels < kMinPixels) ? 1 : FindCoprime(nPixels); + for (int i = 0, iPixel = 0; i < nSamplePixels;) + { + int b = pixels[iPixel].b << kNetBiasShift; + int g = pixels[iPixel].g << kNetBiasShift; + int r = pixels[iPixel].r << kNetBiasShift; + int neuron = Contest(b, g, r); + + AlterSingle(alpha, neuron, b, g, r); + if (rad != 0) + { + AlterNeighbors(rad, radpower, neuron, b, g, r); + } + + iPixel = (iPixel + step) % nPixels; + i++; + + if (i % pixelsPerCycle == 0) + { + alpha -= alpha / alphaDec; + radius -= radius / kRadiuscDec; + radiusAlphaToRad(radius, alpha, out rad, radpower); + } + } + } + + /// Convert color value to color index + public int Map(Color32 c) + { + int b = c.b; + int g = c.g; + int r = c.r; + + // TODO: this uses manhattan distance; maybe it should use euclidean? + // TODO: KD tree instead? + + int dist, a, bestd; + int best; + + bestd = 1000; /* biggest possible dist is 256 * 3 */ + best = -1; + + int i = m_netindex[g]; // moves forward to end + int j = i - 1; // moves backward to beginning + + while ((i < kNetSize) || (j >= 0)) + { + if (i < kNetSize) + { + Pixel p = m_net[i]; + dist = p.g - g; /* inx key */ + if (dist >= bestd) + { + i = kNetSize; /* stop iter */ + } + else + { + i++; + if (dist < 0) + dist = -dist; + a = p.b - b; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) + { + a = p.r - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) + { + bestd = dist; + best = p.c; + } + } + } + } + + if (j >= 0) + { + Pixel p = m_net[j]; + dist = g - p.g; /* inx key - reverse dif */ + if (dist >= bestd) + j = -1; /* stop iter */ + else + { + j--; + if (dist < 0) + dist = -dist; + a = p.b - b; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) + { + a = p.r - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) + { + bestd = dist; + best = p.c; + } + } + } + } + } + return (best); + } + + /* Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in m_radpower[|i-j|] + --------------------------------------------------------------------------------- */ + void AlterNeighbors(int rad, int[] radpower, int i, int b, int g, int r) + { + + int j, k, lo, hi, a, m; + + lo = i - rad; + if (lo < -1) + lo = -1; + hi = i + rad; + if (hi > kNetSize) + hi = kNetSize; + + j = i + 1; + k = i - 1; + m = 1; + while ((j < hi) || (k > lo)) + { + a = radpower[m++]; + if (j < hi) + { + Pixel p = m_net[j]; + try + { + p.b -= (a * (p.b - b)) / kAlphaRadBias; + p.g -= (a * (p.g - g)) / kAlphaRadBias; + p.r -= (a * (p.r - r)) / kAlphaRadBias; + } + catch (Exception e) + { + Debug.Log(e); + } + m_net[j++] = p; + } + if (k > lo) + { + Pixel p = m_net[k]; + try + { + p.b -= (a * (p.b - b)) / kAlphaRadBias; + p.g -= (a * (p.g - g)) / kAlphaRadBias; + p.r -= (a * (p.r - r)) / kAlphaRadBias; + } + catch (Exception e) + { + Debug.Log(e); + } + m_net[k--] = p; + } + } + } + + /* Move neuron i towards biased (b,g,r) by factor alpha + ---------------------------------------------------- */ + protected void AlterSingle(int alpha, int i, int b, int g, int r) + { + /* alter hit neuron */ + Pixel n = m_net[i]; + n.b -= (alpha * (n.b - b)) / kInitAlpha; + n.g -= (alpha * (n.g - g)) / kInitAlpha; + n.r -= (alpha * (n.r - r)) / kInitAlpha; + m_net[i] = n; + } + + /* Search for biased BGR values + ---------------------------- */ + protected int Contest(int b, int g, int r) + { + + /* finds closest neuron (min dist) and updates freq */ + /* finds best neuron (min dist-bias) and returns position */ + /* for frequently chosen neurons, freq[i] is high and bias[i] is negative */ + /* bias[i] = kGamma*((1/kNetSize)-freq[i]) */ + + int i, dist, a, biasdist, betafreq; + int bestpos, bestbiaspos, bestd, bestbiasd; + + bestd = ~(((int)1) << 31); + bestbiasd = bestd; + bestpos = -1; + bestbiaspos = bestpos; + + for (i = 0; i < kNetSize; i++) + { + Pixel n = m_net[i]; + dist = n.b - b; + if (dist < 0) + dist = -dist; + a = n.g - g; + if (a < 0) + a = -a; + dist += a; + a = n.r - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) + { + bestd = dist; + bestpos = i; + } + biasdist = dist - ((m_bias[i]) >> (kIntBiasShift - kNetBiasShift)); + if (biasdist < bestbiasd) + { + bestbiasd = biasdist; + bestbiaspos = i; + } + betafreq = (m_freq[i] >> kBetaShift); + m_freq[i] -= betafreq; + m_bias[i] += (betafreq << kGammaShift); + } + m_freq[bestpos] += kBeta; + m_bias[bestpos] -= kBetaGamma; + return (bestbiaspos); } - m_net[k--] = p; - } - } - } - - /* Move neuron i towards biased (b,g,r) by factor alpha - ---------------------------------------------------- */ - protected void AlterSingle(int alpha, int i, int b, int g, int r) { - /* alter hit neuron */ - Pixel n = m_net[i]; - n.b -= (alpha * (n.b - b)) / kInitAlpha; - n.g -= (alpha * (n.g - g)) / kInitAlpha; - n.r -= (alpha * (n.r - r)) / kInitAlpha; - m_net[i] = n; - } - - /* Search for biased BGR values - ---------------------------- */ - protected int Contest(int b, int g, int r) { - - /* finds closest neuron (min dist) and updates freq */ - /* finds best neuron (min dist-bias) and returns position */ - /* for frequently chosen neurons, freq[i] is high and bias[i] is negative */ - /* bias[i] = kGamma*((1/kNetSize)-freq[i]) */ - - int i, dist, a, biasdist, betafreq; - int bestpos, bestbiaspos, bestd, bestbiasd; - - bestd = ~(((int)1) << 31); - bestbiasd = bestd; - bestpos = -1; - bestbiaspos = bestpos; - - for (i = 0; i < kNetSize; i++) { - Pixel n = m_net[i]; - dist = n.b - b; - if (dist < 0) - dist = -dist; - a = n.g - g; - if (a < 0) - a = -a; - dist += a; - a = n.r - r; - if (a < 0) - a = -a; - dist += a; - if (dist < bestd) { - bestd = dist; - bestpos = i; - } - biasdist = dist - ((m_bias[i]) >> (kIntBiasShift - kNetBiasShift)); - if (biasdist < bestbiasd) { - bestbiasd = biasdist; - bestbiaspos = i; - } - betafreq = (m_freq[i] >> kBetaShift); - m_freq[i] -= betafreq; - m_bias[i] += (betafreq << kGammaShift); } - m_freq[bestpos] += kBeta; - m_bias[bestpos] -= kBetaGamma; - return (bestbiaspos); - } -} } // namespace TiltBrush diff --git a/Assets/Scripts/guides/GridPlaneAnchor.cs b/Assets/Scripts/guides/GridPlaneAnchor.cs index 3a0ad281..6526602a 100644 --- a/Assets/Scripts/guides/GridPlaneAnchor.cs +++ b/Assets/Scripts/guides/GridPlaneAnchor.cs @@ -25,250 +25,287 @@ /// A grid plane consistes of the visual grid with a child "anchor". A grid plane anchor is the interaction /// point for the grid plane to which the anchor is nested. /// -public class GridPlaneAnchor : MonoBehaviour { - - /// - /// Axis type to specify to which axis the plane belongs. - /// - public enum AxisType { - X = 0, - Y = 1, - Z = 2 - } - - // Immutable marks the object as a source for generating new instances and are not moved themselves. - public bool isImmutable = true; - public AxisType axisType; - /// - /// Holds a lookup of the currently cloned planes, the root GameObject of a "GridPlane" to which the anchor is parented, - /// which is used for de-duping along a particular axis type. This dictionary is populated for any source grid planes, i.e. a plane that spawns another plane. - /// - public Dictionary planeClones; - - private bool isSetup = false; - private bool isHovered = false; - private bool isHeld = false; - private bool isGrabFrameUpdate = false; - private bool isReleaseFrameUpdate = false; - - private Material planeMaterial; - private Material anchorMaterial; - private TextMeshPro label; - private GridPlaneAnchor rootGridPlaneAnchor; - - void Start() { - Setup(); - } - - private void Setup() { - PeltzerMain.Instance.peltzerController.PeltzerControllerActionHandler += ControllerEventHandler; - planeMaterial = transform.parent.GetComponent().material; - label = transform.parent.Find("Label").gameObject.GetComponent(); - if (isImmutable) { - planeClones = new Dictionary(); +public class GridPlaneAnchor : MonoBehaviour +{ + + /// + /// Axis type to specify to which axis the plane belongs. + /// + public enum AxisType + { + X = 0, + Y = 1, + Z = 2 + } + + // Immutable marks the object as a source for generating new instances and are not moved themselves. + public bool isImmutable = true; + public AxisType axisType; + /// + /// Holds a lookup of the currently cloned planes, the root GameObject of a "GridPlane" to which the anchor is parented, + /// which is used for de-duping along a particular axis type. This dictionary is populated for any source grid planes, i.e. a plane that spawns another plane. + /// + public Dictionary planeClones; + + private bool isSetup = false; + private bool isHovered = false; + private bool isHeld = false; + private bool isGrabFrameUpdate = false; + private bool isReleaseFrameUpdate = false; + + private Material planeMaterial; + private Material anchorMaterial; + private TextMeshPro label; + private GridPlaneAnchor rootGridPlaneAnchor; + + void Start() + { + Setup(); } - isSetup = true; - } - - void Update() { - if (!isSetup) return; - - isHovered = IsCollidedWithSelector(); - - if (!isHovered && isImmutable) { - isHeld = false; + + private void Setup() + { + PeltzerMain.Instance.peltzerController.PeltzerControllerActionHandler += ControllerEventHandler; + planeMaterial = transform.parent.GetComponent().material; + label = transform.parent.Find("Label").gameObject.GetComponent(); + if (isImmutable) + { + planeClones = new Dictionary(); + } + isSetup = true; + } + + void Update() + { + if (!isSetup) return; + + isHovered = IsCollidedWithSelector(); + + if (!isHovered && isImmutable) + { + isHeld = false; + } + + // Remove the reference. + if (isGrabFrameUpdate && !isImmutable) + { + isGrabFrameUpdate = false; + if (rootGridPlaneAnchor.planeClones.ContainsKey(transform.parent.localPosition[(int)axisType])) + { + rootGridPlaneAnchor.planeClones.Remove(transform.parent.localPosition[(int)axisType]); + } + } + + if (isHeld && !isImmutable) + { + // Set zoomer flag to show world bounds while held. + PeltzerMain.Instance.Zoomer.isManipulatingGridPlane = true; + + // Snap grid plane to grid. + Vector3 newLSPos = transform.parent.parent.parent + .InverseTransformPoint(PeltzerMain.Instance.worldSpace + .ModelToWorld(PeltzerMain.Instance.GetSelector().selectorPosition)); + + switch (axisType) + { + case AxisType.X: + newLSPos = new Vector3(newLSPos.x, + transform.parent.transform.localPosition.y, + transform.parent.transform.localPosition.z); + break; + case AxisType.Y: + newLSPos = new Vector3(transform.parent.transform.localPosition.x, + newLSPos.y, + transform.parent.transform.localPosition.z); + break; + case AxisType.Z: + newLSPos = new Vector3(transform.parent.transform.localPosition.x, + transform.parent.transform.localPosition.y, + newLSPos.z); + break; + } + + // Clamp precision @ .3 + newLSPos = new Vector3(Mathf.RoundToInt(newLSPos.x * 1000.000f) / 1000.000f, + Mathf.RoundToInt(newLSPos.y * 1000.000f) / 1000.000f, + Mathf.RoundToInt(newLSPos.z * 1000.000f) / 1000.000f); + + transform.parent.localPosition = newLSPos; + + // Display value. + SetLabel(newLSPos[(int)axisType].ToString("F3") + " bu"); + } + else if (isHovered && !isImmutable) + { + // Display value. + SetLabel(transform.parent.localPosition[(int)axisType].ToString("F3") + " bu"); + } + else + { + // Clear label. + SetLabel(""); + } + + // Pass material to shader// Get world position of selector position. + Vector4 selectorWorldPosition = PeltzerMain.Instance.worldSpace + .ModelToWorld(PeltzerMain.Instance.peltzerController.LastPositionModel); + selectorWorldPosition.w = PeltzerMain.Instance.peltzerController.isBlockMode ? 0 : 1; + planeMaterial.SetVector("_SelectorPosition", selectorWorldPosition); + + // check if it is overlapping + if (isReleaseFrameUpdate && !isImmutable) + { + isReleaseFrameUpdate = false; + if (IsOverlappingPlaneOnSameAxis()) + { + DestroyGridPlane(); + } + else + { + rootGridPlaneAnchor.planeClones.Add(transform.parent.localPosition[(int)axisType], transform.parent.gameObject); + } + } } - // Remove the reference. - if (isGrabFrameUpdate && !isImmutable) { - isGrabFrameUpdate = false; - if (rootGridPlaneAnchor.planeClones.ContainsKey(transform.parent.localPosition[(int)axisType])) { - rootGridPlaneAnchor.planeClones.Remove(transform.parent.localPosition[(int)axisType]); - } + /// + /// Set the label text from position information. + /// + /// The string to to display in the label. + private void SetLabel(string text) + { + label.gameObject.transform.LookAt(PeltzerMain.Instance.hmd.transform); + label.text = text; } - if (isHeld && !isImmutable) { - // Set zoomer flag to show world bounds while held. - PeltzerMain.Instance.Zoomer.isManipulatingGridPlane = true; - - // Snap grid plane to grid. - Vector3 newLSPos = transform.parent.parent.parent - .InverseTransformPoint(PeltzerMain.Instance.worldSpace - .ModelToWorld(PeltzerMain.Instance.GetSelector().selectorPosition)); - - switch (axisType) { - case AxisType.X: - newLSPos = new Vector3(newLSPos.x, - transform.parent.transform.localPosition.y, - transform.parent.transform.localPosition.z); - break; - case AxisType.Y: - newLSPos = new Vector3(transform.parent.transform.localPosition.x, - newLSPos.y, - transform.parent.transform.localPosition.z); - break; - case AxisType.Z: - newLSPos = new Vector3(transform.parent.transform.localPosition.x, - transform.parent.transform.localPosition.y, - newLSPos.z); - break; - } - - // Clamp precision @ .3 - newLSPos = new Vector3(Mathf.RoundToInt(newLSPos.x * 1000.000f) / 1000.000f, - Mathf.RoundToInt(newLSPos.y * 1000.000f) / 1000.000f, - Mathf.RoundToInt(newLSPos.z * 1000.000f) / 1000.000f); - - transform.parent.localPosition = newLSPos; - - // Display value. - SetLabel(newLSPos[(int)axisType].ToString("F3") + " bu"); - } else if (isHovered && !isImmutable) { - // Display value. - SetLabel(transform.parent.localPosition[(int)axisType].ToString("F3") + " bu"); - } else { - // Clear label. - SetLabel(""); + + /// + /// Creates a new grid plane along a coordinate axis. + /// + private void SpawnNewGridPlane() + { + // Clone and set transform. + GameObject planeClone = GameObject.Instantiate(transform.parent).gameObject; + planeClone.transform.Find("GridAnchor").GetComponent().rootGridPlaneAnchor = this; + GridPlaneAnchor gridPlaneAnchor = planeClone.transform.Find("GridAnchor").GetComponent(); + gridPlaneAnchor.isImmutable = false; + gridPlaneAnchor.isHeld = true; + gridPlaneAnchor.isHovered = true; + planeClone.transform.parent = transform.parent.parent; + planeClone.transform.localEulerAngles = transform.parent.transform.localEulerAngles; + planeClone.transform.position = transform.parent.transform.position; + planeClone.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f); + gridPlaneAnchor.Setup(); + + // Reset the interactions on this plane. + if (isImmutable) + { + isHeld = false; + isHovered = false; + } } - // Pass material to shader// Get world position of selector position. - Vector4 selectorWorldPosition = PeltzerMain.Instance.worldSpace - .ModelToWorld(PeltzerMain.Instance.peltzerController.LastPositionModel); - selectorWorldPosition.w = PeltzerMain.Instance.peltzerController.isBlockMode ? 0 : 1; - planeMaterial.SetVector("_SelectorPosition", selectorWorldPosition); - - // check if it is overlapping - if(isReleaseFrameUpdate && !isImmutable) { - isReleaseFrameUpdate = false; - if (IsOverlappingPlaneOnSameAxis()) { - DestroyGridPlane(); - } else { - rootGridPlaneAnchor.planeClones.Add(transform.parent.localPosition[(int)axisType], transform.parent.gameObject); - } + // Handle mode selection when appropriate. + private void ControllerEventHandler(object sender, ControllerEventArgs args) + { + if (IsGrabbingAnchor(args)) + { + // Disable multiselection. + PeltzerMain.Instance.GetSelector().EndMultiSelection(); + + // As we use the eraser on spawned clones to provide a "delete" method, we exclude + // any delete tool (eraser) from spawning to provide clarity. + if (isImmutable && PeltzerMain.Instance.peltzerController.mode != ControllerMode.delete) + { + SpawnNewGridPlane(); + } + else if (!isImmutable) + { + isHeld = true; + // if tool is eraser, delete this plane via parent. + if (PeltzerMain.Instance.peltzerController.mode == ControllerMode.delete) + { + DestroyGridPlane(); + return; + } + + // Set grab check semaphore for next update. + // This semaphore is used to check for duplicate planes at same level. + isGrabFrameUpdate = true; + } + } + else if (IsReleasingAnchor(args)) + { + isHeld = false; + PeltzerMain.Instance.Zoomer.isManipulatingGridPlane = false; + + // If plane is set outside of world space, destroy it. + if (!isImmutable && + (Mathf.Abs(transform.parent.localPosition[(int)axisType]) >= 0.5f || IsOverlappingPlaneOnSameAxis())) + { + DestroyGridPlane(); + return; + } + + // Set release check semaphore for next update. + // This semaphore is used to check for duplicate planes at same level. + isReleaseFrameUpdate = true; + } } - } - - /// - /// Set the label text from position information. - /// - /// The string to to display in the label. - private void SetLabel(string text) { - label.gameObject.transform.LookAt(PeltzerMain.Instance.hmd.transform); - label.text = text; - } - - - /// - /// Creates a new grid plane along a coordinate axis. - /// - private void SpawnNewGridPlane() { - // Clone and set transform. - GameObject planeClone = GameObject.Instantiate(transform.parent).gameObject; - planeClone.transform.Find("GridAnchor").GetComponent().rootGridPlaneAnchor = this; - GridPlaneAnchor gridPlaneAnchor = planeClone.transform.Find("GridAnchor").GetComponent(); - gridPlaneAnchor.isImmutable = false; - gridPlaneAnchor.isHeld = true; - gridPlaneAnchor.isHovered = true; - planeClone.transform.parent = transform.parent.parent; - planeClone.transform.localEulerAngles = transform.parent.transform.localEulerAngles; - planeClone.transform.position = transform.parent.transform.position; - planeClone.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f); - gridPlaneAnchor.Setup(); - - // Reset the interactions on this plane. - if (isImmutable) { - isHeld = false; - isHovered = false; + + /// + /// Destroys this instance of a grid plane. + /// + private void DestroyGridPlane() + { + isHeld = false; + isHovered = false; + PeltzerMain.Instance.peltzerController.PeltzerControllerActionHandler -= ControllerEventHandler; + DestroyImmediate(transform.parent.gameObject); } - } - - // Handle mode selection when appropriate. - private void ControllerEventHandler(object sender, ControllerEventArgs args) { - if (IsGrabbingAnchor(args)) { - // Disable multiselection. - PeltzerMain.Instance.GetSelector().EndMultiSelection(); - - // As we use the eraser on spawned clones to provide a "delete" method, we exclude - // any delete tool (eraser) from spawning to provide clarity. - if (isImmutable && PeltzerMain.Instance.peltzerController.mode != ControllerMode.delete) { - SpawnNewGridPlane(); - } else if (!isImmutable) { - isHeld = true; - // if tool is eraser, delete this plane via parent. - if (PeltzerMain.Instance.peltzerController.mode == ControllerMode.delete) { - DestroyGridPlane(); - return; + + /// + /// Determines if a planes position is currently the same as another of the same axis alignment. + /// + private bool IsOverlappingPlaneOnSameAxis() + { + if (rootGridPlaneAnchor.planeClones.ContainsKey(transform.parent.localPosition[(int)axisType])) + { + return true; } + return false; + } - // Set grab check semaphore for next update. - // This semaphore is used to check for duplicate planes at same level. - isGrabFrameUpdate = true; - } - } else if(IsReleasingAnchor(args)) { - isHeld = false; - PeltzerMain.Instance.Zoomer.isManipulatingGridPlane = false; - - // If plane is set outside of world space, destroy it. - if(!isImmutable && - (Mathf.Abs(transform.parent.localPosition[(int)axisType]) >= 0.5f || IsOverlappingPlaneOnSameAxis())) { - DestroyGridPlane(); - return; - } - - // Set release check semaphore for next update. - // This semaphore is used to check for duplicate planes at same level. - isReleaseFrameUpdate = true; + /// + /// Determines if the selector is colliding with this transform. + /// + private bool IsCollidedWithSelector() + { + float dist = Vector3.Distance(PeltzerMain.Instance.worldSpace + .ModelToWorld(PeltzerMain.Instance.GetSelector().selectorPosition), transform.position); + return dist <= transform.localScale.x / 2f * PeltzerMain.Instance.worldSpace.scale; } - } - - /// - /// Destroys this instance of a grid plane. - /// - private void DestroyGridPlane() { - isHeld = false; - isHovered = false; - PeltzerMain.Instance.peltzerController.PeltzerControllerActionHandler -= ControllerEventHandler; - DestroyImmediate(transform.parent.gameObject); - } - - /// - /// Determines if a planes position is currently the same as another of the same axis alignment. - /// - private bool IsOverlappingPlaneOnSameAxis() { - if(rootGridPlaneAnchor.planeClones.ContainsKey(transform.parent.localPosition[(int)axisType])) { - return true; + + /// + /// Determines if this anchor is being grabbed. + /// + /// Controller event information + private bool IsGrabbingAnchor(ControllerEventArgs args) + { + return isHovered + && args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN; + } + + /// + /// Determines if the anchor is being released. + /// + /// Controller event informatin + private bool IsReleasingAnchor(ControllerEventArgs args) + { + return isHeld + && args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.UP; } - return false; - } - - /// - /// Determines if the selector is colliding with this transform. - /// - private bool IsCollidedWithSelector() { - float dist = Vector3.Distance(PeltzerMain.Instance.worldSpace - .ModelToWorld(PeltzerMain.Instance.GetSelector().selectorPosition), transform.position); - return dist <= transform.localScale.x/2f * PeltzerMain.Instance.worldSpace.scale; - } - - /// - /// Determines if this anchor is being grabbed. - /// - /// Controller event information - private bool IsGrabbingAnchor(ControllerEventArgs args) { - return isHovered - && args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN; - } - - /// - /// Determines if the anchor is being released. - /// - /// Controller event informatin - private bool IsReleasingAnchor(ControllerEventArgs args) { - return isHeld - && args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.UP; - } } \ No newline at end of file diff --git a/Assets/Scripts/guides/Rope.cs b/Assets/Scripts/guides/Rope.cs index 37e98977..95825d95 100644 --- a/Assets/Scripts/guides/Rope.cs +++ b/Assets/Scripts/guides/Rope.cs @@ -14,46 +14,53 @@ using UnityEngine; -namespace com.google.apps.peltzer.client.guides { - /// - /// Rope rendering utility class for debugging or developing. - /// - /// NOTE: Should not be used in production since it uses game objects to render. - /// - public class Rope { - private static readonly float WIDTH = 0.002f; - private readonly Color START_COLOR = Color.red; - private readonly Color END_COLOR = Color.red; - - GameObject go; - LineRenderer lineRenderer; - - public Rope() { - go = new GameObject("rope"); - - lineRenderer = go.AddComponent(); - lineRenderer.startWidth = WIDTH; - lineRenderer.endWidth = WIDTH; - - lineRenderer.startColor = START_COLOR; - lineRenderer.endColor = END_COLOR; - } +namespace com.google.apps.peltzer.client.guides +{ + /// + /// Rope rendering utility class for debugging or developing. + /// + /// NOTE: Should not be used in production since it uses game objects to render. + /// + public class Rope + { + private static readonly float WIDTH = 0.002f; + private readonly Color START_COLOR = Color.red; + private readonly Color END_COLOR = Color.red; - public void UpdatePosition(Vector3 sourceWorldSpace, Vector3 targetWorldSpace) { - lineRenderer.SetPosition(0, sourceWorldSpace); - lineRenderer.SetPosition(1, targetWorldSpace); - } + GameObject go; + LineRenderer lineRenderer; - public void Hide() { - go.SetActive(false); - } + public Rope() + { + go = new GameObject("rope"); - public void Unhide() { - go.SetActive(true); - } + lineRenderer = go.AddComponent(); + lineRenderer.startWidth = WIDTH; + lineRenderer.endWidth = WIDTH; + + lineRenderer.startColor = START_COLOR; + lineRenderer.endColor = END_COLOR; + } + + public void UpdatePosition(Vector3 sourceWorldSpace, Vector3 targetWorldSpace) + { + lineRenderer.SetPosition(0, sourceWorldSpace); + lineRenderer.SetPosition(1, targetWorldSpace); + } + + public void Hide() + { + go.SetActive(false); + } + + public void Unhide() + { + go.SetActive(true); + } - public void Destroy() { - GameObject.Destroy(go); + public void Destroy() + { + GameObject.Destroy(go); + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/menu/IntroChoreographer.cs b/Assets/Scripts/menu/IntroChoreographer.cs index ac5704ee..08b6064b 100644 --- a/Assets/Scripts/menu/IntroChoreographer.cs +++ b/Assets/Scripts/menu/IntroChoreographer.cs @@ -17,257 +17,276 @@ using com.google.apps.peltzer.client.model.controller; using com.google.apps.peltzer.client.tutorial; -namespace com.google.apps.peltzer.client.menu { - /// - /// Handles the choreography of the intro animation. - /// - /// The intro animation starts in darkness with the Blocks logo assembling from shards as we play the intro - /// sound. After the logo assembles, the lighting changes to show the environment and keep the Blocks logo - /// on the screen for a few seconds. - /// - public class IntroChoreographer : MonoBehaviour { +namespace com.google.apps.peltzer.client.menu +{ /// - /// Duration of introduction animation (Blocks logo assembling together). + /// Handles the choreography of the intro animation. + /// + /// The intro animation starts in darkness with the Blocks logo assembling from shards as we play the intro + /// sound. After the logo assembles, the lighting changes to show the environment and keep the Blocks logo + /// on the screen for a few seconds. /// - private const float INTRO_ANIMATION_DURATION = 10.0f; + public class IntroChoreographer : MonoBehaviour + { + /// + /// Duration of introduction animation (Blocks logo assembling together). + /// + private const float INTRO_ANIMATION_DURATION = 10.0f; - /// - /// Speed scale of the intro animation (tweaked to match it with the intro sound). - /// - private const float INTRO_ANIMATION_SPEED_SCALE = 0.75f; + /// + /// Speed scale of the intro animation (tweaked to match it with the intro sound). + /// + private const float INTRO_ANIMATION_SPEED_SCALE = 0.75f; - /// - /// How long to display the Blocks logo after the intro animation. - /// - private const float INTRO_LOGO_DURATION = 0.7f; + /// + /// How long to display the Blocks logo after the intro animation. + /// + private const float INTRO_LOGO_DURATION = 0.7f; - /// - /// How long the lighting change takes (from dark to light). - /// - private const float LIGHTING_CHANGE_DURATION = 1.0f; + /// + /// How long the lighting change takes (from dark to light). + /// + private const float LIGHTING_CHANGE_DURATION = 1.0f; - /// - /// How long the terrain animation takes to complete. - /// - private const float TERRAIN_ANIMATION_DURATION = 2.0f; + /// + /// How long the terrain animation takes to complete. + /// + private const float TERRAIN_ANIMATION_DURATION = 2.0f; - /// - /// Minimum amount of time user should hold down the triggers to skip intro. - /// - private const float SKIP_INTRO_TRIGGER_HOLD_DURATION = 0.25f; + /// + /// Minimum amount of time user should hold down the triggers to skip intro. + /// + private const float SKIP_INTRO_TRIGGER_HOLD_DURATION = 0.25f; - /// - /// Library for playing sounds. - /// - private AudioLibrary audioLibrary; - /// - /// Peltzer controller, to allow user to skip intro. - /// - private PeltzerController peltzerController; - /// - /// Palette controller, to allow user to skip intro. - /// - private PaletteController paletteController; - /// - /// Time user has spent holding the triggers down. - /// - private float triggerTime; + /// + /// Library for playing sounds. + /// + private AudioLibrary audioLibrary; + /// + /// Peltzer controller, to allow user to skip intro. + /// + private PeltzerController peltzerController; + /// + /// Palette controller, to allow user to skip intro. + /// + private PaletteController paletteController; + /// + /// Time user has spent holding the triggers down. + /// + private float triggerTime; - public Color StartSkyColor = new Color32(22, 23, 33, 255); - public Color StartGroundColor = new Color32(23, 23, 33, 255); + public Color StartSkyColor = new Color32(22, 23, 33, 255); + public Color StartGroundColor = new Color32(23, 23, 33, 255); - // GameObjects that choreograph (obtained during Setup()): - private GameObject introAnim; - private GameObject introLogo; - private GameObject introLogoLine1; - private GameObject environment; - private GameObject terrainLift; - private GameObject terrainFloor; - private GameObject dust; + // GameObjects that choreograph (obtained during Setup()): + private GameObject introAnim; + private GameObject introLogo; + private GameObject introLogoLine1; + private GameObject environment; + private GameObject terrainLift; + private GameObject terrainFloor; + private GameObject dust; - public enum State { - // Playing the intro animation (Blocks logo floating and assembling). - INTRO_ANIMATION, - // Animating lighting change, - INTRO_LIGHTING, - // Showing the intro logo ("Blocks by Google") - INTRO_LOGO, - // Done with intro choreography. - DONE, - } + public enum State + { + // Playing the intro animation (Blocks logo floating and assembling). + INTRO_ANIMATION, + // Animating lighting change, + INTRO_LIGHTING, + // Showing the intro logo ("Blocks by Google") + INTRO_LOGO, + // Done with intro choreography. + DONE, + } - public State state {get; private set;} + public State state { get; private set; } - /// - /// Countdown to next state transtiion. - /// - private float countdown = INTRO_ANIMATION_DURATION; + /// + /// Countdown to next state transtiion. + /// + private float countdown = INTRO_ANIMATION_DURATION; - /// - /// True if Setup() was called. - /// - private bool setupDone; + /// + /// True if Setup() was called. + /// + private bool setupDone; - /// - /// True when intro animation is complete. - /// - public bool introIsComplete = false; + /// + /// True when intro animation is complete. + /// + public bool introIsComplete = false; - /// - /// True if the user's Zandria creations should be loaded once the start up animation finishes. - /// - public bool loadCreationsWhenDone; + /// + /// True if the user's Zandria creations should be loaded once the start up animation finishes. + /// + public bool loadCreationsWhenDone; - /// - /// Initial setup. Must be called before anything else. - /// - public void Setup(AudioLibrary audioLibrary, PeltzerController peltzerController, - PaletteController paletteController) { - this.audioLibrary = audioLibrary; - this.peltzerController = peltzerController; - this.paletteController = paletteController; - triggerTime = 0; - introAnim = ObjectFinder.ObjectById("ID_IntroAnim"); - introLogo = ObjectFinder.ObjectById("ID_IntroLogo"); - introLogoLine1 = ObjectFinder.ObjectById("ID_Logo_Line_1"); - environment = ObjectFinder.ObjectById("ID_Environment"); - terrainLift = ObjectFinder.ObjectById("ID_TerrainLift"); - terrainFloor = ObjectFinder.ObjectById("ID_TerrainNoMountains"); - dust = ObjectFinder.ObjectById("ID_Dust"); - dust.SetActive(false); + /// + /// Initial setup. Must be called before anything else. + /// + public void Setup(AudioLibrary audioLibrary, PeltzerController peltzerController, + PaletteController paletteController) + { + this.audioLibrary = audioLibrary; + this.peltzerController = peltzerController; + this.paletteController = paletteController; + triggerTime = 0; + introAnim = ObjectFinder.ObjectById("ID_IntroAnim"); + introLogo = ObjectFinder.ObjectById("ID_IntroLogo"); + introLogoLine1 = ObjectFinder.ObjectById("ID_Logo_Line_1"); + environment = ObjectFinder.ObjectById("ID_Environment"); + terrainLift = ObjectFinder.ObjectById("ID_TerrainLift"); + terrainFloor = ObjectFinder.ObjectById("ID_TerrainNoMountains"); + dust = ObjectFinder.ObjectById("ID_Dust"); + dust.SetActive(false); - // Forbid everything initially (until the user selects an item from the menu). - PeltzerMain.Instance.restrictionManager.ForbidAll(); + // Forbid everything initially (until the user selects an item from the menu). + PeltzerMain.Instance.restrictionManager.ForbidAll(); - ChangeState(State.INTRO_ANIMATION); - setupDone = true; + ChangeState(State.INTRO_ANIMATION); + setupDone = true; - // Initially false until the user is authenticated by PeltzerMain. - loadCreationsWhenDone = false; - } + // Initially false until the user is authenticated by PeltzerMain. + loadCreationsWhenDone = false; + } - private void Update() { - if (!setupDone) return; + private void Update() + { + if (!setupDone) return; - // Hold the triggers for the set duration to skip the intro. - bool paletteTriggerDown = PaletteController.AcquireIfNecessary(ref paletteController) - && paletteController.controller.IsPressed(ButtonId.Trigger); - bool peltzerTriggerDown = PeltzerController.AcquireIfNecessary(ref peltzerController) - && peltzerController.controller.IsPressed(ButtonId.Trigger); - if (peltzerTriggerDown && paletteTriggerDown) { - if (triggerTime == 0) { - // Start tracking the time spent with the triggers held. - triggerTime = Time.time; - } else if (Time.time - triggerTime > SKIP_INTRO_TRIGGER_HOLD_DURATION) { - // Skip the rest of the intro. - ChangeState(State.DONE); - } - } else if (triggerTime > 0) { - // One or both of the triggers were released, reset the trigger time to 0. - triggerTime = 0; - } + // Hold the triggers for the set duration to skip the intro. + bool paletteTriggerDown = PaletteController.AcquireIfNecessary(ref paletteController) + && paletteController.controller.IsPressed(ButtonId.Trigger); + bool peltzerTriggerDown = PeltzerController.AcquireIfNecessary(ref peltzerController) + && peltzerController.controller.IsPressed(ButtonId.Trigger); + if (peltzerTriggerDown && paletteTriggerDown) + { + if (triggerTime == 0) + { + // Start tracking the time spent with the triggers held. + triggerTime = Time.time; + } + else if (Time.time - triggerTime > SKIP_INTRO_TRIGGER_HOLD_DURATION) + { + // Skip the rest of the intro. + ChangeState(State.DONE); + } + } + else if (triggerTime > 0) + { + // One or both of the triggers were released, reset the trigger time to 0. + triggerTime = 0; + } - // Note: Update() stops being called once we go into the DONE state because we set MonoBehavior.enabled = false - // when we go into that state. + // Note: Update() stops being called once we go into the DONE state because we set MonoBehavior.enabled = false + // when we go into that state. - countdown -= Time.deltaTime; - switch (state) { - case State.INTRO_ANIMATION: - // Check to see if it's time to advance. - if (countdown <= 0) ChangeState(State.INTRO_LOGO); - break; - case State.INTRO_LOGO: - // Check to see if it's time to advance. - introLogo.transform.LookAt(PeltzerMain.Instance.hmd.transform); - float fadeInPct = Mathf.Max(0.0f, Mathf.Min(1.0f, 1 - countdown/INTRO_LOGO_DURATION)); - TextMesh textLine1 = introLogoLine1.GetComponent(); - textLine1.color = new Color(textLine1.color.r, textLine1.color.g, textLine1.color.b, fadeInPct); - if (countdown <= 0) ChangeState(State.INTRO_LIGHTING); - break; - case State.INTRO_LIGHTING: - // Set the skybox lighting to animate from darkness to light. - SetSkyboxLightFactor(Mathf.Clamp01(1.0f - (countdown / LIGHTING_CHANGE_DURATION))); - // Check to see if it's time to advance. - if ((countdown + TERRAIN_ANIMATION_DURATION) <= 0) ChangeState(State.DONE); - break; - default: - break; - } - } + countdown -= Time.deltaTime; + switch (state) + { + case State.INTRO_ANIMATION: + // Check to see if it's time to advance. + if (countdown <= 0) ChangeState(State.INTRO_LOGO); + break; + case State.INTRO_LOGO: + // Check to see if it's time to advance. + introLogo.transform.LookAt(PeltzerMain.Instance.hmd.transform); + float fadeInPct = Mathf.Max(0.0f, Mathf.Min(1.0f, 1 - countdown / INTRO_LOGO_DURATION)); + TextMesh textLine1 = introLogoLine1.GetComponent(); + textLine1.color = new Color(textLine1.color.r, textLine1.color.g, textLine1.color.b, fadeInPct); + if (countdown <= 0) ChangeState(State.INTRO_LIGHTING); + break; + case State.INTRO_LIGHTING: + // Set the skybox lighting to animate from darkness to light. + SetSkyboxLightFactor(Mathf.Clamp01(1.0f - (countdown / LIGHTING_CHANGE_DURATION))); + // Check to see if it's time to advance. + if ((countdown + TERRAIN_ANIMATION_DURATION) <= 0) ChangeState(State.DONE); + break; + default: + break; + } + } - private void ChangeState(State newState) { - state = newState; - switch (newState) { - case State.INTRO_ANIMATION: - PeltzerMain.Instance.GetMover().currentMoveType = tools.Mover.MoveType.NONE; - PeltzerMain.Instance.paletteController.ChangeTouchpadOverlay(TouchpadOverlay.NONE); - terrainLift.SetActive(false); - terrainFloor.SetActive(false); - introLogo.SetActive(false); - introAnim.SetActive(true); - introAnim.GetComponentInChildren().speed = INTRO_ANIMATION_SPEED_SCALE; - audioLibrary.PlayClip(audioLibrary.startupSound); - countdown = INTRO_ANIMATION_DURATION; - SetSkyboxLightFactor(0f); - break; - case State.INTRO_LOGO: - introLogo.SetActive(true); - countdown = INTRO_LOGO_DURATION; - break; - case State.INTRO_LIGHTING: - environment.SetActive(true); - countdown = LIGHTING_CHANGE_DURATION; - break; - case State.DONE: - // This transition has some redundant work below because we want to be able to shortcut directly - // to State.DONE from any state if the user presses a key (in debug mode). - SetSkyboxLightFactor(1); - introAnim.SetActive(false); - introLogo.SetActive(false); - environment.SetActive(true); - dust.SetActive(true); - audioLibrary.FadeClip(audioLibrary.startupSound); - // We're now ready to either show the startup menu if the user has ever used Blocks before; - // or skip straight to the tutorial if they haven't. - if (!PeltzerMain.Instance.HasEverStartedPoly && Features.forceFirstTimeUsersIntoTutorial) { - PeltzerMain.Instance.tutorialManager.StartTutorial(0); - PeltzerMain.Instance.GetMover().currentMoveType = tools.Mover.MoveType.MOVE; - PeltzerMain.Instance.paletteController.ChangeTouchpadOverlay(TouchpadOverlay.NONE); - } else { - PeltzerMain.Instance.GetMover().currentMoveType = tools.Mover.MoveType.MOVE; - PeltzerMain.Instance.paletteController.ChangeTouchpadOverlay(TouchpadOverlay.UNDO_REDO); - // Don't clear reference images added by the user while in the menu (because they typically do that before - // putting on the headset). - PeltzerMain.Instance.CreateNewModel(/* clearReferenceImages */ false); - } + private void ChangeState(State newState) + { + state = newState; + switch (newState) + { + case State.INTRO_ANIMATION: + PeltzerMain.Instance.GetMover().currentMoveType = tools.Mover.MoveType.NONE; + PeltzerMain.Instance.paletteController.ChangeTouchpadOverlay(TouchpadOverlay.NONE); + terrainLift.SetActive(false); + terrainFloor.SetActive(false); + introLogo.SetActive(false); + introAnim.SetActive(true); + introAnim.GetComponentInChildren().speed = INTRO_ANIMATION_SPEED_SCALE; + audioLibrary.PlayClip(audioLibrary.startupSound); + countdown = INTRO_ANIMATION_DURATION; + SetSkyboxLightFactor(0f); + break; + case State.INTRO_LOGO: + introLogo.SetActive(true); + countdown = INTRO_LOGO_DURATION; + break; + case State.INTRO_LIGHTING: + environment.SetActive(true); + countdown = LIGHTING_CHANGE_DURATION; + break; + case State.DONE: + // This transition has some redundant work below because we want to be able to shortcut directly + // to State.DONE from any state if the user presses a key (in debug mode). + SetSkyboxLightFactor(1); + introAnim.SetActive(false); + introLogo.SetActive(false); + environment.SetActive(true); + dust.SetActive(true); + audioLibrary.FadeClip(audioLibrary.startupSound); + // We're now ready to either show the startup menu if the user has ever used Blocks before; + // or skip straight to the tutorial if they haven't. + if (!PeltzerMain.Instance.HasEverStartedPoly && Features.forceFirstTimeUsersIntoTutorial) + { + PeltzerMain.Instance.tutorialManager.StartTutorial(0); + PeltzerMain.Instance.GetMover().currentMoveType = tools.Mover.MoveType.MOVE; + PeltzerMain.Instance.paletteController.ChangeTouchpadOverlay(TouchpadOverlay.NONE); + } + else + { + PeltzerMain.Instance.GetMover().currentMoveType = tools.Mover.MoveType.MOVE; + PeltzerMain.Instance.paletteController.ChangeTouchpadOverlay(TouchpadOverlay.UNDO_REDO); + // Don't clear reference images added by the user while in the menu (because they typically do that before + // putting on the headset). + PeltzerMain.Instance.CreateNewModel(/* clearReferenceImages */ false); + } - introIsComplete = true; - // Prompt the user to take a tutorial. - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.TAKE_A_TUTORIAL_BUTTON); - PeltzerMain.Instance.peltzerController.LookAtMe(); - PeltzerMain.Instance.CheckLeftHandedPlayerPreference(); - PeltzerMain.Instance.menuHint.SetTimer(); - PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); + introIsComplete = true; + // Prompt the user to take a tutorial. + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.TAKE_A_TUTORIAL_BUTTON); + PeltzerMain.Instance.peltzerController.LookAtMe(); + PeltzerMain.Instance.CheckLeftHandedPlayerPreference(); + PeltzerMain.Instance.menuHint.SetTimer(); + PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); - // If the user is already signed in, load their zandria creations after the startup animation finishes. - if (loadCreationsWhenDone) { - PeltzerMain.Instance.LoadCreations(); - } - // We no longer need to Update(), as our work is done. - enabled = false; // This is the MonoBehaviour.enabled property. - GameObject visualBoundingBox = ObjectFinder.ObjectById("ID_PolyWorldBounds"); - visualBoundingBox.SetActive(true); + // If the user is already signed in, load their zandria creations after the startup animation finishes. + if (loadCreationsWhenDone) + { + PeltzerMain.Instance.LoadCreations(); + } + // We no longer need to Update(), as our work is done. + enabled = false; // This is the MonoBehaviour.enabled property. + GameObject visualBoundingBox = ObjectFinder.ObjectById("ID_PolyWorldBounds"); + visualBoundingBox.SetActive(true); - break; - } - } + break; + } + } - private void SetSkyboxLightFactor(float factor) { - Color skyColorToSet = Color.white * factor + StartSkyColor * (1f - factor); - RenderSettings.skybox.SetColor("_Tint", skyColorToSet);//new Color(factor, factor, factor, 1f)); - Color groundColorToSet = Color.white * factor + StartGroundColor * (1f - factor); + private void SetSkyboxLightFactor(float factor) + { + Color skyColorToSet = Color.white * factor + StartSkyColor * (1f - factor); + RenderSettings.skybox.SetColor("_Tint", skyColorToSet);//new Color(factor, factor, factor, 1f)); + Color groundColorToSet = Color.white * factor + StartGroundColor * (1f - factor); + } } - } } diff --git a/Assets/Scripts/menu/MenuHint.cs b/Assets/Scripts/menu/MenuHint.cs index 2c0eac51..2e033a10 100644 --- a/Assets/Scripts/menu/MenuHint.cs +++ b/Assets/Scripts/menu/MenuHint.cs @@ -21,450 +21,499 @@ using System.Collections.Generic; using com.google.apps.peltzer.client.app; -namespace com.google.apps.peltzer.client.menu { - /// - /// Creates and positions a preview for the menuHint and maintains information about the preview. - /// - public class MenuPreview { - public GameObject preview; - public MeshWithMaterialRenderer renderer; - public Vector3 positionAtStartOfOperation; - - public MenuPreview(MeshWithMaterialRenderer mwmRenderer, GameObject parent, WorldSpace worldSpace) { - preview = new GameObject(); - renderer = preview.AddComponent(); - renderer.SetupAsCopyOf(mwmRenderer); - renderer.worldSpace = worldSpace; - - preview.transform.parent = parent.transform; - preview.transform.localPosition = Vector3.zero; - preview.transform.localRotation = Quaternion.identity; - - // This is set at the beginning of an operation and can be local or global depending on what the - // operation calls for. - positionAtStartOfOperation = Vector3.zero; - } - } - - /// - /// Handles creating and animating a menuHint which previews four featured models and hints to the user - /// where the poly menu is. - /// - public class MenuHint : MonoBehaviour { - /// - /// Represents what state the menuHint is in. - /// - public enum State { - // Default. - NONE, - // The preview is being populated from the menu. - POPULATING, - // The menuHint has been shown and is now inactive. - INACTIVE, - // The preview is loaded but we are waiting for the timer to run down. - WAITING, - // The preview is being scaled into existence. - SCALE_ANIMATING, - // The preview is rotating. - ROTATION_ANIMATING, - // The preview is being suctioned into the menu button. - SUCTION_ANIMATING, - // The button is expanding out in response to the preview hitting it. - BUTTON_EXPAND_ANIMATING, - // The button is collapsing back to its original size. - BUTTON_COLLAPSE_ANIMATING - }; - - /// - /// The time between the intro completing and the menuHint starting to show. - /// - public static float WAIT_DURATION = 5f; - /// - /// How long the scale in animation lasts. - /// - private const float SCALE_IN_ANIMATION_DURATION = 0.2f; - /// - /// How long the previews rotate for. - /// - public static float ROTATION_ANIMATION_DURATION = 3f; - /// - /// How long it takes to suck the preview into the button. - /// - public static float SUCTION_ANIMATION_DURATION = 0.75f; - /// - /// How long the buttons expands for after being hit. - /// - private const float BUTTON_EXPAND_ANIMATION_DURATION = 0.2f; - /// - /// How long it takes for the button to reset. - /// - private const float BUTTON_COLLAPSE_ANIMATION_DURATION = 0.1f; - /// - /// How much larger the button gets. - /// - private const float BUTTON_SCALE_FACTOR = 1.4f; - /// - /// How long we wait in between haptic pulses when getting the users attention. - /// - private const float HAPTICS_PAUSE = 0.5f; +namespace com.google.apps.peltzer.client.menu +{ /// - /// How many times we pulse the controller to get the users attention. + /// Creates and positions a preview for the menuHint and maintains information about the preview. /// - private const int ATTENTION_PULSES = 3; - /// - /// The default scale for the model previews. - /// - public const float DEFAULT_SCALE = 0.45f; - /// - /// The minimum speed factor used at the start of rotating the previews. - /// - private const float MIN_ROTATION_SPEED = 20f; - /// - /// The rotation speed factor we hit before starting to suck the previews into the button. - /// - private const float MID_ROTATION_SPEED = 150f; - /// - /// The max rotation speed factor for the previews as they are sucked into the button. - /// - private const float MAX_ROTATION_SPEED = 2500f; - - /// - /// The current state of the menuHint. - /// - private State state; - /// - /// The previews shown as a hint. - /// - private List menuPreviews; - /// - /// The MeshWithMaterialRenderers passed from the ZandriaCreationsManager that will be used to make - /// preview meshes of the models used as hints. - /// - List mwmRenderers; - /// - /// The scale of the button before any animating. - /// - private Vector3 buttonStartScale; - /// - /// The max scale the button should reach while animating. - /// - private Vector3 buttonMaxScale; - - /// - /// The time that we started the current operation. - /// - private float operationStartTime; - /// - /// The position of the menuHint at the start of the current operation. This can be in global or local - /// depending on the operation. - /// - private Vector3 operationStartPosition; - /// - /// The world space that the preview models exist in. This is used to manipulate their apparent size. - /// - private WorldSpace previewSpace; - /// - /// The number of haptic pulses that have been triggered to get the users attention. - /// - private int attentionPulseCount; - - /// - /// The game object that holds the four previews. - /// - private GameObject menuHint; - /// - /// The game object that holds the text displayed with the menu hint. - /// - private GameObject menuHintText; - /// - /// Holds the menuHint and the menuHintText. - /// - private GameObject menuHintRoot; - /// - /// Empty game objects that hold the positions that the previews can be attached. - /// - private GameObject[] menuPreviewHolders; - /// - /// The position of the button the previews are sucked into. This is in the same space as the menuHint - /// so that we can animate without the motion of the controller having an effect. - /// - private Vector3 buttonPositionInPreviewSpace; - - /// - /// Position of the menu hint when using the Oculus. - /// - private static readonly Vector3 ROOT_POSITION_OCULUS = new Vector3(0.002721673f, 0.05050485f, 0.06261638f); + public class MenuPreview + { + public GameObject preview; + public MeshWithMaterialRenderer renderer; + public Vector3 positionAtStartOfOperation; + + public MenuPreview(MeshWithMaterialRenderer mwmRenderer, GameObject parent, WorldSpace worldSpace) + { + preview = new GameObject(); + renderer = preview.AddComponent(); + renderer.SetupAsCopyOf(mwmRenderer); + renderer.worldSpace = worldSpace; + + preview.transform.parent = parent.transform; + preview.transform.localPosition = Vector3.zero; + preview.transform.localRotation = Quaternion.identity; + + // This is set at the beginning of an operation and can be local or global depending on what the + // operation calls for. + positionAtStartOfOperation = Vector3.zero; + } + } /// - /// Rotation of the menu hint when using the Oculus. + /// Handles creating and animating a menuHint which previews four featured models and hints to the user + /// where the poly menu is. /// - private static readonly Vector3 ROOT_ROTATION_OCULUS = new Vector3(-46.218f, 6.207f, -11.243f); - - public void Setup() { - // Indicate to ZandriaCreationsManager that the MenuHint is waiting for previews. - state = State.POPULATING; - - // Find all the relevant game objects. - menuHint = ObjectFinder.ObjectById("ID_MenuHint"); - menuHintRoot = ObjectFinder.ObjectById("ID_MenuHintHolder"); - menuHintText = ObjectFinder.ObjectById("ID_MenuHintText"); - menuPreviewHolders = new GameObject[4] { + public class MenuHint : MonoBehaviour + { + /// + /// Represents what state the menuHint is in. + /// + public enum State + { + // Default. + NONE, + // The preview is being populated from the menu. + POPULATING, + // The menuHint has been shown and is now inactive. + INACTIVE, + // The preview is loaded but we are waiting for the timer to run down. + WAITING, + // The preview is being scaled into existence. + SCALE_ANIMATING, + // The preview is rotating. + ROTATION_ANIMATING, + // The preview is being suctioned into the menu button. + SUCTION_ANIMATING, + // The button is expanding out in response to the preview hitting it. + BUTTON_EXPAND_ANIMATING, + // The button is collapsing back to its original size. + BUTTON_COLLAPSE_ANIMATING + }; + + /// + /// The time between the intro completing and the menuHint starting to show. + /// + public static float WAIT_DURATION = 5f; + /// + /// How long the scale in animation lasts. + /// + private const float SCALE_IN_ANIMATION_DURATION = 0.2f; + /// + /// How long the previews rotate for. + /// + public static float ROTATION_ANIMATION_DURATION = 3f; + /// + /// How long it takes to suck the preview into the button. + /// + public static float SUCTION_ANIMATION_DURATION = 0.75f; + /// + /// How long the buttons expands for after being hit. + /// + private const float BUTTON_EXPAND_ANIMATION_DURATION = 0.2f; + /// + /// How long it takes for the button to reset. + /// + private const float BUTTON_COLLAPSE_ANIMATION_DURATION = 0.1f; + /// + /// How much larger the button gets. + /// + private const float BUTTON_SCALE_FACTOR = 1.4f; + /// + /// How long we wait in between haptic pulses when getting the users attention. + /// + private const float HAPTICS_PAUSE = 0.5f; + /// + /// How many times we pulse the controller to get the users attention. + /// + private const int ATTENTION_PULSES = 3; + /// + /// The default scale for the model previews. + /// + public const float DEFAULT_SCALE = 0.45f; + /// + /// The minimum speed factor used at the start of rotating the previews. + /// + private const float MIN_ROTATION_SPEED = 20f; + /// + /// The rotation speed factor we hit before starting to suck the previews into the button. + /// + private const float MID_ROTATION_SPEED = 150f; + /// + /// The max rotation speed factor for the previews as they are sucked into the button. + /// + private const float MAX_ROTATION_SPEED = 2500f; + + /// + /// The current state of the menuHint. + /// + private State state; + /// + /// The previews shown as a hint. + /// + private List menuPreviews; + /// + /// The MeshWithMaterialRenderers passed from the ZandriaCreationsManager that will be used to make + /// preview meshes of the models used as hints. + /// + List mwmRenderers; + /// + /// The scale of the button before any animating. + /// + private Vector3 buttonStartScale; + /// + /// The max scale the button should reach while animating. + /// + private Vector3 buttonMaxScale; + + /// + /// The time that we started the current operation. + /// + private float operationStartTime; + /// + /// The position of the menuHint at the start of the current operation. This can be in global or local + /// depending on the operation. + /// + private Vector3 operationStartPosition; + /// + /// The world space that the preview models exist in. This is used to manipulate their apparent size. + /// + private WorldSpace previewSpace; + /// + /// The number of haptic pulses that have been triggered to get the users attention. + /// + private int attentionPulseCount; + + /// + /// The game object that holds the four previews. + /// + private GameObject menuHint; + /// + /// The game object that holds the text displayed with the menu hint. + /// + private GameObject menuHintText; + /// + /// Holds the menuHint and the menuHintText. + /// + private GameObject menuHintRoot; + /// + /// Empty game objects that hold the positions that the previews can be attached. + /// + private GameObject[] menuPreviewHolders; + /// + /// The position of the button the previews are sucked into. This is in the same space as the menuHint + /// so that we can animate without the motion of the controller having an effect. + /// + private Vector3 buttonPositionInPreviewSpace; + + /// + /// Position of the menu hint when using the Oculus. + /// + private static readonly Vector3 ROOT_POSITION_OCULUS = new Vector3(0.002721673f, 0.05050485f, 0.06261638f); + + /// + /// Rotation of the menu hint when using the Oculus. + /// + private static readonly Vector3 ROOT_ROTATION_OCULUS = new Vector3(-46.218f, 6.207f, -11.243f); + + public void Setup() + { + // Indicate to ZandriaCreationsManager that the MenuHint is waiting for previews. + state = State.POPULATING; + + // Find all the relevant game objects. + menuHint = ObjectFinder.ObjectById("ID_MenuHint"); + menuHintRoot = ObjectFinder.ObjectById("ID_MenuHintHolder"); + menuHintText = ObjectFinder.ObjectById("ID_MenuHintText"); + menuPreviewHolders = new GameObject[4] { ObjectFinder.ObjectById("ID_MenuPreview_1"), ObjectFinder.ObjectById("ID_MenuPreview_2"), ObjectFinder.ObjectById("ID_MenuPreview_3"), ObjectFinder.ObjectById("ID_MenuPreview_4")}; - // Make sure we position the menuHint and text correctly for Rift/Vive by copying the position of the save - // indicator. - menuHintRoot.transform.position = ObjectFinder.ObjectById("ID_ProgressIndicatorPanel").transform.position; + // Make sure we position the menuHint and text correctly for Rift/Vive by copying the position of the save + // indicator. + menuHintRoot.transform.position = ObjectFinder.ObjectById("ID_ProgressIndicatorPanel").transform.position; - // Find where the menu button is in the menuHints transform space so that we can animate the menuHint - // locally so that movement of the controller doesn't affect the animation. - buttonPositionInPreviewSpace = menuHint.transform.parent.InverseTransformPoint( - PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButtonHolder.transform.position); + // Find where the menu button is in the menuHints transform space so that we can animate the menuHint + // locally so that movement of the controller doesn't affect the animation. + buttonPositionInPreviewSpace = menuHint.transform.parent.InverseTransformPoint( + PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButtonHolder.transform.position); - menuPreviews = new List(menuPreviewHolders.Length); - mwmRenderers = new List(menuPreviewHolders.Length); + menuPreviews = new List(menuPreviewHolders.Length); + mwmRenderers = new List(menuPreviewHolders.Length); - // Hide everything until the previews are ready and the timer has run down. - menuHintRoot.SetActive(false); + // Hide everything until the previews are ready and the timer has run down. + menuHintRoot.SetActive(false); - // We won't set the operation startTime until the intro choreography is complete. This way we can guarantee - // the menu is active. - operationStartTime = 0f; - - if (Config.Instance.sdkMode == SdkMode.Oculus) { - menuHintRoot.transform.localPosition = ROOT_POSITION_OCULUS; - menuHintRoot.transform.localRotation = Quaternion.Euler(ROOT_ROTATION_OCULUS); - } - } + // We won't set the operation startTime until the intro choreography is complete. This way we can guarantee + // the menu is active. + operationStartTime = 0f; - void Update() { - if (state == State.WAITING) { - // While operationStartTime == 0f we know the introchoreography hasn't finished so even if the - // previews are ready we continue to wait. Otherwise we wait until HAPTICS_PAUSE time has passed. - // trigger haptics to get the users attention and repeat for ATTENTION_PULSES number of pulses. - if (operationStartTime != 0f && Time.time > operationStartTime + HAPTICS_PAUSE) { - PeltzerMain.Instance.paletteController.LookAtMe(); - operationStartTime = Time.time; - attentionPulseCount++; - - // ZandriaCreationsManager has given MenuHint all the previews, the introChoreography is done, the - // menuHint timer has run down and we have the users attention from the haptics so we setup the preview - // and start animating the menuHint. - if (attentionPulseCount >= ATTENTION_PULSES) { - SetupPreviews(); - } + if (Config.Instance.sdkMode == SdkMode.Oculus) + { + menuHintRoot.transform.localPosition = ROOT_POSITION_OCULUS; + menuHintRoot.transform.localRotation = Quaternion.Euler(ROOT_ROTATION_OCULUS); + } } - } else if (state == State.SCALE_ANIMATING) { - // Scale the preview into scene by lerping the worldSpace for the preview. - - // No need to be fancy scale animation is linear relative to passed time. - float pctDone = (Time.time - operationStartTime) / SCALE_IN_ANIMATION_DURATION; - - if (pctDone > 1f) { - // Set the scale to its final value. - previewSpace.scale = DEFAULT_SCALE; - // Progress to the next state of the animation which is where the previews rotate around the controller. - ChangeState(State.ROTATION_ANIMATING); - } else { - // Animate the previews into existance. - previewSpace.scale = Mathf.Lerp(0f, DEFAULT_SCALE, pctDone); - } - } else if (state == State.ROTATION_ANIMATING) { - // We increase the speed that the previews rotate at lineraly. - float pctDone = (Time.time - operationStartTime) / ROTATION_ANIMATION_DURATION; - - if (pctDone > 1) { - ChangeState(State.SUCTION_ANIMATING); - } else { - float speed = Mathf.Lerp(MIN_ROTATION_SPEED, MID_ROTATION_SPEED, pctDone); - // Rotate the menuHint about itself. The previews are children and rotate with it. - menuHint.transform.RotateAround(menuHint.transform.position, menuHint.transform.up, speed * Time.deltaTime); - - // Rotate each preview individually in the opposite direction. This causes them to keep facing the - // user and the menuHint spins. Plus it looks better. - foreach (MenuPreview preview in menuPreviews) { - preview.preview.transform.RotateAround( - preview.preview.transform.position, - preview.preview.transform.up, - -(speed * Time.deltaTime)); - } - } - } else if (state == State.SUCTION_ANIMATING) { - // Use an easy curve to suck in the preview. - float pctDone = Math3d.CubicBezierEasing(0f, 0f, 0.1f, 1f, - (Time.time - operationStartTime) / SUCTION_ANIMATION_DURATION); - - if (pctDone > 1f) { - // Make a thud when the preview hits the button. - PeltzerMain.Instance.paletteController - .TriggerHapticFeedback(HapticFeedback.HapticFeedbackType.FEEDBACK_1, 0.04f, 1f); - - // Expand the button in response to the preview hitting it. - ChangeState(State.BUTTON_EXPAND_ANIMATING); - } else { - // Animate the scale. - previewSpace.scale = Mathf.Lerp(DEFAULT_SCALE, 0f, pctDone); - - // Animate the position of the entire menuHint towards the button in local space. - menuHint.transform.localPosition = Vector3.Slerp( - operationStartPosition, - buttonPositionInPreviewSpace, - pctDone); - - // Continue to rotate to give the menuHint a tornado effect. - float speed = Mathf.Lerp(MID_ROTATION_SPEED, MAX_ROTATION_SPEED, pctDone); - menuHint.transform.RotateAround(menuHint.transform.position, menuHint.transform.up, speed * Time.deltaTime); - - foreach (MenuPreview preview in menuPreviews) { - // Animate each individual preview towards the center of the whole menuHint converting the - // position of the menuHint to be a sibling of the preview so that we can animate the change locally. - preview.preview.transform.localPosition = Vector3.Slerp( - preview.positionAtStartOfOperation, - preview.preview.transform.parent.InverseTransformPoint(menuHint.transform.position), - pctDone); - - // Continue to rotate each individual preview. - preview.preview.transform.RotateAround( - preview.preview.transform.position, - preview.preview.transform.up, - -(speed * Time.deltaTime)); - } - - PeltzerMain.Instance.paletteController.TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_1, 0.001f, pctDone == 0f ? 0f : pctDone * 0.1f); - } - } else if (state == State.BUTTON_EXPAND_ANIMATING) { - // Animate the button expanding. We want it to look like a ripple because the preview hit it so we - // use a Cubic Bezier curve to lerp the scale quickly at first before slowing down. - float pctDone = Math3d.CubicBezierEasing(0f, 1.0f, 1.0f, 1.0f, - (Time.time - operationStartTime) / BUTTON_EXPAND_ANIMATION_DURATION); - - // We need the holder which is centered over the button. The transform of the actual button is - // incorrect because of a flaw in the controller UI. - Transform buttonHolder = - PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButtonHolder.transform; - - if (pctDone > 1.0f) { - buttonHolder.localScale = buttonMaxScale; - ChangeState(State.BUTTON_COLLAPSE_ANIMATING); - } else { - // Animate the scale. - buttonHolder.localScale = Vector3.Lerp(buttonStartScale, buttonMaxScale, pctDone); - - // Animate the glow on the actual button. - GameObject actualButton = - PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButton; - - AttentionCaller.SetEmissiveFactor( - actualButton, - (Time.time - operationStartTime) / BUTTON_EXPAND_ANIMATION_DURATION, - actualButton.GetComponent().material.color); - } - } else if (state == State.BUTTON_COLLAPSE_ANIMATING) { - // No need to do anything fancy here. We scale down the button linearly and quickly. - float pctDone = (Time.time - operationStartTime) / BUTTON_COLLAPSE_ANIMATION_DURATION; - - Transform buttonHolder = - PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButtonHolder.transform; - - if (pctDone > 1.0f) { - buttonHolder.localScale = buttonStartScale; - - // We don't animate the glow away. We want it to be as luminous for as long as possible. - GameObject actualButton = - PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButton; - AttentionCaller.SetEmissiveFactor(actualButton, 0f, actualButton.GetComponent().material.color); - ChangeState(State.INACTIVE); - } else { - buttonHolder.localScale = Vector3.Lerp(buttonMaxScale, buttonStartScale, pctDone); + + void Update() + { + if (state == State.WAITING) + { + // While operationStartTime == 0f we know the introchoreography hasn't finished so even if the + // previews are ready we continue to wait. Otherwise we wait until HAPTICS_PAUSE time has passed. + // trigger haptics to get the users attention and repeat for ATTENTION_PULSES number of pulses. + if (operationStartTime != 0f && Time.time > operationStartTime + HAPTICS_PAUSE) + { + PeltzerMain.Instance.paletteController.LookAtMe(); + operationStartTime = Time.time; + attentionPulseCount++; + + // ZandriaCreationsManager has given MenuHint all the previews, the introChoreography is done, the + // menuHint timer has run down and we have the users attention from the haptics so we setup the preview + // and start animating the menuHint. + if (attentionPulseCount >= ATTENTION_PULSES) + { + SetupPreviews(); + } + } + } + else if (state == State.SCALE_ANIMATING) + { + // Scale the preview into scene by lerping the worldSpace for the preview. + + // No need to be fancy scale animation is linear relative to passed time. + float pctDone = (Time.time - operationStartTime) / SCALE_IN_ANIMATION_DURATION; + + if (pctDone > 1f) + { + // Set the scale to its final value. + previewSpace.scale = DEFAULT_SCALE; + // Progress to the next state of the animation which is where the previews rotate around the controller. + ChangeState(State.ROTATION_ANIMATING); + } + else + { + // Animate the previews into existance. + previewSpace.scale = Mathf.Lerp(0f, DEFAULT_SCALE, pctDone); + } + } + else if (state == State.ROTATION_ANIMATING) + { + // We increase the speed that the previews rotate at lineraly. + float pctDone = (Time.time - operationStartTime) / ROTATION_ANIMATION_DURATION; + + if (pctDone > 1) + { + ChangeState(State.SUCTION_ANIMATING); + } + else + { + float speed = Mathf.Lerp(MIN_ROTATION_SPEED, MID_ROTATION_SPEED, pctDone); + // Rotate the menuHint about itself. The previews are children and rotate with it. + menuHint.transform.RotateAround(menuHint.transform.position, menuHint.transform.up, speed * Time.deltaTime); + + // Rotate each preview individually in the opposite direction. This causes them to keep facing the + // user and the menuHint spins. Plus it looks better. + foreach (MenuPreview preview in menuPreviews) + { + preview.preview.transform.RotateAround( + preview.preview.transform.position, + preview.preview.transform.up, + -(speed * Time.deltaTime)); + } + } + } + else if (state == State.SUCTION_ANIMATING) + { + // Use an easy curve to suck in the preview. + float pctDone = Math3d.CubicBezierEasing(0f, 0f, 0.1f, 1f, + (Time.time - operationStartTime) / SUCTION_ANIMATION_DURATION); + + if (pctDone > 1f) + { + // Make a thud when the preview hits the button. + PeltzerMain.Instance.paletteController + .TriggerHapticFeedback(HapticFeedback.HapticFeedbackType.FEEDBACK_1, 0.04f, 1f); + + // Expand the button in response to the preview hitting it. + ChangeState(State.BUTTON_EXPAND_ANIMATING); + } + else + { + // Animate the scale. + previewSpace.scale = Mathf.Lerp(DEFAULT_SCALE, 0f, pctDone); + + // Animate the position of the entire menuHint towards the button in local space. + menuHint.transform.localPosition = Vector3.Slerp( + operationStartPosition, + buttonPositionInPreviewSpace, + pctDone); + + // Continue to rotate to give the menuHint a tornado effect. + float speed = Mathf.Lerp(MID_ROTATION_SPEED, MAX_ROTATION_SPEED, pctDone); + menuHint.transform.RotateAround(menuHint.transform.position, menuHint.transform.up, speed * Time.deltaTime); + + foreach (MenuPreview preview in menuPreviews) + { + // Animate each individual preview towards the center of the whole menuHint converting the + // position of the menuHint to be a sibling of the preview so that we can animate the change locally. + preview.preview.transform.localPosition = Vector3.Slerp( + preview.positionAtStartOfOperation, + preview.preview.transform.parent.InverseTransformPoint(menuHint.transform.position), + pctDone); + + // Continue to rotate each individual preview. + preview.preview.transform.RotateAround( + preview.preview.transform.position, + preview.preview.transform.up, + -(speed * Time.deltaTime)); + } + + PeltzerMain.Instance.paletteController.TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_1, 0.001f, pctDone == 0f ? 0f : pctDone * 0.1f); + } + } + else if (state == State.BUTTON_EXPAND_ANIMATING) + { + // Animate the button expanding. We want it to look like a ripple because the preview hit it so we + // use a Cubic Bezier curve to lerp the scale quickly at first before slowing down. + float pctDone = Math3d.CubicBezierEasing(0f, 1.0f, 1.0f, 1.0f, + (Time.time - operationStartTime) / BUTTON_EXPAND_ANIMATION_DURATION); + + // We need the holder which is centered over the button. The transform of the actual button is + // incorrect because of a flaw in the controller UI. + Transform buttonHolder = + PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButtonHolder.transform; + + if (pctDone > 1.0f) + { + buttonHolder.localScale = buttonMaxScale; + ChangeState(State.BUTTON_COLLAPSE_ANIMATING); + } + else + { + // Animate the scale. + buttonHolder.localScale = Vector3.Lerp(buttonStartScale, buttonMaxScale, pctDone); + + // Animate the glow on the actual button. + GameObject actualButton = + PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButton; + + AttentionCaller.SetEmissiveFactor( + actualButton, + (Time.time - operationStartTime) / BUTTON_EXPAND_ANIMATION_DURATION, + actualButton.GetComponent().material.color); + } + } + else if (state == State.BUTTON_COLLAPSE_ANIMATING) + { + // No need to do anything fancy here. We scale down the button linearly and quickly. + float pctDone = (Time.time - operationStartTime) / BUTTON_COLLAPSE_ANIMATION_DURATION; + + Transform buttonHolder = + PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButtonHolder.transform; + + if (pctDone > 1.0f) + { + buttonHolder.localScale = buttonStartScale; + + // We don't animate the glow away. We want it to be as luminous for as long as possible. + GameObject actualButton = + PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButton; + AttentionCaller.SetEmissiveFactor(actualButton, 0f, actualButton.GetComponent().material.color); + ChangeState(State.INACTIVE); + } + else + { + buttonHolder.localScale = Vector3.Lerp(buttonMaxScale, buttonStartScale, pctDone); + } + } } - } - } - public void ChangeState(State newState) { - state = newState; - - switch (state) { - // The animation is over. - case State.INACTIVE: - foreach (MenuPreview preview in menuPreviews) { - Destroy(preview.preview); - } - - // Reset the active states of all the menu hint components. - menuHintText.SetActive(true); - menuHint.SetActive(true); - menuHintRoot.SetActive(false); - - menuPreviews.Clear(); - break; - case State.WAITING: - break; - case State.SCALE_ANIMATING: - menuHintRoot.SetActive(true); - menuHintText.SetActive(false); - previewSpace.scale = 0f; - operationStartTime = Time.time; - break; - case State.ROTATION_ANIMATING: - operationStartTime = Time.time; - menuHintText.SetActive(true); - break; - case State.SUCTION_ANIMATING: - menuHintText.SetActive(false); - foreach (MenuPreview preview in menuPreviews) { - preview.positionAtStartOfOperation = preview.preview.transform.localPosition; - } - operationStartTime = Time.time; - operationStartPosition = menuHint.transform.localPosition; - break; - case State.BUTTON_EXPAND_ANIMATING: - menuHintRoot.SetActive(false); - operationStartTime = Time.time; - buttonStartScale = - PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButton.transform.parent.localScale; - buttonMaxScale = new Vector3( - buttonStartScale.x * BUTTON_SCALE_FACTOR, - buttonStartScale.y, - buttonStartScale.z * BUTTON_SCALE_FACTOR); - break; - case State.BUTTON_COLLAPSE_ANIMATING: - operationStartTime = Time.time; - break; - } - } + public void ChangeState(State newState) + { + state = newState; + + switch (state) + { + // The animation is over. + case State.INACTIVE: + foreach (MenuPreview preview in menuPreviews) + { + Destroy(preview.preview); + } + + // Reset the active states of all the menu hint components. + menuHintText.SetActive(true); + menuHint.SetActive(true); + menuHintRoot.SetActive(false); + + menuPreviews.Clear(); + break; + case State.WAITING: + break; + case State.SCALE_ANIMATING: + menuHintRoot.SetActive(true); + menuHintText.SetActive(false); + previewSpace.scale = 0f; + operationStartTime = Time.time; + break; + case State.ROTATION_ANIMATING: + operationStartTime = Time.time; + menuHintText.SetActive(true); + break; + case State.SUCTION_ANIMATING: + menuHintText.SetActive(false); + foreach (MenuPreview preview in menuPreviews) + { + preview.positionAtStartOfOperation = preview.preview.transform.localPosition; + } + operationStartTime = Time.time; + operationStartPosition = menuHint.transform.localPosition; + break; + case State.BUTTON_EXPAND_ANIMATING: + menuHintRoot.SetActive(false); + operationStartTime = Time.time; + buttonStartScale = + PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButton.transform.parent.localScale; + buttonMaxScale = new Vector3( + buttonStartScale.x * BUTTON_SCALE_FACTOR, + buttonStartScale.y, + buttonStartScale.z * BUTTON_SCALE_FACTOR); + break; + case State.BUTTON_COLLAPSE_ANIMATING: + operationStartTime = Time.time; + break; + } + } - private void SetupPreviews() { - previewSpace = new WorldSpace(PeltzerMain.DEFAULT_BOUNDS, /* isLimited */ false); - previewSpace.scale = DEFAULT_SCALE; + private void SetupPreviews() + { + previewSpace = new WorldSpace(PeltzerMain.DEFAULT_BOUNDS, /* isLimited */ false); + previewSpace.scale = DEFAULT_SCALE; - for (int i = 0; i < menuPreviewHolders.Length; i++) { - menuPreviews.Add(new MenuPreview(mwmRenderers[i], menuPreviewHolders[i], previewSpace)); - } + for (int i = 0; i < menuPreviewHolders.Length; i++) + { + menuPreviews.Add(new MenuPreview(mwmRenderers[i], menuPreviewHolders[i], previewSpace)); + } - ChangeState(State.SCALE_ANIMATING); - } + ChangeState(State.SCALE_ANIMATING); + } - public void SetTimer() { - operationStartTime = Time.time + WAIT_DURATION; - } + public void SetTimer() + { + operationStartTime = Time.time + WAIT_DURATION; + } - public void AddPreview(MeshWithMaterialRenderer mwmRenderer) { - if (state != State.POPULATING) { - return; - } + public void AddPreview(MeshWithMaterialRenderer mwmRenderer) + { + if (state != State.POPULATING) + { + return; + } - mwmRenderers.Add(mwmRenderer); + mwmRenderers.Add(mwmRenderer); - if (mwmRenderers.Count == menuPreviewHolders.Length) { - ChangeState(State.WAITING); - } - } + if (mwmRenderers.Count == menuPreviewHolders.Length) + { + ChangeState(State.WAITING); + } + } - public bool IsPopulating() { - return state == State.POPULATING; + public bool IsPopulating() + { + return state == State.POPULATING; + } } - } } diff --git a/Assets/Scripts/menu/PolyMenuMain.cs b/Assets/Scripts/menu/PolyMenuMain.cs index 7e4c0083..be9d8bde 100644 --- a/Assets/Scripts/menu/PolyMenuMain.cs +++ b/Assets/Scripts/menu/PolyMenuMain.cs @@ -28,975 +28,1112 @@ using TMPro; using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.menu { - public class PolyMenuMain : MonoBehaviour { - // Struct to hold information about the current state of the menu. - public struct PolyMenuMode { - public PolyMenuSection menuSection; - public CreationType creationType; - public int page; - - public PolyMenuMode(PolyMenuSection menuSection, CreationType creationType, int page) { - this.menuSection = menuSection; - this.creationType = creationType; - this.page = page; - } - } +namespace com.google.apps.peltzer.client.menu +{ + public class PolyMenuMain : MonoBehaviour + { + // Struct to hold information about the current state of the menu. + public struct PolyMenuMode + { + public PolyMenuSection menuSection; + public CreationType creationType; + public int page; + + public PolyMenuMode(PolyMenuSection menuSection, CreationType creationType, int page) + { + this.menuSection = menuSection; + this.creationType = creationType; + this.page = page; + } + } - // The types of Zandria creations that can be loaded. - public enum CreationType { NONE, YOUR, FEATURED, LIKED } - // The different sections of the PolyMenu. - public enum PolyMenuSection { CREATION, OPTION, DETAIL, ENVIRONMENT, LABS } - // The different actions available in the Details section. - public enum DetailsMenuAction { - OPEN, IMPORT, PUBLISH, DOWNLOAD, DELETE, UNLIKE, LIKE, CLOSE, - OPEN_WITHOUT_SAVING, SAVE_THEN_OPEN, CANCEL_OPEN, CONFIRM_DELETE, CANCEL_DELETE - } + // The types of Zandria creations that can be loaded. + public enum CreationType { NONE, YOUR, FEATURED, LIKED } + // The different sections of the PolyMenu. + public enum PolyMenuSection { CREATION, OPTION, DETAIL, ENVIRONMENT, LABS } + // The different actions available in the Details section. + public enum DetailsMenuAction + { + OPEN, IMPORT, PUBLISH, DOWNLOAD, DELETE, UNLIKE, LIKE, CLOSE, + OPEN_WITHOUT_SAVING, SAVE_THEN_OPEN, CANCEL_OPEN, CONFIRM_DELETE, CANCEL_DELETE + } - // Set in editor. - public Sprite signedOutIcon; - - private static Color UNSELECTED_ICON_COLOR = new Color(1f, 1f, 1f, 0.117f); - private static Color SELECTED_ICON_COLOR = new Color(1f, 1f, 1f, 1f); - private static Color UNSELECTED_AVATAR_COLOR = new Color(1f, 1f, 1f, 150f / 255f); - private static Color SELECTED_AVATAR_COLOR = new Color(1f, 1f, 1f, 1f); - private static Vector3 DEFAULT_AVATAR_SCALE = new Vector3(0.1875f, 0.25f, 0.75f); - private static Vector3 USER_AVATAR_SCALE = new Vector3(0.121875f, 0.1625f, 0.75f); - - private static float DETAIL_TILE_SIZE = 0.15f; - - private static string TAKE_HEADSET_OFF_FOR_SIGN_IN_PROMPT = "Take off your headset to sign in"; - - private static StringBuilder BASE_CREATOR = new StringBuilder("by "); - - // The number of tiles for Zandria creations on the PolyMenu. - private const int TILE_COUNT = 9; - - public PolyMenuMode yourCreations = new PolyMenuMode(PolyMenuSection.CREATION, CreationType.YOUR, 0); - public PolyMenuMode featuredCreations = new PolyMenuMode(PolyMenuSection.CREATION, CreationType.FEATURED, 0); - public PolyMenuMode likedCreations = new PolyMenuMode(PolyMenuSection.CREATION, CreationType.LIKED, 0); - public PolyMenuMode options = new PolyMenuMode(PolyMenuSection.OPTION, CreationType.NONE, 0); - public PolyMenuMode environment = new PolyMenuMode(PolyMenuSection.ENVIRONMENT, CreationType.NONE, 0); - public PolyMenuMode labs = new PolyMenuMode(PolyMenuSection.LABS, CreationType.NONE, 0); - - public GameObject polyMenu; - public GameObject toolMenu; - public GameObject detailsMenu; - - // The placeholder gameObjects on the Zandria Menu. - public GameObject[] placeholders = new GameObject[TILE_COUNT]; - - private PaletteController paletteController; - private ControllerMain controllerMain; - private ZandriaCreationsManager creationsManager; - private ZandriaCreationHandler currentCreationHandler; - - // The possible menuModes in the order they can be moved through using the palette touchpad. - private PolyMenuMode[] menuModes; - // Reference to the current menuMode the user is in. - public int menuIndex; - - // Reference to the state of each menu panel. - public enum Menu { POLY_MENU, TOOLS_MENU, DETAILS_MENU }; - private Menu activeMenu = Menu.TOOLS_MENU; - - // Menu panels. - private GameObject optionsMenu; - private GameObject labsMenu; - private GameObject modelsMenu; - private GameObject noSavedModelsMenu; - private GameObject noLikedModelsMenu; - private GameObject signedOutYourModelsMenu; - private GameObject signedOutLikedModelsMenu; - private GameObject offlineModelsMenu; - private GameObject detailsPreviewHolder; - private GameObject detailsThumbnail; - private GameObject environmentMenu; - - // Menu button icons. - private SpriteRenderer optionsIcon; - private SpriteRenderer yourModelsIcon; - private SpriteRenderer likedModelsIcon; - private SpriteRenderer featuredModelsIcon; - private SpriteRenderer environmentIcon; - private SpriteRenderer labsIcon; - - // Pagination icons and text. - private SpriteRenderer pageLeftIcon; - private SpriteRenderer pageRightIcon; - private TextMeshPro pageIndicator; - - private SelectablePaginationMenuItem pageLeftScript; - private SelectablePaginationMenuItem pageRightScript; - - // Menu titles. - private GameObject optionsTitle; - private GameObject yourModelsTitle; - private GameObject likedModelsTitle; - private GameObject featuredModelsTitle; - - // Options buttons. - private TextMeshPro signInText; - private TextMeshPro addReferenceText; - private string defaultSignInMessage; - private GameObject signInButton; - private GameObject signOutButton; - private GameObject addReferenceButton; - - // User info. - private Sprite defaultUserIcon; - private string defaultDisplayName; - private TextMesh displayName; - - // Pop-up dialogs for confirmation. - private GameObject confirmSaveDialog; - private GameObject confirmDeleteDialog; - - // Creation metadata. - private GameObject creationTitle; - private GameObject creatorName; - private GameObject creationDate; - - // Detail menu buttons. - // These aren't all the buttons only the ones that need to be changed depending on creationType. - private GameObject openButton; - private SpriteRenderer openButtonIcon; - private TextMeshPro openButtonText; - private DetailsMenuActionItem openButtonScript; - - private GameObject importButton; - private SpriteRenderer importButtonIcon; - private TextMeshPro importButtonText; - private DetailsMenuActionItem importButtonScript; - - private GameObject deleteButton; - private GameObject yourModelsMenuSpacer; - private GameObject likedOrFeaturedModelsMenuSpacer; - - // Params for a lighting effect on the menu button when selected. - private const float BUTTON_LIGHT_INTENSITY = 8f; - private static readonly Color BUTTON_LIGHT_ON_COLOR = new Color(1f, .8f, 0.2f); - private static readonly Color BUTTON_LIGHT_OFF_COLOR = new Color(0f, 0f, 0f); - private static readonly Color BUTTON_EMISSIVE_COLOR = new Color(.5f, .4f, 0.2f); - - // Use this for initialization - public void Setup(ZandriaCreationsManager creationsManager, PaletteController paletteController) { - this.creationsManager = creationsManager; - this.paletteController = paletteController; - controllerMain = PeltzerMain.Instance.controllerMain; - controllerMain.ControllerActionHandler += ControllerEventHandler; - - menuModes = new PolyMenuMode[6] {options, yourCreations, featuredCreations, likedCreations, environment, labs}; - - // Set the default start up mode for the menu to be Your Models. - SwitchToYourModelsSection(); - - // Find all the appropriate GameObjects from the scene. - optionsMenu = polyMenu.transform.Find("Options").gameObject; - labsMenu = polyMenu.transform.Find("Labs").gameObject; - modelsMenu = polyMenu.transform.Find("Models").gameObject; - noSavedModelsMenu = polyMenu.transform.Find("Models-NoneSaved").gameObject; - noLikedModelsMenu = polyMenu.transform.Find("Models-NoneLiked").gameObject; - signedOutYourModelsMenu = polyMenu.transform.Find("Models-Signedout-Yours").gameObject; - signedOutLikedModelsMenu = polyMenu.transform.Find("Models-Signedout-Likes").gameObject; - offlineModelsMenu = polyMenu.transform.Find("Models-Offline").gameObject; - detailsPreviewHolder = detailsMenu.transform.Find("Model/preview/preview_holder").gameObject; - detailsThumbnail = detailsMenu.transform.Find("Model/Thumbnail").gameObject; - environmentMenu = polyMenu.transform.Find("Environments").gameObject; - - optionsIcon = polyMenu.transform.Find("NavBar/Options/panel/ic").GetComponent(); - yourModelsIcon = polyMenu.transform.Find("NavBar/Your-Models/panel/ic").GetComponent(); - likedModelsIcon = polyMenu.transform.Find("NavBar/Liked-Models/panel/ic").GetComponent(); - featuredModelsIcon = polyMenu.transform.Find("NavBar/Featured-Models/panel/ic").GetComponent(); - environmentIcon = polyMenu.transform.Find("NavBar/Environments/panel/ic").GetComponent(); - labsIcon = polyMenu.transform.Find("NavBar/LabsSection/panel/ic").GetComponent(); - - pageLeftIcon = polyMenu.transform.Find("Models/Pagination/Left/panel/ic").GetComponent(); - pageRightIcon = polyMenu.transform.Find("Models/Pagination/Right/panel/ic").GetComponent(); - pageIndicator = polyMenu.transform.Find("Models/Pagination/PageIndicator/txt").GetComponent(); - - pageLeftScript = polyMenu.transform.Find("Models/Pagination/Left/panel") - .GetComponent(); - pageRightScript = polyMenu.transform.Find("Models/Pagination/Right/panel") - .GetComponent(); - - optionsTitle = polyMenu.transform.Find("Titles/options_title").gameObject; - yourModelsTitle = polyMenu.transform.Find("Titles/your_models_title").gameObject; - likedModelsTitle = polyMenu.transform.Find("Titles/likes_title").gameObject; - featuredModelsTitle = polyMenu.transform.Find("Titles/featured_title").gameObject; - - signInText = polyMenu.transform.Find("Options/sign_in/bg/txt").GetComponent(); - defaultSignInMessage = polyMenu.transform.Find("Options/sign_in/bg/txt").GetComponent().text; - - signInButton = polyMenu.transform.Find("Options/sign_in").gameObject; - signOutButton = polyMenu.transform.Find("Options/sign_out").gameObject; - - displayName = polyMenu.transform.Find("Options/sign_out/bg/txt").GetComponent(); - defaultDisplayName = polyMenu.transform.Find("Options/sign_out/bg/txt").GetComponent().text; - - confirmSaveDialog = detailsMenu.transform.Find("ConfirmSave").gameObject; - confirmDeleteDialog = detailsMenu.transform.Find("ConfirmDelete").gameObject; - - creationTitle = detailsMenu.transform.Find("Metadata/txt-title").gameObject; - creatorName = detailsMenu.transform.Find("Metadata/txt-name").gameObject; - creationDate = detailsMenu.transform.Find("Metadata/txt-time").gameObject; - - openButton = detailsMenu.transform.Find("Buttons/Open").gameObject; - openButtonIcon = detailsMenu.transform.Find("Buttons/Open/bg/ic").GetComponent(); - openButtonText = detailsMenu.transform.Find("Buttons/Open/bg/txt").GetComponent(); - openButtonScript = detailsMenu.transform.Find("Buttons/Open/bg").GetComponent(); - - importButton = detailsMenu.transform.Find("Buttons/Import").gameObject; - importButtonIcon = detailsMenu.transform.Find("Buttons/Import/bg/ic").GetComponent(); - importButtonText = detailsMenu.transform.Find("Buttons/Import/bg/txt").GetComponent(); - importButtonScript = detailsMenu.transform.Find("Buttons/Import/bg").GetComponent(); - - deleteButton = detailsMenu.transform.Find("Buttons/Delete").gameObject; - yourModelsMenuSpacer = detailsMenu.transform.Find("Buttons/bg-space").gameObject; - likedOrFeaturedModelsMenuSpacer = detailsMenu.transform.Find("Buttons/bg-space2").gameObject; - } + // Set in editor. + public Sprite signedOutIcon; + + private static Color UNSELECTED_ICON_COLOR = new Color(1f, 1f, 1f, 0.117f); + private static Color SELECTED_ICON_COLOR = new Color(1f, 1f, 1f, 1f); + private static Color UNSELECTED_AVATAR_COLOR = new Color(1f, 1f, 1f, 150f / 255f); + private static Color SELECTED_AVATAR_COLOR = new Color(1f, 1f, 1f, 1f); + private static Vector3 DEFAULT_AVATAR_SCALE = new Vector3(0.1875f, 0.25f, 0.75f); + private static Vector3 USER_AVATAR_SCALE = new Vector3(0.121875f, 0.1625f, 0.75f); + + private static float DETAIL_TILE_SIZE = 0.15f; + + private static string TAKE_HEADSET_OFF_FOR_SIGN_IN_PROMPT = "Take off your headset to sign in"; + + private static StringBuilder BASE_CREATOR = new StringBuilder("by "); + + // The number of tiles for Zandria creations on the PolyMenu. + private const int TILE_COUNT = 9; + + public PolyMenuMode yourCreations = new PolyMenuMode(PolyMenuSection.CREATION, CreationType.YOUR, 0); + public PolyMenuMode featuredCreations = new PolyMenuMode(PolyMenuSection.CREATION, CreationType.FEATURED, 0); + public PolyMenuMode likedCreations = new PolyMenuMode(PolyMenuSection.CREATION, CreationType.LIKED, 0); + public PolyMenuMode options = new PolyMenuMode(PolyMenuSection.OPTION, CreationType.NONE, 0); + public PolyMenuMode environment = new PolyMenuMode(PolyMenuSection.ENVIRONMENT, CreationType.NONE, 0); + public PolyMenuMode labs = new PolyMenuMode(PolyMenuSection.LABS, CreationType.NONE, 0); + + public GameObject polyMenu; + public GameObject toolMenu; + public GameObject detailsMenu; + + // The placeholder gameObjects on the Zandria Menu. + public GameObject[] placeholders = new GameObject[TILE_COUNT]; + + private PaletteController paletteController; + private ControllerMain controllerMain; + private ZandriaCreationsManager creationsManager; + private ZandriaCreationHandler currentCreationHandler; + + // The possible menuModes in the order they can be moved through using the palette touchpad. + private PolyMenuMode[] menuModes; + // Reference to the current menuMode the user is in. + public int menuIndex; + + // Reference to the state of each menu panel. + public enum Menu { POLY_MENU, TOOLS_MENU, DETAILS_MENU }; + private Menu activeMenu = Menu.TOOLS_MENU; + + // Menu panels. + private GameObject optionsMenu; + private GameObject labsMenu; + private GameObject modelsMenu; + private GameObject noSavedModelsMenu; + private GameObject noLikedModelsMenu; + private GameObject signedOutYourModelsMenu; + private GameObject signedOutLikedModelsMenu; + private GameObject offlineModelsMenu; + private GameObject detailsPreviewHolder; + private GameObject detailsThumbnail; + private GameObject environmentMenu; + + // Menu button icons. + private SpriteRenderer optionsIcon; + private SpriteRenderer yourModelsIcon; + private SpriteRenderer likedModelsIcon; + private SpriteRenderer featuredModelsIcon; + private SpriteRenderer environmentIcon; + private SpriteRenderer labsIcon; + + // Pagination icons and text. + private SpriteRenderer pageLeftIcon; + private SpriteRenderer pageRightIcon; + private TextMeshPro pageIndicator; + + private SelectablePaginationMenuItem pageLeftScript; + private SelectablePaginationMenuItem pageRightScript; + + // Menu titles. + private GameObject optionsTitle; + private GameObject yourModelsTitle; + private GameObject likedModelsTitle; + private GameObject featuredModelsTitle; + + // Options buttons. + private TextMeshPro signInText; + private TextMeshPro addReferenceText; + private string defaultSignInMessage; + private GameObject signInButton; + private GameObject signOutButton; + private GameObject addReferenceButton; + + // User info. + private Sprite defaultUserIcon; + private string defaultDisplayName; + private TextMesh displayName; + + // Pop-up dialogs for confirmation. + private GameObject confirmSaveDialog; + private GameObject confirmDeleteDialog; + + // Creation metadata. + private GameObject creationTitle; + private GameObject creatorName; + private GameObject creationDate; + + // Detail menu buttons. + // These aren't all the buttons only the ones that need to be changed depending on creationType. + private GameObject openButton; + private SpriteRenderer openButtonIcon; + private TextMeshPro openButtonText; + private DetailsMenuActionItem openButtonScript; + + private GameObject importButton; + private SpriteRenderer importButtonIcon; + private TextMeshPro importButtonText; + private DetailsMenuActionItem importButtonScript; + + private GameObject deleteButton; + private GameObject yourModelsMenuSpacer; + private GameObject likedOrFeaturedModelsMenuSpacer; + + // Params for a lighting effect on the menu button when selected. + private const float BUTTON_LIGHT_INTENSITY = 8f; + private static readonly Color BUTTON_LIGHT_ON_COLOR = new Color(1f, .8f, 0.2f); + private static readonly Color BUTTON_LIGHT_OFF_COLOR = new Color(0f, 0f, 0f); + private static readonly Color BUTTON_EMISSIVE_COLOR = new Color(.5f, .4f, 0.2f); + + // Use this for initialization + public void Setup(ZandriaCreationsManager creationsManager, PaletteController paletteController) + { + this.creationsManager = creationsManager; + this.paletteController = paletteController; + controllerMain = PeltzerMain.Instance.controllerMain; + controllerMain.ControllerActionHandler += ControllerEventHandler; + + menuModes = new PolyMenuMode[6] { options, yourCreations, featuredCreations, likedCreations, environment, labs }; + + // Set the default start up mode for the menu to be Your Models. + SwitchToYourModelsSection(); + + // Find all the appropriate GameObjects from the scene. + optionsMenu = polyMenu.transform.Find("Options").gameObject; + labsMenu = polyMenu.transform.Find("Labs").gameObject; + modelsMenu = polyMenu.transform.Find("Models").gameObject; + noSavedModelsMenu = polyMenu.transform.Find("Models-NoneSaved").gameObject; + noLikedModelsMenu = polyMenu.transform.Find("Models-NoneLiked").gameObject; + signedOutYourModelsMenu = polyMenu.transform.Find("Models-Signedout-Yours").gameObject; + signedOutLikedModelsMenu = polyMenu.transform.Find("Models-Signedout-Likes").gameObject; + offlineModelsMenu = polyMenu.transform.Find("Models-Offline").gameObject; + detailsPreviewHolder = detailsMenu.transform.Find("Model/preview/preview_holder").gameObject; + detailsThumbnail = detailsMenu.transform.Find("Model/Thumbnail").gameObject; + environmentMenu = polyMenu.transform.Find("Environments").gameObject; + + optionsIcon = polyMenu.transform.Find("NavBar/Options/panel/ic").GetComponent(); + yourModelsIcon = polyMenu.transform.Find("NavBar/Your-Models/panel/ic").GetComponent(); + likedModelsIcon = polyMenu.transform.Find("NavBar/Liked-Models/panel/ic").GetComponent(); + featuredModelsIcon = polyMenu.transform.Find("NavBar/Featured-Models/panel/ic").GetComponent(); + environmentIcon = polyMenu.transform.Find("NavBar/Environments/panel/ic").GetComponent(); + labsIcon = polyMenu.transform.Find("NavBar/LabsSection/panel/ic").GetComponent(); + + pageLeftIcon = polyMenu.transform.Find("Models/Pagination/Left/panel/ic").GetComponent(); + pageRightIcon = polyMenu.transform.Find("Models/Pagination/Right/panel/ic").GetComponent(); + pageIndicator = polyMenu.transform.Find("Models/Pagination/PageIndicator/txt").GetComponent(); + + pageLeftScript = polyMenu.transform.Find("Models/Pagination/Left/panel") + .GetComponent(); + pageRightScript = polyMenu.transform.Find("Models/Pagination/Right/panel") + .GetComponent(); + + optionsTitle = polyMenu.transform.Find("Titles/options_title").gameObject; + yourModelsTitle = polyMenu.transform.Find("Titles/your_models_title").gameObject; + likedModelsTitle = polyMenu.transform.Find("Titles/likes_title").gameObject; + featuredModelsTitle = polyMenu.transform.Find("Titles/featured_title").gameObject; + + signInText = polyMenu.transform.Find("Options/sign_in/bg/txt").GetComponent(); + defaultSignInMessage = polyMenu.transform.Find("Options/sign_in/bg/txt").GetComponent().text; + + signInButton = polyMenu.transform.Find("Options/sign_in").gameObject; + signOutButton = polyMenu.transform.Find("Options/sign_out").gameObject; + + displayName = polyMenu.transform.Find("Options/sign_out/bg/txt").GetComponent(); + defaultDisplayName = polyMenu.transform.Find("Options/sign_out/bg/txt").GetComponent().text; + + confirmSaveDialog = detailsMenu.transform.Find("ConfirmSave").gameObject; + confirmDeleteDialog = detailsMenu.transform.Find("ConfirmDelete").gameObject; + + creationTitle = detailsMenu.transform.Find("Metadata/txt-title").gameObject; + creatorName = detailsMenu.transform.Find("Metadata/txt-name").gameObject; + creationDate = detailsMenu.transform.Find("Metadata/txt-time").gameObject; + + openButton = detailsMenu.transform.Find("Buttons/Open").gameObject; + openButtonIcon = detailsMenu.transform.Find("Buttons/Open/bg/ic").GetComponent(); + openButtonText = detailsMenu.transform.Find("Buttons/Open/bg/txt").GetComponent(); + openButtonScript = detailsMenu.transform.Find("Buttons/Open/bg").GetComponent(); + + importButton = detailsMenu.transform.Find("Buttons/Import").gameObject; + importButtonIcon = detailsMenu.transform.Find("Buttons/Import/bg/ic").GetComponent(); + importButtonText = detailsMenu.transform.Find("Buttons/Import/bg/txt").GetComponent(); + importButtonScript = detailsMenu.transform.Find("Buttons/Import/bg").GetComponent(); + + deleteButton = detailsMenu.transform.Find("Buttons/Delete").gameObject; + yourModelsMenuSpacer = detailsMenu.transform.Find("Buttons/bg-space").gameObject; + likedOrFeaturedModelsMenuSpacer = detailsMenu.transform.Find("Buttons/bg-space2").gameObject; + } - // Whether the controller events indicate the user wants to toggle between the PolyMenu and ToolMenu. - private bool IsTogglePolyMenuEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.ApplicationMenu - && args.Action == ButtonAction.DOWN - && !PeltzerMain.Instance.Zoomer.Zooming; - } + // Whether the controller events indicate the user wants to toggle between the PolyMenu and ToolMenu. + private bool IsTogglePolyMenuEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.ApplicationMenu + && args.Action == ButtonAction.DOWN + && !PeltzerMain.Instance.Zoomer.Zooming; + } - // Whether the controller events indicate the user wants to move up through the PolyMenu sections. - private bool IsProgressMenuUpEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN - && args.TouchpadLocation == TouchpadLocation.TOP; - } + // Whether the controller events indicate the user wants to move up through the PolyMenu sections. + private bool IsProgressMenuUpEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN + && args.TouchpadLocation == TouchpadLocation.TOP; + } - // Whether the controller events indicate the user wants to move down through the PolyMenu sections. - private bool IsProgressMenuDownEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN - && args.TouchpadLocation == TouchpadLocation.BOTTOM; - } + // Whether the controller events indicate the user wants to move down through the PolyMenu sections. + private bool IsProgressMenuDownEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN + && args.TouchpadLocation == TouchpadLocation.BOTTOM; + } - // Whether the controller events indicate the user wants to move to the next page. - private bool IsProgressPageRightEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN - && args.TouchpadLocation == TouchpadLocation.RIGHT; - } + // Whether the controller events indicate the user wants to move to the next page. + private bool IsProgressPageRightEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN + && args.TouchpadLocation == TouchpadLocation.RIGHT; + } - // Whether the controller events indicate the user wants to move back to the previous page. - private bool IsProgressPageLeftEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN - && args.TouchpadLocation == TouchpadLocation.LEFT; - } + // Whether the controller events indicate the user wants to move back to the previous page. + private bool IsProgressPageLeftEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN + && args.TouchpadLocation == TouchpadLocation.LEFT; + } - private void ControllerEventHandler(object sender, ControllerEventArgs args) { - if (!PeltzerMain.Instance.restrictionManager.menuActionsAllowed) { - return; - } - - // If the user hits toggle on the details page, or the poly menu, we take them to the tools menu. - // If they hit toggle on the tools menu, we take them to the poly menu. - if (IsTogglePolyMenuEvent(args)) { - // If menu switching is not allowed at this time, return. - if (!PeltzerMain.Instance.restrictionManager.menuSwitchAllowed) { - return; - } - - // Play some feedback. - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - - // Disable the 'click me' tooltip for toggling this menu. - PeltzerMain.Instance.applicationButtonToolTips.TurnOff(); - - // Toggle the state of the menus. - if (activeMenu == Menu.TOOLS_MENU) { - SetActiveMenu(Menu.POLY_MENU); - } else { - SetActiveMenu(Menu.TOOLS_MENU); - } - - if (activeMenu != Menu.TOOLS_MENU) { - ChangeMenu(); - } - } else { - // Only handle controller events for moving through the Poly menu if the menu is open and we - // are not currently zooming. - if (polyMenu.activeInHierarchy && !PeltzerMain.Instance.Zoomer.Zooming) { - if (IsProgressMenuDownEvent(args)) { - // If we've hit the end of the menu the user can't carousel around. - if (menuIndex == menuModes.Length - 1) { - // The current operation is invalid, play error... - paletteController.TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_2, /* durationSeconds */ 0.25f, /* strength */ 0.3f); - } else { - menuIndex++; - ChangeMenu(); - } - } else if (IsProgressMenuUpEvent(args)) { - // If we've hit the top of the menu the user can't carousel around. - if (menuIndex == 0) { - // The current operation is invalid, play error... - paletteController.TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_2, /* durationSeconds */ 0.25f, /* strength */ 0.3f); - } else { - menuIndex--; - ChangeMenu(); - } - } else if (IsProgressPageRightEvent(args)) { - if (CurrentMenuSection() == PolyMenuSection.OPTION) { - // There is no pagination for Options. - return; - } - - // If we are already on the last page the user can't carousel around. - if (CurrentPage() == creationsManager.GetNumberOfPages(CurrentCreationType()) - 1) { - // The current operation is invalid, play error... - paletteController.TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_2, /* durationSeconds */ 0.25f, /* strength */ 0.3f); - } else { - menuModes[menuIndex].page++; - ChangeMenu(); - } - } else if (IsProgressPageLeftEvent(args)) { - if (CurrentMenuSection() == PolyMenuSection.OPTION) { - // There is no pagination for Options. - return; - } - - // If we are already on the first page the user can't carousel around. - if (menuModes[menuIndex].page == 0) { - // The current operation is invalid, play error... - paletteController.TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_2, /* durationSeconds */ 0.25f, /* strength */ 0.3f); - } else { - menuModes[menuIndex].page--; - ChangeMenu(); - } - } - } - } - } + private void ControllerEventHandler(object sender, ControllerEventArgs args) + { + if (!PeltzerMain.Instance.restrictionManager.menuActionsAllowed) + { + return; + } - /// - /// Adds a light effect to the button when the poly menu is activated, and removes it when the menu - /// is closed. - /// - private void ToggleButtonState() { - if (activeMenu == Menu.POLY_MENU) { - // Flip on - paletteController.controllerGeometry.appMenuButton.GetComponent().material.SetColor("_EmissiveColor", BUTTON_EMISSIVE_COLOR); - } else { - // Flip off - paletteController.controllerGeometry.appMenuButton.GetComponent().material.SetColor("_EmissiveColor", BUTTON_LIGHT_OFF_COLOR); - } - } + // If the user hits toggle on the details page, or the poly menu, we take them to the tools menu. + // If they hit toggle on the tools menu, we take them to the poly menu. + if (IsTogglePolyMenuEvent(args)) + { + // If menu switching is not allowed at this time, return. + if (!PeltzerMain.Instance.restrictionManager.menuSwitchAllowed) + { + return; + } + + // Play some feedback. + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + + // Disable the 'click me' tooltip for toggling this menu. + PeltzerMain.Instance.applicationButtonToolTips.TurnOff(); + + // Toggle the state of the menus. + if (activeMenu == Menu.TOOLS_MENU) + { + SetActiveMenu(Menu.POLY_MENU); + } + else + { + SetActiveMenu(Menu.TOOLS_MENU); + } + + if (activeMenu != Menu.TOOLS_MENU) + { + ChangeMenu(); + } + } + else + { + // Only handle controller events for moving through the Poly menu if the menu is open and we + // are not currently zooming. + if (polyMenu.activeInHierarchy && !PeltzerMain.Instance.Zoomer.Zooming) + { + if (IsProgressMenuDownEvent(args)) + { + // If we've hit the end of the menu the user can't carousel around. + if (menuIndex == menuModes.Length - 1) + { + // The current operation is invalid, play error... + paletteController.TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_2, /* durationSeconds */ 0.25f, /* strength */ 0.3f); + } + else + { + menuIndex++; + ChangeMenu(); + } + } + else if (IsProgressMenuUpEvent(args)) + { + // If we've hit the top of the menu the user can't carousel around. + if (menuIndex == 0) + { + // The current operation is invalid, play error... + paletteController.TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_2, /* durationSeconds */ 0.25f, /* strength */ 0.3f); + } + else + { + menuIndex--; + ChangeMenu(); + } + } + else if (IsProgressPageRightEvent(args)) + { + if (CurrentMenuSection() == PolyMenuSection.OPTION) + { + // There is no pagination for Options. + return; + } + + // If we are already on the last page the user can't carousel around. + if (CurrentPage() == creationsManager.GetNumberOfPages(CurrentCreationType()) - 1) + { + // The current operation is invalid, play error... + paletteController.TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_2, /* durationSeconds */ 0.25f, /* strength */ 0.3f); + } + else + { + menuModes[menuIndex].page++; + ChangeMenu(); + } + } + else if (IsProgressPageLeftEvent(args)) + { + if (CurrentMenuSection() == PolyMenuSection.OPTION) + { + // There is no pagination for Options. + return; + } + + // If we are already on the first page the user can't carousel around. + if (menuModes[menuIndex].page == 0) + { + // The current operation is invalid, play error... + paletteController.TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_2, /* durationSeconds */ 0.25f, /* strength */ 0.3f); + } + else + { + menuModes[menuIndex].page--; + ChangeMenu(); + } + } + } + } + } - /// - /// Updates the selected sub-section of the POLY_MENU to be 'Your Models'. - /// - public void SwitchToYourModelsSection() { - menuIndex = 1; - } + /// + /// Adds a light effect to the button when the poly menu is activated, and removes it when the menu + /// is closed. + /// + private void ToggleButtonState() + { + if (activeMenu == Menu.POLY_MENU) + { + // Flip on + paletteController.controllerGeometry.appMenuButton.GetComponent().material.SetColor("_EmissiveColor", BUTTON_EMISSIVE_COLOR); + } + else + { + // Flip off + paletteController.controllerGeometry.appMenuButton.GetComponent().material.SetColor("_EmissiveColor", BUTTON_LIGHT_OFF_COLOR); + } + } - /// - /// Updates the selected sub-section of the POLY_MENU to be 'Your Models'. - /// - public void SwitchToFeaturedSection() { - menuIndex = 2; - } + /// + /// Updates the selected sub-section of the POLY_MENU to be 'Your Models'. + /// + public void SwitchToYourModelsSection() + { + menuIndex = 1; + } - /// - /// Set the active menu, and set GameObjects active as required. - /// - public void SetActiveMenu(Menu menu) { - if (menu == activeMenu) return; - - // Clean up the details menu if it's being closed or opened. - if (activeMenu == Menu.DETAILS_MENU || menu == Menu.DETAILS_MENU) { - // Destroy the preview we created. - for (int i = 0; i < detailsPreviewHolder.transform.childCount; i++) { - Destroy(detailsPreviewHolder.transform.GetChild(i).gameObject); - } - - currentCreationHandler = null; - detailsPreviewHolder.GetComponent().meshes = null; - } - - activeMenu = menu; - ToggleButtonState(); - PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.toggleMenuSound); - - polyMenu.SetActive(activeMenu == Menu.POLY_MENU); - toolMenu.SetActive(activeMenu == Menu.TOOLS_MENU); - detailsMenu.SetActive(activeMenu == Menu.DETAILS_MENU); - - if (activeMenu == Menu.TOOLS_MENU) { - paletteController.ResetTouchpadOverlay(); - PeltzerMain.Instance.restrictionManager.undoRedoAllowed = true; - } else { - paletteController.ChangeTouchpadOverlay(TouchpadOverlay.MENU); - PeltzerMain.Instance.restrictionManager.undoRedoAllowed = false; - } - } + /// + /// Updates the selected sub-section of the POLY_MENU to be 'Your Models'. + /// + public void SwitchToFeaturedSection() + { + menuIndex = 2; + } - /// - /// Takes input from a user clicking on a Details section menu button and executes the correct action for the - /// button. - /// - /// The action to take. - public void InvokeDetailsMenuAction(DetailsMenuAction action) { - switch (action) { - case DetailsMenuAction.OPEN: - if (currentCreationHandler != null) { - // Only show the save confirmation dialog if modified since last save. - if (PeltzerMain.Instance.ModelChangedSinceLastSave) { - // The save confirmation dialog will call InvokeDetailsMenuAction according to the user's - // decision (CANCEL_OPEN, OPEN_WITH_SAVING or SAVE_THEN_OPEN). - confirmSaveDialog.SetActive(true); - } else { - // Not modified since last save, so we can clear without confirmation. - OpenCreation(currentCreationHandler); - } - } - break; - case DetailsMenuAction.IMPORT: - // Import is the same action as quick selecting a zandria creation so we can just grab the meshes on the - // quick select script attached to the preview. - SelectCreation( - detailsPreviewHolder.GetComponent().meshes, - currentCreationHandler.creationAssetId); - // Clear the detailSizedMeshes from the creation handler when importing, as import grabs a direct mutable - // reference to these to avoid any lag in generating a copy. Instead, the lag of generating a copy will - // happen the next time the user opens the details page for this model again. - currentCreationHandler.detailSizedMeshes.Clear(); - break; - case DetailsMenuAction.DELETE: - confirmDeleteDialog.SetActive(true); - break; - case DetailsMenuAction.CANCEL_DELETE: - confirmDeleteDialog.SetActive(false); - break; - case DetailsMenuAction.CONFIRM_DELETE: - confirmDeleteDialog.SetActive(false); - // Remove the asset from the list of creations displayed. - if (currentCreationHandler.creationAssetId != null) { - creationsManager.RemoveSingleCreationAndRefreshMenu( - CurrentCreationType(), currentCreationHandler.creationAssetId); - // Invoke the RPC that removes the creation from storage - StartCoroutine(creationsManager.assetsServiceClient.DeleteAsset(currentCreationHandler.creationAssetId)); - } - if (currentCreationHandler.creationLocalId != null) { - creationsManager.RemoveSingleCreationAndRefreshMenu( - CurrentCreationType(), currentCreationHandler.creationLocalId); - creationsManager.DeleteOfflineModel(currentCreationHandler.creationLocalId); - } - SetActiveMenu(Menu.POLY_MENU); - break; - case DetailsMenuAction.CLOSE: - SetActiveMenu(Menu.POLY_MENU); - break; - case DetailsMenuAction.CANCEL_OPEN: - confirmSaveDialog.SetActive(false); - break; - case DetailsMenuAction.OPEN_WITHOUT_SAVING: - // The user does not want to save their current changes so we can just clear the scene and load the creation. - OpenCreation(currentCreationHandler); - break; - case DetailsMenuAction.SAVE_THEN_OPEN: - // The user wants to save before opening a new creation. We need to wait for the save to complete because - // saving makes the model unwritable so we can't clear it and add the new creation to the model until it's - // done. - confirmSaveDialog.SetActive(false); - PeltzerMain.Instance.saveCompleteAction = () => { - OpenCreation(currentCreationHandler); - }; - PeltzerMain.Instance.SaveCurrentModel(publish:false, saveSelected:false); - break; - } - } + /// + /// Set the active menu, and set GameObjects active as required. + /// + public void SetActiveMenu(Menu menu) + { + if (menu == activeMenu) return; + + // Clean up the details menu if it's being closed or opened. + if (activeMenu == Menu.DETAILS_MENU || menu == Menu.DETAILS_MENU) + { + // Destroy the preview we created. + for (int i = 0; i < detailsPreviewHolder.transform.childCount; i++) + { + Destroy(detailsPreviewHolder.transform.GetChild(i).gameObject); + } + + currentCreationHandler = null; + detailsPreviewHolder.GetComponent().meshes = null; + } - /// - /// Performs the 'open' request on a creation. - /// - /// - private void OpenCreation(ZandriaCreationHandler creationHandler) { - confirmSaveDialog.SetActive(false); - PeltzerMain.Instance.CreateNewModel(); - - PeltzerMain.LoadOptions options = new PeltzerMain.LoadOptions(); - options.cloneBeforeLoad = true; - - if (CurrentCreationType() == CreationType.YOUR) { - // Creation belongs to the user, so don't override remix IDs (we leave them as-is on the file). - // But we have to remember the ID of the asset so we can later save it with the same asset ID (to overwrite). - if (currentCreationHandler.creationAssetId != null) { - PeltzerMain.Instance.AssetId = currentCreationHandler.creationAssetId; - } else if (currentCreationHandler.creationLocalId != null) { - PeltzerMain.Instance.LocalId = currentCreationHandler.creationLocalId; - } - } else { - // Creation doesn't belong to the user, so set the attribution appropriately by setting - // the remix ID of all meshes. - options.overrideRemixId = creationHandler.creationAssetId; - } - PeltzerMain.Instance.LoadPeltzerFileIntoModel(currentCreationHandler.peltzerFile, options); - - if (Features.adjustWorldSpaceOnOpen) { - WorldSpaceAdjuster.AdjustWorldSpace(); - } - - - SetActiveMenu(Menu.TOOLS_MENU); - } + activeMenu = menu; + ToggleButtonState(); + PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.toggleMenuSound); - /// - /// Changes the menu to a passed menuIndex. This is used when selecting menu options using the peltzerController - /// and not the palette touchpad. - /// - /// The new menu index that was selected. - /// Whether to also set the passed menuIndex to the first page. - public void ApplyMenuChange(int selectedMenuIndex, bool setToFirstPage = false) { - if (setToFirstPage || selectedMenuIndex == menuIndex) { - menuModes[selectedMenuIndex].page = 0; - } - menuIndex = selectedMenuIndex; - ChangeMenu(); - } + polyMenu.SetActive(activeMenu == Menu.POLY_MENU); + toolMenu.SetActive(activeMenu == Menu.TOOLS_MENU); + detailsMenu.SetActive(activeMenu == Menu.DETAILS_MENU); - public void ApplyPageChange(int indexChange) { - // The left and right buttons will disable themselves if there are no valid actions so we don't have to worry - // about checking if the indexChange is valid. - menuModes[menuIndex].page += indexChange; - ChangeMenu(); - } + if (activeMenu == Menu.TOOLS_MENU) + { + paletteController.ResetTouchpadOverlay(); + PeltzerMain.Instance.restrictionManager.undoRedoAllowed = true; + } + else + { + paletteController.ChangeTouchpadOverlay(TouchpadOverlay.MENU); + PeltzerMain.Instance.restrictionManager.undoRedoAllowed = false; + } + } - private void ChangePaginationButtons() { - int numPages = creationsManager.GetNumberOfPages(CurrentCreationType()); + /// + /// Takes input from a user clicking on a Details section menu button and executes the correct action for the + /// button. + /// + /// The action to take. + public void InvokeDetailsMenuAction(DetailsMenuAction action) + { + switch (action) + { + case DetailsMenuAction.OPEN: + if (currentCreationHandler != null) + { + // Only show the save confirmation dialog if modified since last save. + if (PeltzerMain.Instance.ModelChangedSinceLastSave) + { + // The save confirmation dialog will call InvokeDetailsMenuAction according to the user's + // decision (CANCEL_OPEN, OPEN_WITH_SAVING or SAVE_THEN_OPEN). + confirmSaveDialog.SetActive(true); + } + else + { + // Not modified since last save, so we can clear without confirmation. + OpenCreation(currentCreationHandler); + } + } + break; + case DetailsMenuAction.IMPORT: + // Import is the same action as quick selecting a zandria creation so we can just grab the meshes on the + // quick select script attached to the preview. + SelectCreation( + detailsPreviewHolder.GetComponent().meshes, + currentCreationHandler.creationAssetId); + // Clear the detailSizedMeshes from the creation handler when importing, as import grabs a direct mutable + // reference to these to avoid any lag in generating a copy. Instead, the lag of generating a copy will + // happen the next time the user opens the details page for this model again. + currentCreationHandler.detailSizedMeshes.Clear(); + break; + case DetailsMenuAction.DELETE: + confirmDeleteDialog.SetActive(true); + break; + case DetailsMenuAction.CANCEL_DELETE: + confirmDeleteDialog.SetActive(false); + break; + case DetailsMenuAction.CONFIRM_DELETE: + confirmDeleteDialog.SetActive(false); + // Remove the asset from the list of creations displayed. + if (currentCreationHandler.creationAssetId != null) + { + creationsManager.RemoveSingleCreationAndRefreshMenu( + CurrentCreationType(), currentCreationHandler.creationAssetId); + // Invoke the RPC that removes the creation from storage + StartCoroutine(creationsManager.assetsServiceClient.DeleteAsset(currentCreationHandler.creationAssetId)); + } + if (currentCreationHandler.creationLocalId != null) + { + creationsManager.RemoveSingleCreationAndRefreshMenu( + CurrentCreationType(), currentCreationHandler.creationLocalId); + creationsManager.DeleteOfflineModel(currentCreationHandler.creationLocalId); + } + SetActiveMenu(Menu.POLY_MENU); + break; + case DetailsMenuAction.CLOSE: + SetActiveMenu(Menu.POLY_MENU); + break; + case DetailsMenuAction.CANCEL_OPEN: + confirmSaveDialog.SetActive(false); + break; + case DetailsMenuAction.OPEN_WITHOUT_SAVING: + // The user does not want to save their current changes so we can just clear the scene and load the creation. + OpenCreation(currentCreationHandler); + break; + case DetailsMenuAction.SAVE_THEN_OPEN: + // The user wants to save before opening a new creation. We need to wait for the save to complete because + // saving makes the model unwritable so we can't clear it and add the new creation to the model until it's + // done. + confirmSaveDialog.SetActive(false); + PeltzerMain.Instance.saveCompleteAction = () => + { + OpenCreation(currentCreationHandler); + }; + PeltzerMain.Instance.SaveCurrentModel(publish: false, saveSelected: false); + break; + } + } - bool pageLeftActive = CurrentPage() > 0; - bool pageRightActive = CurrentPage() < numPages - 1; + /// + /// Performs the 'open' request on a creation. + /// + /// + private void OpenCreation(ZandriaCreationHandler creationHandler) + { + confirmSaveDialog.SetActive(false); + PeltzerMain.Instance.CreateNewModel(); + + PeltzerMain.LoadOptions options = new PeltzerMain.LoadOptions(); + options.cloneBeforeLoad = true; + + if (CurrentCreationType() == CreationType.YOUR) + { + // Creation belongs to the user, so don't override remix IDs (we leave them as-is on the file). + // But we have to remember the ID of the asset so we can later save it with the same asset ID (to overwrite). + if (currentCreationHandler.creationAssetId != null) + { + PeltzerMain.Instance.AssetId = currentCreationHandler.creationAssetId; + } + else if (currentCreationHandler.creationLocalId != null) + { + PeltzerMain.Instance.LocalId = currentCreationHandler.creationLocalId; + } + } + else + { + // Creation doesn't belong to the user, so set the attribution appropriately by setting + // the remix ID of all meshes. + options.overrideRemixId = creationHandler.creationAssetId; + } + PeltzerMain.Instance.LoadPeltzerFileIntoModel(currentCreationHandler.peltzerFile, options); - pageLeftIcon.color = pageLeftActive ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; - pageRightIcon.color = pageRightActive ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; + if (Features.adjustWorldSpaceOnOpen) + { + WorldSpaceAdjuster.AdjustWorldSpace(); + } - pageLeftScript.isActive = pageLeftActive; - pageRightScript.isActive = pageRightActive; - // Don't use a zero index for the page display. - int currentPageForDisplay = CurrentPage() + 1; - pageIndicator.text = string.Format("{0} of {1}", currentPageForDisplay, numPages); - } + SetActiveMenu(Menu.TOOLS_MENU); + } - private void ChangeMenu() { - // Start by ensuring Poly Menu is active, to cover the case that 'details' was open and needs to be closed. - SetActiveMenu(Menu.POLY_MENU); - - // Activate the correct title and deactivate all others. - if (optionsTitle != null) { - optionsTitle.SetActive(CurrentMenuSection() == PolyMenuSection.OPTION); - } - if (yourModelsTitle != null) { - yourModelsTitle.SetActive(CurrentCreationType() == CreationType.YOUR); - } - if (likedModelsTitle != null) { - likedModelsTitle.SetActive(CurrentCreationType() == CreationType.LIKED); - } - if (featuredModelsTitle != null) { - featuredModelsTitle.SetActive(CurrentCreationType() == CreationType.FEATURED); - } - - if (optionsIcon != null) { - optionsIcon.color = CurrentMenuSection() == - PolyMenuSection.OPTION ? SELECTED_AVATAR_COLOR : UNSELECTED_AVATAR_COLOR; - } - if (yourModelsIcon != null) { - yourModelsIcon.color = CurrentCreationType() == - CreationType.YOUR ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; - } - if (likedModelsIcon != null) { - likedModelsIcon.color = CurrentCreationType() == - CreationType.LIKED ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; - } - if (featuredModelsIcon != null) { - featuredModelsIcon.color = CurrentCreationType() == - CreationType.FEATURED ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; - } - if (environmentIcon != null) { - environmentIcon.color = CurrentMenuSection() == PolyMenuSection.ENVIRONMENT ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; - } - if (labsIcon != null) { - labsIcon.color = CurrentMenuSection() == PolyMenuSection.LABS ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; - } - - // Activate or deactive the necessary menus. - if (optionsMenu != null) { - optionsMenu.SetActive(CurrentMenuSection() == PolyMenuSection.OPTION); - } - - if (labsMenu != null) { - labsMenu.SetActive(CurrentMenuSection() == PolyMenuSection.LABS); - } - if (environmentMenu != null) { - environmentMenu.SetActive(CurrentMenuSection() == PolyMenuSection.ENVIRONMENT); - } - // Deactivate all the user prompt menus. If they need to be activated they will be in PopulateZandriaMenu(). - if (noSavedModelsMenu != null) { - noSavedModelsMenu.SetActive(false); - } - if (noLikedModelsMenu != null) { - noLikedModelsMenu.SetActive(false); - } - if (signedOutYourModelsMenu != null) { - signedOutYourModelsMenu.SetActive(false); - } - if (signedOutLikedModelsMenu != null) { - signedOutLikedModelsMenu.SetActive(false); - } - if (offlineModelsMenu != null) { - offlineModelsMenu.SetActive(false); - } - // Activate or deactivate the models menu. - if (modelsMenu != null) { - modelsMenu.SetActive(CurrentMenuSection() == PolyMenuSection.CREATION); - if (CurrentMenuSection() == PolyMenuSection.CREATION) { - // Update the pagination icons. - ChangePaginationButtons(); - - // Populate the menu with Zandria creations. - PopulateZandriaMenu(CurrentCreationType()); - } - } - } + /// + /// Changes the menu to a passed menuIndex. This is used when selecting menu options using the peltzerController + /// and not the palette touchpad. + /// + /// The new menu index that was selected. + /// Whether to also set the passed menuIndex to the first page. + public void ApplyMenuChange(int selectedMenuIndex, bool setToFirstPage = false) + { + if (setToFirstPage || selectedMenuIndex == menuIndex) + { + menuModes[selectedMenuIndex].page = 0; + } + menuIndex = selectedMenuIndex; + ChangeMenu(); + } - private void PopulateZandriaMenu(CreationType type) { - // This is a naive approach. Every time you open the menu it will just attach the - // available creations but we should keep adding creations as they load when the menu is open. This - // should be implemented with our pagination. - - // First hide any gameObjects on the palette so we can show the correct ones. - for (int i = 0; i < placeholders.Length; i++) { - ZandriaCreationHandler[] creationHandlers = - placeholders[i].GetComponentsInChildren(); - - for (int j = 0; j < creationHandlers.Length; j++) { - creationHandlers[j].isActiveOnMenu = false; - creationHandlers[j].gameObject.SetActive(false); - } - } - - int from = CurrentPage() * TILE_COUNT; - int upToNotIncluding = from + TILE_COUNT; - List previews = creationsManager.GetPreviews(type, CurrentPage() * TILE_COUNT, upToNotIncluding); - - // If there are available previews load them onto the palette. - if (previews.Count > 0) { - for (int i = 0; i < TILE_COUNT && i < previews.Count; i++) { - GameObject zandriaCreationHolder = previews[i]; - zandriaCreationHolder.GetComponent().isActiveOnMenu = true; - zandriaCreationHolder.SetActive(true); - - // Parent the zandriaCreationHolder to the placeholders on the ZandriaMenu. - zandriaCreationHolder.transform.parent = placeholders[i].transform; - - zandriaCreationHolder.transform.localPosition = Vector3.zero; - zandriaCreationHolder.transform.localRotation = Quaternion.Euler(new Vector3(90, 0, 0)); - } - } - - // If there were no valid previews, replace the modelsMenu with a menu panel displaying a prompt to the user. - // Unless its the FEATURED menu which has no prompt. We've just failed to load (or are loading) featured - // models. - if (modelsMenu != null) { - // Even though there are no previews keep the modelsMenu active if there aren't any previews available - // but the creationsManager is trying to load that type. The user has signed in, the load is just not ready. - modelsMenu.SetActive( - (type == CreationType.FEATURED && creationsManager.HasPendingOrValidLoad(CreationType.FEATURED)) || - (type == CreationType.YOUR && creationsManager.HasPendingOrValidLoad(CreationType.YOUR)) || - (type == CreationType.LIKED && creationsManager.HasPendingOrValidLoad(CreationType.LIKED))); - } - - bool modelsMenuActive = modelsMenu.activeInHierarchy; - - if (noSavedModelsMenu != null) { - // Tell the user that they have no saved models if: The creations manager has tried to load your models - // and it's invalid but the user is logged in. - noSavedModelsMenu.SetActive(!modelsMenuActive && type == CreationType.YOUR - && !creationsManager.HasValidLoad(CreationType.YOUR) - && OAuth2Identity.Instance.LoggedIn); - } - if (noLikedModelsMenu != null) { - // Tell the user that they have no liked models if: The creations manager has tried to load liked models - // and it's invalid but the user is logged in. - noLikedModelsMenu.SetActive(!modelsMenuActive && type == CreationType.LIKED - && !creationsManager.HasValidLoad(CreationType.LIKED) - && OAuth2Identity.Instance.LoggedIn); - } - if (signedOutYourModelsMenu != null) { - // Tell the user to log in if they are not logged in. - signedOutYourModelsMenu.SetActive(!modelsMenuActive && type == CreationType.YOUR - && !OAuth2Identity.Instance.LoggedIn); - } - if (signedOutLikedModelsMenu != null) { - // Tell the user to log in if the are not logged in. - signedOutLikedModelsMenu.SetActive(!modelsMenuActive && type == CreationType.LIKED - && !OAuth2Identity.Instance.LoggedIn); - } - if (offlineModelsMenu != null) { - // Tell the user to check their internet connection if we have no featured models. - offlineModelsMenu.SetActive(!modelsMenuActive && type == CreationType.FEATURED && - !creationsManager.HasPendingOrValidLoad(CreationType.FEATURED)); - } - } + public void ApplyPageChange(int indexChange) + { + // The left and right buttons will disable themselves if there are no valid actions so we don't have to worry + // about checking if the indexChange is valid. + menuModes[menuIndex].page += indexChange; + ChangeMenu(); + } - /// - /// Refreshes the PolyMenu if the menu is already open in the hierachy. - /// - public void RefreshPolyMenu() { - if (modelsMenu != null) { - if (CurrentMenuSection() == PolyMenuSection.CREATION) { - // Update the pagination icons. - ChangePaginationButtons(); + private void ChangePaginationButtons() + { + int numPages = creationsManager.GetNumberOfPages(CurrentCreationType()); - // Populate the menu with Zandria creations. - PopulateZandriaMenu(CurrentCreationType()); + bool pageLeftActive = CurrentPage() > 0; + bool pageRightActive = CurrentPage() < numPages - 1; + + pageLeftIcon.color = pageLeftActive ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; + pageRightIcon.color = pageRightActive ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; + + pageLeftScript.isActive = pageLeftActive; + pageRightScript.isActive = pageRightActive; + + // Don't use a zero index for the page display. + int currentPageForDisplay = CurrentPage() + 1; + pageIndicator.text = string.Format("{0} of {1}", currentPageForDisplay, numPages); } - } - } - /// - /// Opens the Details section of the PolyMenu by setting the UI element active in the scene and loading the - /// creation onto the palette. - /// - /// The creation to be opened. - public void OpenDetailsSection(Creation creation) { - SetActiveMenu(Menu.DETAILS_MENU); - // First close and remove the information for an already open Details panel. - // The user is able to click on a new creation by clicking under the Details panel before closing. - - if (creation != null) { - currentCreationHandler = creation.handler; - StartCoroutine(AttachPreviewToDetailsHolder(creation)); - - // Activate/Deactivate the correct buttons and UI elements for each creation type. - creationTitle.SetActive(CurrentCreationType() == CreationType.YOUR); - creationDate.SetActive(CurrentCreationType() == CreationType.FEATURED - || CurrentCreationType() == CreationType.LIKED); - creatorName.SetActive(CurrentCreationType() == CreationType.FEATURED - || CurrentCreationType() == CreationType.LIKED); - - // Activate or deactivate the Open/Import buttons if the model is loaded. - ActivateOpenImportButtons(creation.entry.loadStatus == ZandriaCreationsManager.LoadStatus.SUCCESSFUL); - deleteButton.SetActive(CurrentCreationType() == CreationType.YOUR); - yourModelsMenuSpacer.SetActive(CurrentCreationType() == CreationType.YOUR); - likedOrFeaturedModelsMenuSpacer.SetActive( - CurrentCreationType() == CreationType.FEATURED || CurrentCreationType() == CreationType.LIKED); - - if (CurrentCreationType() == CreationType.YOUR) { - // Reset the creation title and creator name UI elements. - creationTitle.SetActive(false); - creationTitle.GetComponent().text = ""; - creatorName.SetActive(false); - creatorName.GetComponent().text = ""; - } else if (CurrentCreationType() == CreationType.FEATURED || CurrentCreationType() == CreationType.LIKED) { - // Reset the creation date UI element. - creationDate.SetActive(false); - creationDate.GetComponent().text = ""; - - // Activate and populate the creation title and creator name UI elements. - creationTitle.SetActive(true); - creationTitle.GetComponent().text = currentCreationHandler.creationTitle; - creatorName.SetActive(true); - creatorName.GetComponent().text = - new StringBuilder().Append(BASE_CREATOR).Append(currentCreationHandler.creatorName).ToString(); - } - } - } + private void ChangeMenu() + { + // Start by ensuring Poly Menu is active, to cover the case that 'details' was open and needs to be closed. + SetActiveMenu(Menu.POLY_MENU); - /// - /// Activates or deactivates the Open and Import detail buttons by changing the icon, text and stopping the button - /// action by setting the script inactive. - /// - /// Whether the buttons should be active. - private void ActivateOpenImportButtons(bool active) { - openButtonIcon.color = active ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; - openButtonText.color = active ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; - openButtonScript.isActive = active; - - importButtonIcon.color = active ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; - importButtonText.color = active ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; - importButtonScript.isActive = active; - } + // Activate the correct title and deactivate all others. + if (optionsTitle != null) + { + optionsTitle.SetActive(CurrentMenuSection() == PolyMenuSection.OPTION); + } + if (yourModelsTitle != null) + { + yourModelsTitle.SetActive(CurrentCreationType() == CreationType.YOUR); + } + if (likedModelsTitle != null) + { + likedModelsTitle.SetActive(CurrentCreationType() == CreationType.LIKED); + } + if (featuredModelsTitle != null) + { + featuredModelsTitle.SetActive(CurrentCreationType() == CreationType.FEATURED); + } - /// - /// Takes a creation and loads a scaled up version of its preview onto the details menu. If the preview isn't - /// loaded yet it will wait for it to be loaded. - /// - /// The creation to attach to the menu. - public IEnumerator AttachPreviewToDetailsHolder(Creation creation) { - // Make sure the details thumbnail is active and then set the thumbnail to the creation's thumbnail. - detailsThumbnail.SetActive(true); - detailsThumbnail.GetComponent().sprite = creation.thumbnailSprite; - - // Wait until the creation is loaded to do anything else. During this time the thumbnail is displayed and the - // Open/Import buttons are inactive. - while (creation.entry.loadStatus != ZandriaCreationsManager.LoadStatus.SUCCESSFUL) { - yield return null; - } - - // The creation has loaded, scale the meshes for the details menu. - List detailSizedMeshes; - - // Check if detailSizedMeshes already exist. We don't want to replicate them again from the originals if the - // model has been open in the scene since they will reference the same MMesh instance. - if (creation.handler.detailSizedMeshes.Count > 0) { - detailSizedMeshes = creation.handler.detailSizedMeshes; - } else { - detailSizedMeshes = Scaler.ScaleMeshes(creation.handler.originalMeshes, DETAIL_TILE_SIZE); - creation.handler.detailSizedMeshes = detailSizedMeshes; - } - - // Get a preview from the MMeshes on a background thread. When it's done it will call back with the preview - // and attach it to the details menu. - MeshHelper.GameObjectFromMMeshesForMenu(new WorldSpace(PeltzerMain.DEFAULT_BOUNDS), detailSizedMeshes, - delegate (GameObject meshPreview) { - // We have successfully loaded the creation as a preview so we attach it to the menu. - if (meshPreview != null) { - // Zero the transform so we're only being transformed by the parent. - meshPreview.GetComponent().ResetTransform(); - // Parent the mesh preview to the details menu. - meshPreview.transform.parent = detailsPreviewHolder.transform; - meshPreview.transform.localPosition = Vector3.zero; - meshPreview.transform.localRotation = Quaternion.Euler( - new Vector3(0, creation.handler.recommendedRotation, 0)); - - detailsPreviewHolder.GetComponent().meshes = detailSizedMeshes; - - // Deactivate the thumbnail now that the meshes are displaying and activate the Open/Import buttons. - detailsThumbnail.SetActive(false); - ActivateOpenImportButtons(/*active*/ true); - } - }); - } + if (optionsIcon != null) + { + optionsIcon.color = CurrentMenuSection() == + PolyMenuSection.OPTION ? SELECTED_AVATAR_COLOR : UNSELECTED_AVATAR_COLOR; + } + if (yourModelsIcon != null) + { + yourModelsIcon.color = CurrentCreationType() == + CreationType.YOUR ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; + } + if (likedModelsIcon != null) + { + likedModelsIcon.color = CurrentCreationType() == + CreationType.LIKED ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; + } + if (featuredModelsIcon != null) + { + featuredModelsIcon.color = CurrentCreationType() == + CreationType.FEATURED ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; + } + if (environmentIcon != null) + { + environmentIcon.color = CurrentMenuSection() == PolyMenuSection.ENVIRONMENT ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; + } + if (labsIcon != null) + { + labsIcon.color = CurrentMenuSection() == PolyMenuSection.LABS ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; + } - private PolyMenuSection CurrentMenuSection() { - return menuModes[menuIndex].menuSection; - } + // Activate or deactive the necessary menus. + if (optionsMenu != null) + { + optionsMenu.SetActive(CurrentMenuSection() == PolyMenuSection.OPTION); + } - private CreationType CurrentCreationType() { - return menuModes[menuIndex].creationType; - } + if (labsMenu != null) + { + labsMenu.SetActive(CurrentMenuSection() == PolyMenuSection.LABS); + } + if (environmentMenu != null) + { + environmentMenu.SetActive(CurrentMenuSection() == PolyMenuSection.ENVIRONMENT); + } + // Deactivate all the user prompt menus. If they need to be activated they will be in PopulateZandriaMenu(). + if (noSavedModelsMenu != null) + { + noSavedModelsMenu.SetActive(false); + } + if (noLikedModelsMenu != null) + { + noLikedModelsMenu.SetActive(false); + } + if (signedOutYourModelsMenu != null) + { + signedOutYourModelsMenu.SetActive(false); + } + if (signedOutLikedModelsMenu != null) + { + signedOutLikedModelsMenu.SetActive(false); + } + if (offlineModelsMenu != null) + { + offlineModelsMenu.SetActive(false); + } + // Activate or deactivate the models menu. + if (modelsMenu != null) + { + modelsMenu.SetActive(CurrentMenuSection() == PolyMenuSection.CREATION); + if (CurrentMenuSection() == PolyMenuSection.CREATION) + { + // Update the pagination icons. + ChangePaginationButtons(); + + // Populate the menu with Zandria creations. + PopulateZandriaMenu(CurrentCreationType()); + } + } + } - private int CurrentPage() { - return menuModes[menuIndex].page; - } + private void PopulateZandriaMenu(CreationType type) + { + // This is a naive approach. Every time you open the menu it will just attach the + // available creations but we should keep adding creations as they load when the menu is open. This + // should be implemented with our pagination. + + // First hide any gameObjects on the palette so we can show the correct ones. + for (int i = 0; i < placeholders.Length; i++) + { + ZandriaCreationHandler[] creationHandlers = + placeholders[i].GetComponentsInChildren(); + + for (int j = 0; j < creationHandlers.Length; j++) + { + creationHandlers[j].isActiveOnMenu = false; + creationHandlers[j].gameObject.SetActive(false); + } + } - /// - /// Called after the user has successfully signed in and we need to update the PolyMenu. - /// - /// The user's avatar to display on the PolyMenu. - /// The user's name to display on the PolyMenu. - public void SignIn(Sprite avatarIcon, string displayName) { - // Set the signIn button false and reset the prompt to read "Sign In" instead of "Take headset off..." - signInButton.SetActive(false); - signInText.text = defaultSignInMessage; - signOutButton.SetActive(true); - - // Change the UI elements to display the user's icon and name. - optionsIcon.sprite = avatarIcon != null ? avatarIcon : signedOutIcon; - optionsIcon.transform.localScale = avatarIcon != null ? USER_AVATAR_SCALE : DEFAULT_AVATAR_SCALE; - this.displayName.text = displayName != null ? displayName : defaultDisplayName; - - RefreshPolyMenu(); - } + int from = CurrentPage() * TILE_COUNT; + int upToNotIncluding = from + TILE_COUNT; + List previews = creationsManager.GetPreviews(type, CurrentPage() * TILE_COUNT, upToNotIncluding); + + // If there are available previews load them onto the palette. + if (previews.Count > 0) + { + for (int i = 0; i < TILE_COUNT && i < previews.Count; i++) + { + GameObject zandriaCreationHolder = previews[i]; + zandriaCreationHolder.GetComponent().isActiveOnMenu = true; + zandriaCreationHolder.SetActive(true); + + // Parent the zandriaCreationHolder to the placeholders on the ZandriaMenu. + zandriaCreationHolder.transform.parent = placeholders[i].transform; + + zandriaCreationHolder.transform.localPosition = Vector3.zero; + zandriaCreationHolder.transform.localRotation = Quaternion.Euler(new Vector3(90, 0, 0)); + } + } - /// - /// Called after the user has signed out and we need to update the PolyMenu. - /// - public void SignOut() { - signInButton.SetActive(true); - signOutButton.SetActive(false); + // If there were no valid previews, replace the modelsMenu with a menu panel displaying a prompt to the user. + // Unless its the FEATURED menu which has no prompt. We've just failed to load (or are loading) featured + // models. + if (modelsMenu != null) + { + // Even though there are no previews keep the modelsMenu active if there aren't any previews available + // but the creationsManager is trying to load that type. The user has signed in, the load is just not ready. + modelsMenu.SetActive( + (type == CreationType.FEATURED && creationsManager.HasPendingOrValidLoad(CreationType.FEATURED)) || + (type == CreationType.YOUR && creationsManager.HasPendingOrValidLoad(CreationType.YOUR)) || + (type == CreationType.LIKED && creationsManager.HasPendingOrValidLoad(CreationType.LIKED))); + } - optionsIcon.sprite = signedOutIcon; - optionsIcon.transform.localScale = DEFAULT_AVATAR_SCALE; - displayName.text = defaultDisplayName; + bool modelsMenuActive = modelsMenu.activeInHierarchy; - signInText.text = defaultSignInMessage; + if (noSavedModelsMenu != null) + { + // Tell the user that they have no saved models if: The creations manager has tried to load your models + // and it's invalid but the user is logged in. + noSavedModelsMenu.SetActive(!modelsMenuActive && type == CreationType.YOUR + && !creationsManager.HasValidLoad(CreationType.YOUR) + && OAuth2Identity.Instance.LoggedIn); + } + if (noLikedModelsMenu != null) + { + // Tell the user that they have no liked models if: The creations manager has tried to load liked models + // and it's invalid but the user is logged in. + noLikedModelsMenu.SetActive(!modelsMenuActive && type == CreationType.LIKED + && !creationsManager.HasValidLoad(CreationType.LIKED) + && OAuth2Identity.Instance.LoggedIn); + } + if (signedOutYourModelsMenu != null) + { + // Tell the user to log in if they are not logged in. + signedOutYourModelsMenu.SetActive(!modelsMenuActive && type == CreationType.YOUR + && !OAuth2Identity.Instance.LoggedIn); + } + if (signedOutLikedModelsMenu != null) + { + // Tell the user to log in if the are not logged in. + signedOutLikedModelsMenu.SetActive(!modelsMenuActive && type == CreationType.LIKED + && !OAuth2Identity.Instance.LoggedIn); + } + if (offlineModelsMenu != null) + { + // Tell the user to check their internet connection if we have no featured models. + offlineModelsMenu.SetActive(!modelsMenuActive && type == CreationType.FEATURED && + !creationsManager.HasPendingOrValidLoad(CreationType.FEATURED)); + } + } - RefreshPolyMenu(); - } + /// + /// Refreshes the PolyMenu if the menu is already open in the hierachy. + /// + public void RefreshPolyMenu() + { + if (modelsMenu != null) + { + if (CurrentMenuSection() == PolyMenuSection.CREATION) + { + // Update the pagination icons. + ChangePaginationButtons(); + + // Populate the menu with Zandria creations. + PopulateZandriaMenu(CurrentCreationType()); + } + } + } - /// - /// Called when the user has started the authentication process and needs to take off their headset to sign in - /// using their web browser. - /// - public void PromptUserToSignIn() { - signInText.text = TAKE_HEADSET_OFF_FOR_SIGN_IN_PROMPT; - } + /// + /// Opens the Details section of the PolyMenu by setting the UI element active in the scene and loading the + /// creation onto the palette. + /// + /// The creation to be opened. + public void OpenDetailsSection(Creation creation) + { + SetActiveMenu(Menu.DETAILS_MENU); + // First close and remove the information for an already open Details panel. + // The user is able to click on a new creation by clicking under the Details panel before closing. + + if (creation != null) + { + currentCreationHandler = creation.handler; + StartCoroutine(AttachPreviewToDetailsHolder(creation)); + + // Activate/Deactivate the correct buttons and UI elements for each creation type. + creationTitle.SetActive(CurrentCreationType() == CreationType.YOUR); + creationDate.SetActive(CurrentCreationType() == CreationType.FEATURED + || CurrentCreationType() == CreationType.LIKED); + creatorName.SetActive(CurrentCreationType() == CreationType.FEATURED + || CurrentCreationType() == CreationType.LIKED); + + // Activate or deactivate the Open/Import buttons if the model is loaded. + ActivateOpenImportButtons(creation.entry.loadStatus == ZandriaCreationsManager.LoadStatus.SUCCESSFUL); + deleteButton.SetActive(CurrentCreationType() == CreationType.YOUR); + yourModelsMenuSpacer.SetActive(CurrentCreationType() == CreationType.YOUR); + likedOrFeaturedModelsMenuSpacer.SetActive( + CurrentCreationType() == CreationType.FEATURED || CurrentCreationType() == CreationType.LIKED); + + if (CurrentCreationType() == CreationType.YOUR) + { + // Reset the creation title and creator name UI elements. + creationTitle.SetActive(false); + creationTitle.GetComponent().text = ""; + creatorName.SetActive(false); + creatorName.GetComponent().text = ""; + } + else if (CurrentCreationType() == CreationType.FEATURED || CurrentCreationType() == CreationType.LIKED) + { + // Reset the creation date UI element. + creationDate.SetActive(false); + creationDate.GetComponent().text = ""; + + // Activate and populate the creation title and creator name UI elements. + creationTitle.SetActive(true); + creationTitle.GetComponent().text = currentCreationHandler.creationTitle; + creatorName.SetActive(true); + creatorName.GetComponent().text = + new StringBuilder().Append(BASE_CREATOR).Append(currentCreationHandler.creatorName).ToString(); + } + } + } - /// - /// Selects a Zandria creation represented by a list of MMeshes and adds them to the model then passes them to the - /// move tool to be moved and placed in the scene. - /// - /// The MMeshes to be selected and then moved. - public void SelectCreation(List meshes, string selectedAssetId) { - if (meshes == null) { - return; - } - - Model model = PeltzerMain.Instance.GetModel(); - - // We ignore the 'bool' output of the below: it it fails, we'll continue with the mesh in its current scale. - Scaler.TryScalingMeshes(meshes, 1f / PeltzerMain.Instance.worldSpace.scale); - - // We give them new IDs at this point so they won't collide with anything already in the scene or - // (much more likely) with a previous import of this same creation. We need to store a local list of usedIds - // to avoid some rare, but potential, cases where the same ID is generated twice during this import. - List usedIds = new List(meshes.Count); - - // We group every mesh together in an import, overwriting previous groupings, under the assumption that users - // are much more likely to want to move and place the entire import than they are to subtly edit it and thereby - // depend on its original groupings. - int groupId = model.GenerateGroupId(); - for (int i = 0; i < meshes.Count; i++) { - MMesh mesh = meshes[i]; - int newId = model.GenerateMeshId(usedIds); - usedIds.Add(newId); - mesh.ChangeId(newId); - mesh.ChangeGroupId(groupId); - if (CurrentCreationType() != CreationType.YOUR) { - // Mark this mesh as being remixed from the selected asset. - mesh.ChangeRemixId(selectedAssetId); - } - mesh.offset = PeltzerMain.Instance.peltzerController.LastPositionModel + mesh.offset; - } - - // Switch to the 'move' tool in 'create' mode and start the 'move-create' operation. - PeltzerMain.Instance.peltzerController - .ChangeMode(ControllerMode.move, ObjectFinder.ObjectById("ID_ToolGrab")); - PeltzerMain.Instance.GetMover().currentMoveType = tools.Mover.MoveType.CREATE; - PeltzerMain.Instance.GetMover().StartMove(meshes); - PeltzerMain.Instance.peltzerController.ResetUnhoveredItem(); - PeltzerMain.Instance.peltzerController.ResetMenu(); + /// + /// Activates or deactivates the Open and Import detail buttons by changing the icon, text and stopping the button + /// action by setting the script inactive. + /// + /// Whether the buttons should be active. + private void ActivateOpenImportButtons(bool active) + { + openButtonIcon.color = active ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; + openButtonText.color = active ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; + openButtonScript.isActive = active; + + importButtonIcon.color = active ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; + importButtonText.color = active ? SELECTED_ICON_COLOR : UNSELECTED_ICON_COLOR; + importButtonScript.isActive = active; + } - } + /// + /// Takes a creation and loads a scaled up version of its preview onto the details menu. If the preview isn't + /// loaded yet it will wait for it to be loaded. + /// + /// The creation to attach to the menu. + public IEnumerator AttachPreviewToDetailsHolder(Creation creation) + { + // Make sure the details thumbnail is active and then set the thumbnail to the creation's thumbnail. + detailsThumbnail.SetActive(true); + detailsThumbnail.GetComponent().sprite = creation.thumbnailSprite; + + // Wait until the creation is loaded to do anything else. During this time the thumbnail is displayed and the + // Open/Import buttons are inactive. + while (creation.entry.loadStatus != ZandriaCreationsManager.LoadStatus.SUCCESSFUL) + { + yield return null; + } - public bool ToolMenuIsActive() { - return activeMenu == Menu.TOOLS_MENU; - } + // The creation has loaded, scale the meshes for the details menu. + List detailSizedMeshes; - public bool PolyMenuIsActive() { - return activeMenu == Menu.POLY_MENU; - } + // Check if detailSizedMeshes already exist. We don't want to replicate them again from the originals if the + // model has been open in the scene since they will reference the same MMesh instance. + if (creation.handler.detailSizedMeshes.Count > 0) + { + detailSizedMeshes = creation.handler.detailSizedMeshes; + } + else + { + detailSizedMeshes = Scaler.ScaleMeshes(creation.handler.originalMeshes, DETAIL_TILE_SIZE); + creation.handler.detailSizedMeshes = detailSizedMeshes; + } + + // Get a preview from the MMeshes on a background thread. When it's done it will call back with the preview + // and attach it to the details menu. + MeshHelper.GameObjectFromMMeshesForMenu(new WorldSpace(PeltzerMain.DEFAULT_BOUNDS), detailSizedMeshes, + delegate (GameObject meshPreview) + { + // We have successfully loaded the creation as a preview so we attach it to the menu. + if (meshPreview != null) + { + // Zero the transform so we're only being transformed by the parent. + meshPreview.GetComponent().ResetTransform(); + // Parent the mesh preview to the details menu. + meshPreview.transform.parent = detailsPreviewHolder.transform; + meshPreview.transform.localPosition = Vector3.zero; + meshPreview.transform.localRotation = Quaternion.Euler( + new Vector3(0, creation.handler.recommendedRotation, 0)); + + detailsPreviewHolder.GetComponent().meshes = detailSizedMeshes; + + // Deactivate the thumbnail now that the meshes are displaying and activate the Open/Import buttons. + detailsThumbnail.SetActive(false); + ActivateOpenImportButtons(/*active*/ true); + } + }); + } + + private PolyMenuSection CurrentMenuSection() + { + return menuModes[menuIndex].menuSection; + } + + private CreationType CurrentCreationType() + { + return menuModes[menuIndex].creationType; + } + + private int CurrentPage() + { + return menuModes[menuIndex].page; + } + + /// + /// Called after the user has successfully signed in and we need to update the PolyMenu. + /// + /// The user's avatar to display on the PolyMenu. + /// The user's name to display on the PolyMenu. + public void SignIn(Sprite avatarIcon, string displayName) + { + // Set the signIn button false and reset the prompt to read "Sign In" instead of "Take headset off..." + signInButton.SetActive(false); + signInText.text = defaultSignInMessage; + signOutButton.SetActive(true); + + // Change the UI elements to display the user's icon and name. + optionsIcon.sprite = avatarIcon != null ? avatarIcon : signedOutIcon; + optionsIcon.transform.localScale = avatarIcon != null ? USER_AVATAR_SCALE : DEFAULT_AVATAR_SCALE; + this.displayName.text = displayName != null ? displayName : defaultDisplayName; + + RefreshPolyMenu(); + } + + /// + /// Called after the user has signed out and we need to update the PolyMenu. + /// + public void SignOut() + { + signInButton.SetActive(true); + signOutButton.SetActive(false); + + optionsIcon.sprite = signedOutIcon; + optionsIcon.transform.localScale = DEFAULT_AVATAR_SCALE; + displayName.text = defaultDisplayName; + + signInText.text = defaultSignInMessage; - public bool DetailsMenuIsActive() { - return activeMenu == Menu.DETAILS_MENU; + RefreshPolyMenu(); + } + + /// + /// Called when the user has started the authentication process and needs to take off their headset to sign in + /// using their web browser. + /// + public void PromptUserToSignIn() + { + signInText.text = TAKE_HEADSET_OFF_FOR_SIGN_IN_PROMPT; + } + + /// + /// Selects a Zandria creation represented by a list of MMeshes and adds them to the model then passes them to the + /// move tool to be moved and placed in the scene. + /// + /// The MMeshes to be selected and then moved. + public void SelectCreation(List meshes, string selectedAssetId) + { + if (meshes == null) + { + return; + } + + Model model = PeltzerMain.Instance.GetModel(); + + // We ignore the 'bool' output of the below: it it fails, we'll continue with the mesh in its current scale. + Scaler.TryScalingMeshes(meshes, 1f / PeltzerMain.Instance.worldSpace.scale); + + // We give them new IDs at this point so they won't collide with anything already in the scene or + // (much more likely) with a previous import of this same creation. We need to store a local list of usedIds + // to avoid some rare, but potential, cases where the same ID is generated twice during this import. + List usedIds = new List(meshes.Count); + + // We group every mesh together in an import, overwriting previous groupings, under the assumption that users + // are much more likely to want to move and place the entire import than they are to subtly edit it and thereby + // depend on its original groupings. + int groupId = model.GenerateGroupId(); + for (int i = 0; i < meshes.Count; i++) + { + MMesh mesh = meshes[i]; + int newId = model.GenerateMeshId(usedIds); + usedIds.Add(newId); + mesh.ChangeId(newId); + mesh.ChangeGroupId(groupId); + if (CurrentCreationType() != CreationType.YOUR) + { + // Mark this mesh as being remixed from the selected asset. + mesh.ChangeRemixId(selectedAssetId); + } + mesh.offset = PeltzerMain.Instance.peltzerController.LastPositionModel + mesh.offset; + } + + // Switch to the 'move' tool in 'create' mode and start the 'move-create' operation. + PeltzerMain.Instance.peltzerController + .ChangeMode(ControllerMode.move, ObjectFinder.ObjectById("ID_ToolGrab")); + PeltzerMain.Instance.GetMover().currentMoveType = tools.Mover.MoveType.CREATE; + PeltzerMain.Instance.GetMover().StartMove(meshes); + PeltzerMain.Instance.peltzerController.ResetUnhoveredItem(); + PeltzerMain.Instance.peltzerController.ResetMenu(); + + } + + public bool ToolMenuIsActive() + { + return activeMenu == Menu.TOOLS_MENU; + } + + public bool PolyMenuIsActive() + { + return activeMenu == Menu.POLY_MENU; + } + + public bool DetailsMenuIsActive() + { + return activeMenu == Menu.DETAILS_MENU; + } } - } } diff --git a/Assets/Scripts/menu/ProgressIndicator.cs b/Assets/Scripts/menu/ProgressIndicator.cs index 30ba301e..56a797c2 100644 --- a/Assets/Scripts/menu/ProgressIndicator.cs +++ b/Assets/Scripts/menu/ProgressIndicator.cs @@ -18,293 +18,322 @@ using UnityEngine; using UnityEngine.UI; -namespace com.google.apps.peltzer.client.menu { - /// - /// Widget that gives visual feedback about the progress of a long operation. - /// We can show two states: working and done, and animate appropriately between them. - /// - public class ProgressIndicator : MonoBehaviour { +namespace com.google.apps.peltzer.client.menu +{ /// - /// Minimum apparent duration of work. To avoid visual jank, if the operation finishes sooner than this, - /// we continue pretending to be working (showing the progress indicator) until this minimum time elapses. + /// Widget that gives visual feedback about the progress of a long operation. + /// We can show two states: working and done, and animate appropriately between them. /// - private const float MIN_APPARENT_WORK_DURATION_SECONDS = 2.0f; + public class ProgressIndicator : MonoBehaviour + { + /// + /// Minimum apparent duration of work. To avoid visual jank, if the operation finishes sooner than this, + /// we continue pretending to be working (showing the progress indicator) until this minimum time elapses. + /// + private const float MIN_APPARENT_WORK_DURATION_SECONDS = 2.0f; - /// - /// How long we linger in the "finished" state, showing the "done" animation. After this time elapses, - /// we hide the progress indicator. - /// - private const float FINISHED_STATE_DURATION_SECONDS = 1.0f; + /// + /// How long we linger in the "finished" state, showing the "done" animation. After this time elapses, + /// we hide the progress indicator. + /// + private const float FINISHED_STATE_DURATION_SECONDS = 1.0f; - /// - /// Duration of the transition animations between states. - /// - private const float STATE_TRANSITION_ANIM_DURATION_SECONDS = 0.3f; + /// + /// Duration of the transition animations between states. + /// + private const float STATE_TRANSITION_ANIM_DURATION_SECONDS = 0.3f; - /// - /// Starting scale for the state transition animation. We animate objects starting from this scale and going - /// to their natural scale (1.0f). - /// - private const float STATE_TRANSITION_ANIM_INIT_SCALE = 0.02f; - - /// - /// Percentage of original scale to scale up to during animation. - /// - private const float STATE_TRANSITION_ANIM_SCALE_INCREASE = .5f; + /// + /// Starting scale for the state transition animation. We animate objects starting from this scale and going + /// to their natural scale (1.0f). + /// + private const float STATE_TRANSITION_ANIM_INIT_SCALE = 0.02f; - /// - /// The time in seconds we spend scaling down the progress indicator on completion. - /// - private const float SCALE_OUT_ANIMATION_DURATION = 0.2f; + /// + /// Percentage of original scale to scale up to during animation. + /// + private const float STATE_TRANSITION_ANIM_SCALE_INCREASE = .5f; - /// - /// Position of the progress indicator when using the Oculus. - /// - private static readonly Vector3 ROOT_POSITION_OCULUS = new Vector3(0f, 0f, 0.0808f); + /// + /// The time in seconds we spend scaling down the progress indicator on completion. + /// + private const float SCALE_OUT_ANIMATION_DURATION = 0.2f; + /// + /// Position of the progress indicator when using the Oculus. + /// + private static readonly Vector3 ROOT_POSITION_OCULUS = new Vector3(0f, 0f, 0.0808f); - private Color blueBlock = new Color(5f/255f, 144f/255f, 179f/255f, 1.0f); - private Color yellowBlock = new Color(250f/255f, 175f/255f, 9f/255f, 1.0f); - private Color redBlock = new Color(255f/255f, 24f/255f, 4f/255f, 1.0f); - private Color greenBlock = new Color(39f/255f, 192f/255f, 72f/255f, 1.0f); - - /// - /// Represents which state we are in. - /// - private enum State { - // Not showing progress indicator. This is the initial state. - NOT_SHOWING, - // Showing the "working" indeterminate spinner. - WORKING, - // In the PENDING_SUCCESS and PENDING_ERROR states, we are ready to go to either the success or error - // state, but waiting a bit so it's not too abrupt (this happens if the operation finishes too quickly, - // in which case we want to keep the indicator around on the screen for a bit). - PENDING_SUCCESS, - PENDING_ERROR, - // Showing the success state. Automatically transitions to NOT_SHOWING after the appropriate time passes. - SUCCESS, - // Showing the error state. Automatically transitions to NOT_SHOWING after the appropriate time passes. - ERROR, - // In the pending not showing state we know we don't want to show the progress indicator anymore and are - // going to fade it out. - PENDING_NOT_SHOWING - } - private State state = State.NOT_SHOWING; + private Color blueBlock = new Color(5f / 255f, 144f / 255f, 179f / 255f, 1.0f); + private Color yellowBlock = new Color(250f / 255f, 175f / 255f, 9f / 255f, 1.0f); + private Color redBlock = new Color(255f / 255f, 24f / 255f, 4f / 255f, 1.0f); + private Color greenBlock = new Color(39f / 255f, 192f / 255f, 72f / 255f, 1.0f); - /// - /// Root object of the progress indicator hierarchy. - /// - private GameObject rootObject; + /// + /// Represents which state we are in. + /// + private enum State + { + // Not showing progress indicator. This is the initial state. + NOT_SHOWING, + // Showing the "working" indeterminate spinner. + WORKING, + // In the PENDING_SUCCESS and PENDING_ERROR states, we are ready to go to either the success or error + // state, but waiting a bit so it's not too abrupt (this happens if the operation finishes too quickly, + // in which case we want to keep the indicator around on the screen for a bit). + PENDING_SUCCESS, + PENDING_ERROR, + // Showing the success state. Automatically transitions to NOT_SHOWING after the appropriate time passes. + SUCCESS, + // Showing the error state. Automatically transitions to NOT_SHOWING after the appropriate time passes. + ERROR, + // In the pending not showing state we know we don't want to show the progress indicator anymore and are + // going to fade it out. + PENDING_NOT_SHOWING + } - /// - /// Visual indicator of the "working" state, which we show when an operation is in progress. - /// - private GameObject workingIndicator; + private State state = State.NOT_SHOWING; - private GameObject redCube; - private GameObject yellowCube; - private GameObject blueCube; - + /// + /// Root object of the progress indicator hierarchy. + /// + private GameObject rootObject; - /// - /// Text mesh containing the text we are currently displaying. - /// - private TextMesh progressText; + /// + /// Visual indicator of the "working" state, which we show when an operation is in progress. + /// + private GameObject workingIndicator; - /// - /// Time at which the operation started (transitioned to WORKING state). - /// - private float operationStartTime; + private GameObject redCube; + private GameObject yellowCube; + private GameObject blueCube; - /// - /// Time at which the operation finished (transitioned to FINISHED state). - /// - private float operationFinishTime; - /// - /// The text to update the text mesh with when we transition to FINISHED. - /// Only used in the PENDING_* states. - /// - private string pendingText; + /// + /// Text mesh containing the text we are currently displaying. + /// + private TextMesh progressText; - /// - /// The scale of the root object of the progress indicator hierarchy before any scaling animations - /// are applied to it. - /// - private Vector3 rootObjectScaleAtStart; + /// + /// Time at which the operation started (transitioned to WORKING state). + /// + private float operationStartTime; - /// - /// The previous state before our current state. - /// - private State lastState; - - private void Start() { - rootObject = ObjectFinder.ObjectById("ID_ProgressIndicatorPanel"); - workingIndicator = ObjectFinder.ObjectById("ID_BlocksProgressLooper"); - redCube = ObjectFinder.ObjectById("ID_LooperCubeRed"); - yellowCube = ObjectFinder.ObjectById("ID_LooperCubeYellow"); - blueCube = ObjectFinder.ObjectById("ID_LooperCubeBlue"); - - progressText = ObjectFinder.ComponentById("ID_ProgressText"); - rootObject.SetActive(true); - InstanceMats(); - rootObject.SetActive(false); - rootObjectScaleAtStart = rootObject.transform.localScale; - ChangeState(State.NOT_SHOWING); - - if (Config.Instance.sdkMode == SdkMode.Oculus) { - rootObject.transform.localPosition = ROOT_POSITION_OCULUS; - } - } + /// + /// Time at which the operation finished (transitioned to FINISHED state). + /// + private float operationFinishTime; - /// - /// Starts showing the indicator with the given message. - /// Caller should call FinishOperation when the operation is done. - /// - /// - public void StartOperation(string message) { - progressText.text = message; - operationStartTime = Time.time; - // Force hide the menu hint. - PeltzerMain.Instance.menuHint.ChangeState(MenuHint.State.INACTIVE); - ChangeState(State.WORKING); - } + /// + /// The text to update the text mesh with when we transition to FINISHED. + /// Only used in the PENDING_* states. + /// + private string pendingText; - /// - /// Transitions the indicator into the "done" state, showing the given new message. - /// The indicator will automatically disappear after a few seconds. - /// - /// Whether the operation finished successfully. - /// The message to show. - public void FinishOperation(bool success, string message) { - if (Time.time - operationStartTime < MIN_APPARENT_WORK_DURATION_SECONDS) { - // It's too soon after we started the operation, so can't finish yet because it would look janky - // (the progress message would just pop for a brief time). So we will keep on pretending that we're - // still working for a bit, just for visual polish purposes. - pendingText = message; - ChangeState(success ? State.PENDING_SUCCESS : State.PENDING_ERROR); - } else { - // We've been showing the progress indicator for long enough that we can just finish immediately - // if it is an error. However we still need to go into a "pending" success state to make sure the - // save preview is ready. - progressText.text = message; - ChangeState(success ? State.PENDING_SUCCESS : State.ERROR); - } - } + /// + /// The scale of the root object of the progress indicator hierarchy before any scaling animations + /// are applied to it. + /// + private Vector3 rootObjectScaleAtStart; + + /// + /// The previous state before our current state. + /// + private State lastState; + + private void Start() + { + rootObject = ObjectFinder.ObjectById("ID_ProgressIndicatorPanel"); + workingIndicator = ObjectFinder.ObjectById("ID_BlocksProgressLooper"); + redCube = ObjectFinder.ObjectById("ID_LooperCubeRed"); + yellowCube = ObjectFinder.ObjectById("ID_LooperCubeYellow"); + blueCube = ObjectFinder.ObjectById("ID_LooperCubeBlue"); + + progressText = ObjectFinder.ComponentById("ID_ProgressText"); + rootObject.SetActive(true); + InstanceMats(); + rootObject.SetActive(false); + rootObjectScaleAtStart = rootObject.transform.localScale; + ChangeState(State.NOT_SHOWING); - private void Update() { - // Auto-advance the states at the right times. - float timeSinceStart = Time.time - operationStartTime; - float timeSinceFinish = Time.time - operationFinishTime; - if (state == State.PENDING_SUCCESS - && timeSinceStart > MIN_APPARENT_WORK_DURATION_SECONDS - && PeltzerMain.Instance.savePreview.state == SavePreview.State.WAITING) { - // PENDING_SUCCESS -> SUCCESS. - progressText.text = pendingText; - ChangeState(State.SUCCESS); - } else if (state == State.PENDING_ERROR && timeSinceStart > MIN_APPARENT_WORK_DURATION_SECONDS) { - // PENDING_ERROR -> ERROR. - progressText.text = pendingText; - ChangeState(State.ERROR); - } else if (state == State.ERROR && timeSinceFinish > FINISHED_STATE_DURATION_SECONDS) { - // (SUCCESS | ERROR) -> NOT_SHOWING - ChangeState(State.PENDING_NOT_SHOWING); - } else if (state == State.PENDING_NOT_SHOWING ) { - float pctDone = (Time.time - operationStartTime) / SCALE_OUT_ANIMATION_DURATION; - - if (pctDone > 1) { - // If the last state was successful we want to show the saved preview after the progress - // indicator scales down. - if (lastState == State.SUCCESS) { - PeltzerMain.Instance.savePreview.ChangeState(SavePreview.State.SCALE_ANIMATING); - } - ChangeState(State.NOT_SHOWING); - } else { - rootObject.transform.localScale = Vector3.Lerp(rootObjectScaleAtStart, Vector3.zero, pctDone); + if (Config.Instance.sdkMode == SdkMode.Oculus) + { + rootObject.transform.localPosition = ROOT_POSITION_OCULUS; + } } - } - } - private void InstanceMats() { - MeshRenderer redRenderer = redCube.GetComponent(); - redRenderer.material = new Material(redRenderer.material); - MeshRenderer yellowRenderer = yellowCube.GetComponent(); - yellowRenderer.material = new Material(yellowRenderer.material); - MeshRenderer blueRenderer = blueCube.GetComponent(); - blueRenderer.material = new Material(blueRenderer.material); - } - - private void SetAllColors(Color color) { - MeshRenderer redRenderer = redCube.GetComponent(); - MeshRenderer yellowRenderer = yellowCube.GetComponent(); - MeshRenderer blueRenderer = blueCube.GetComponent(); - - redRenderer.material.color = color; - yellowRenderer.material.color = color; - blueRenderer.material.color = color; - } - - private void ResetAnimColors() { - MeshRenderer redRenderer = redCube.GetComponent(); - MeshRenderer yellowRenderer = yellowCube.GetComponent(); - MeshRenderer blueRenderer = blueCube.GetComponent(); - - redRenderer.material.color = redBlock; - yellowRenderer.material.color = yellowBlock; - blueRenderer.material.color = blueBlock; - } - - /// - /// Changes state and does the necessary maintenance tasks to enter the new state. - /// - /// The new state to enter. - private void ChangeState(State newState) { - lastState = state; - AudioLibrary audioLibrary = PeltzerMain.Instance.audioLibrary; - - state = newState; - rootObject.SetActive(newState != State.NOT_SHOWING); - - switch (state) { - case State.PENDING_NOT_SHOWING: - operationStartTime = Time.time; - break; - case State.NOT_SHOWING: - rootObject.transform.localScale = rootObjectScaleAtStart; - break; - case State.WORKING: - case State.PENDING_SUCCESS: - case State.PENDING_ERROR: - workingIndicator.SetActive(true); - if (lastState == State.NOT_SHOWING && newState != State.NOT_SHOWING) { - ResetAnimColors(); - } - // "Working" indicator should be visible; "done" indicator should be invisible. - if (newState == State.WORKING) { + /// + /// Starts showing the indicator with the given message. + /// Caller should call FinishOperation when the operation is done. + /// + /// + public void StartOperation(string message) + { + progressText.text = message; operationStartTime = Time.time; - } - rootObject.SetActive(true); - break; - case State.SUCCESS: - // Clear the text so that it doesn't scale down. - progressText.text = ""; - audioLibrary.PlayClip(audioLibrary.saveSound); - ChangeState(State.PENDING_NOT_SHOWING); - break; - case State.ERROR: - // Both indicators ("working" and "success/error") are simultaneously visible, for animation purposes. - // After the transition animation, we hide the working indicator and leave only the success/error - // indicator. - operationFinishTime = Time.time; - rootObject.SetActive(true); - workingIndicator.SetActive(true); - SetAllColors(redBlock); - audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.errorSound); - break; - default: - throw new System.Exception("Invalid state " + state); - } + // Force hide the menu hint. + PeltzerMain.Instance.menuHint.ChangeState(MenuHint.State.INACTIVE); + ChangeState(State.WORKING); + } + + /// + /// Transitions the indicator into the "done" state, showing the given new message. + /// The indicator will automatically disappear after a few seconds. + /// + /// Whether the operation finished successfully. + /// The message to show. + public void FinishOperation(bool success, string message) + { + if (Time.time - operationStartTime < MIN_APPARENT_WORK_DURATION_SECONDS) + { + // It's too soon after we started the operation, so can't finish yet because it would look janky + // (the progress message would just pop for a brief time). So we will keep on pretending that we're + // still working for a bit, just for visual polish purposes. + pendingText = message; + ChangeState(success ? State.PENDING_SUCCESS : State.PENDING_ERROR); + } + else + { + // We've been showing the progress indicator for long enough that we can just finish immediately + // if it is an error. However we still need to go into a "pending" success state to make sure the + // save preview is ready. + progressText.text = message; + ChangeState(success ? State.PENDING_SUCCESS : State.ERROR); + } + } + + private void Update() + { + // Auto-advance the states at the right times. + float timeSinceStart = Time.time - operationStartTime; + float timeSinceFinish = Time.time - operationFinishTime; + if (state == State.PENDING_SUCCESS + && timeSinceStart > MIN_APPARENT_WORK_DURATION_SECONDS + && PeltzerMain.Instance.savePreview.state == SavePreview.State.WAITING) + { + // PENDING_SUCCESS -> SUCCESS. + progressText.text = pendingText; + ChangeState(State.SUCCESS); + } + else if (state == State.PENDING_ERROR && timeSinceStart > MIN_APPARENT_WORK_DURATION_SECONDS) + { + // PENDING_ERROR -> ERROR. + progressText.text = pendingText; + ChangeState(State.ERROR); + } + else if (state == State.ERROR && timeSinceFinish > FINISHED_STATE_DURATION_SECONDS) + { + // (SUCCESS | ERROR) -> NOT_SHOWING + ChangeState(State.PENDING_NOT_SHOWING); + } + else if (state == State.PENDING_NOT_SHOWING) + { + float pctDone = (Time.time - operationStartTime) / SCALE_OUT_ANIMATION_DURATION; + + if (pctDone > 1) + { + // If the last state was successful we want to show the saved preview after the progress + // indicator scales down. + if (lastState == State.SUCCESS) + { + PeltzerMain.Instance.savePreview.ChangeState(SavePreview.State.SCALE_ANIMATING); + } + ChangeState(State.NOT_SHOWING); + } + else + { + rootObject.transform.localScale = Vector3.Lerp(rootObjectScaleAtStart, Vector3.zero, pctDone); + } + } + } + + private void InstanceMats() + { + MeshRenderer redRenderer = redCube.GetComponent(); + redRenderer.material = new Material(redRenderer.material); + MeshRenderer yellowRenderer = yellowCube.GetComponent(); + yellowRenderer.material = new Material(yellowRenderer.material); + MeshRenderer blueRenderer = blueCube.GetComponent(); + blueRenderer.material = new Material(blueRenderer.material); + } + + private void SetAllColors(Color color) + { + MeshRenderer redRenderer = redCube.GetComponent(); + MeshRenderer yellowRenderer = yellowCube.GetComponent(); + MeshRenderer blueRenderer = blueCube.GetComponent(); + + redRenderer.material.color = color; + yellowRenderer.material.color = color; + blueRenderer.material.color = color; + } + + private void ResetAnimColors() + { + MeshRenderer redRenderer = redCube.GetComponent(); + MeshRenderer yellowRenderer = yellowCube.GetComponent(); + MeshRenderer blueRenderer = blueCube.GetComponent(); + + redRenderer.material.color = redBlock; + yellowRenderer.material.color = yellowBlock; + blueRenderer.material.color = blueBlock; + } + + /// + /// Changes state and does the necessary maintenance tasks to enter the new state. + /// + /// The new state to enter. + private void ChangeState(State newState) + { + lastState = state; + AudioLibrary audioLibrary = PeltzerMain.Instance.audioLibrary; + + state = newState; + rootObject.SetActive(newState != State.NOT_SHOWING); + + switch (state) + { + case State.PENDING_NOT_SHOWING: + operationStartTime = Time.time; + break; + case State.NOT_SHOWING: + rootObject.transform.localScale = rootObjectScaleAtStart; + break; + case State.WORKING: + case State.PENDING_SUCCESS: + case State.PENDING_ERROR: + workingIndicator.SetActive(true); + if (lastState == State.NOT_SHOWING && newState != State.NOT_SHOWING) + { + ResetAnimColors(); + } + // "Working" indicator should be visible; "done" indicator should be invisible. + if (newState == State.WORKING) + { + operationStartTime = Time.time; + } + rootObject.SetActive(true); + break; + case State.SUCCESS: + // Clear the text so that it doesn't scale down. + progressText.text = ""; + audioLibrary.PlayClip(audioLibrary.saveSound); + ChangeState(State.PENDING_NOT_SHOWING); + break; + case State.ERROR: + // Both indicators ("working" and "success/error") are simultaneously visible, for animation purposes. + // After the transition animation, we hide the working indicator and leave only the success/error + // indicator. + operationFinishTime = Time.time; + rootObject.SetActive(true); + workingIndicator.SetActive(true); + SetAllColors(redBlock); + audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.errorSound); + break; + default: + throw new System.Exception("Invalid state " + state); + } + } } - } } diff --git a/Assets/Scripts/menu/SavePreview.cs b/Assets/Scripts/menu/SavePreview.cs index 9ef27b97..21919420 100644 --- a/Assets/Scripts/menu/SavePreview.cs +++ b/Assets/Scripts/menu/SavePreview.cs @@ -19,248 +19,280 @@ using com.google.apps.peltzer.client.tutorial; using UnityEngine; -namespace com.google.apps.peltzer.client.menu { - /// - /// Handles creating and animating a preview of the model a user just saved. This preview is used to - /// show that a save was successful but also shows the user where the model is saved in the poly menu. - /// - public class SavePreview : MonoBehaviour { +namespace com.google.apps.peltzer.client.menu +{ /// - /// Represents what state the savePreview is in. + /// Handles creating and animating a preview of the model a user just saved. This preview is used to + /// show that a save was successful but also shows the user where the model is saved in the poly menu. /// - public enum State { - // Default. - NONE, - // Not trying to show any preview. - INACTIVE, - // The preview is ready and waiting to be activated. - WAITING, - // The preview is active on the controller and is not currently being animated. This represents the - // time in between scaling up and then getting sucked into the controller. - ACTIVE, - // The preview is being scaled into existence. - SCALE_ANIMATING, - // The preview is being suctioned into the menu button. - SUCTION_ANIMATING, - // The button is expanding out in response to the preview hitting it. - BUTTON_EXPAND_ANIMATING, - // The button is collapsing back to its original size. - BUTTON_COLLAPSE_ANIMATING - }; + public class SavePreview : MonoBehaviour + { + /// + /// Represents what state the savePreview is in. + /// + public enum State + { + // Default. + NONE, + // Not trying to show any preview. + INACTIVE, + // The preview is ready and waiting to be activated. + WAITING, + // The preview is active on the controller and is not currently being animated. This represents the + // time in between scaling up and then getting sucked into the controller. + ACTIVE, + // The preview is being scaled into existence. + SCALE_ANIMATING, + // The preview is being suctioned into the menu button. + SUCTION_ANIMATING, + // The button is expanding out in response to the preview hitting it. + BUTTON_EXPAND_ANIMATING, + // The button is collapsing back to its original size. + BUTTON_COLLAPSE_ANIMATING + }; - public static float PREVIEW_DURATION = 0.5f; - public static float SUCTION_ANIMATION_DURATION = 0.75f; - private const float SCALE_IN_ANIMATION_DURATION = 0.2f; - private const float BUTTON_EXPAND_ANIMATION_DURATION = 0.2f; - private const float BUTTON_COLLAPSE_ANIMATION_DURATION = 0.1f; - private const float BUTTON_SCALE_FACTOR = 1.4f; + public static float PREVIEW_DURATION = 0.5f; + public static float SUCTION_ANIMATION_DURATION = 0.75f; + private const float SCALE_IN_ANIMATION_DURATION = 0.2f; + private const float BUTTON_EXPAND_ANIMATION_DURATION = 0.2f; + private const float BUTTON_COLLAPSE_ANIMATION_DURATION = 0.1f; + private const float BUTTON_SCALE_FACTOR = 1.4f; - public State state { get; private set; } + public State state { get; private set; } - /// - /// The save preview that replaces the saving indicator and then gets sucked into the controller. - /// - private GameObject preview; - /// - /// The worldspace that the preview exists in. We use a unique worldspace for the preview that we - /// can scale, this is easier than actually changing the mesh. - /// - private WorldSpace previewSpace; - /// - /// The scale of the preview before any animating. - /// - private float previewScale; - /// - /// The position of the preview before any animating. - /// - private Vector3 previewPosAtStart; + /// + /// The save preview that replaces the saving indicator and then gets sucked into the controller. + /// + private GameObject preview; + /// + /// The worldspace that the preview exists in. We use a unique worldspace for the preview that we + /// can scale, this is easier than actually changing the mesh. + /// + private WorldSpace previewSpace; + /// + /// The scale of the preview before any animating. + /// + private float previewScale; + /// + /// The position of the preview before any animating. + /// + private Vector3 previewPosAtStart; - /// - /// The scale of the button before any animating. - /// - private Vector3 buttonStartScale; - /// - /// The max scale the button should reach while animating. - /// - private Vector3 buttonMaxScale; + /// + /// The scale of the button before any animating. + /// + private Vector3 buttonStartScale; + /// + /// The max scale the button should reach while animating. + /// + private Vector3 buttonMaxScale; - /// - /// The time that we started the current operation. - /// - private float operationStartTime; + /// + /// The time that we started the current operation. + /// + private float operationStartTime; - public void Setup() { - previewSpace = new WorldSpace(PeltzerMain.DEFAULT_BOUNDS, /* isLimited */ false); - previewScale = previewSpace.scale; - } + public void Setup() + { + previewSpace = new WorldSpace(PeltzerMain.DEFAULT_BOUNDS, /* isLimited */ false); + previewScale = previewSpace.scale; + } - void Update() { - if (state == State.SCALE_ANIMATING) { - // Scale the preview into scene by lerping the worldSpace for the preview. + void Update() + { + if (state == State.SCALE_ANIMATING) + { + // Scale the preview into scene by lerping the worldSpace for the preview. - // Scale animation is linear relative to passed time. - float pctDone = (Time.time - operationStartTime) / SCALE_IN_ANIMATION_DURATION; + // Scale animation is linear relative to passed time. + float pctDone = (Time.time - operationStartTime) / SCALE_IN_ANIMATION_DURATION; - if (pctDone > 1f) { - // Set the scale to its final value. - previewSpace.scale = previewScale; - // Make the preview active. This is the state where it previews on the controller before being - // sucked into the button. - ChangeState(State.ACTIVE); - } else { - previewSpace.scale = Mathf.Lerp(0f, previewScale, pctDone); - } - } else if (state == State.ACTIVE && Time.time > operationStartTime + PREVIEW_DURATION) { - // Show the preview on the controller before starting the suction animation. - ChangeState(State.SUCTION_ANIMATING); - } else if (state == State.SUCTION_ANIMATING) { - // Make the preview look like its being sucked into the controller by slerping its position - // and lerping its scale down. We use a Cubic Bezier curve to make the animation speed up - // over time to get the suction effect. + if (pctDone > 1f) + { + // Set the scale to its final value. + previewSpace.scale = previewScale; + // Make the preview active. This is the state where it previews on the controller before being + // sucked into the button. + ChangeState(State.ACTIVE); + } + else + { + previewSpace.scale = Mathf.Lerp(0f, previewScale, pctDone); + } + } + else if (state == State.ACTIVE && Time.time > operationStartTime + PREVIEW_DURATION) + { + // Show the preview on the controller before starting the suction animation. + ChangeState(State.SUCTION_ANIMATING); + } + else if (state == State.SUCTION_ANIMATING) + { + // Make the preview look like its being sucked into the controller by slerping its position + // and lerping its scale down. We use a Cubic Bezier curve to make the animation speed up + // over time to get the suction effect. - float pctDone = Math3d.CubicBezierEasing(0f, 0f, 0.1f, 1f, - (Time.time - operationStartTime) / SUCTION_ANIMATION_DURATION); + float pctDone = Math3d.CubicBezierEasing(0f, 0f, 0.1f, 1f, + (Time.time - operationStartTime) / SUCTION_ANIMATION_DURATION); - if (pctDone > 1f) { - // Make a thud when the preview hits the button. - PeltzerMain.Instance.paletteController - .TriggerHapticFeedback(HapticFeedback.HapticFeedbackType.FEEDBACK_1, 0.02f, 1f); + if (pctDone > 1f) + { + // Make a thud when the preview hits the button. + PeltzerMain.Instance.paletteController + .TriggerHapticFeedback(HapticFeedback.HapticFeedbackType.FEEDBACK_1, 0.02f, 1f); - // State expanding the button in response to the preview hitting it. - ChangeState(State.BUTTON_EXPAND_ANIMATING); - } else { - // Animate the scale. - previewSpace.scale = Mathf.Lerp(previewScale, 0f, pctDone); + // State expanding the button in response to the preview hitting it. + ChangeState(State.BUTTON_EXPAND_ANIMATING); + } + else + { + // Animate the scale. + previewSpace.scale = Mathf.Lerp(previewScale, 0f, pctDone); - // Animate the position. - preview.transform.localPosition = Vector3.Slerp( - previewPosAtStart, - PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButtonHolder.transform.localPosition, - pctDone); + // Animate the position. + preview.transform.localPosition = Vector3.Slerp( + previewPosAtStart, + PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButtonHolder.transform.localPosition, + pctDone); - // Trigger feedback that gets stronger over the animation. - PeltzerMain.Instance.paletteController.TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_1, 0.001f, pctDone == 0f ? 0f : pctDone * 0.1f); - } - } else if (state == State.BUTTON_EXPAND_ANIMATING) { - // Animate the button expanding. We want it to look like a ripple because the preview hit it so we - // use a Cubic Bezier curve to lerp the scale quickly at first before slowing down. - float pctDone = Math3d.CubicBezierEasing(0f, 1.0f, 1.0f, 1.0f, - (Time.time - operationStartTime) / BUTTON_EXPAND_ANIMATION_DURATION); - - // We need the holder which is centered over the button. The transform of the actual button is - // incorrect because of a flaw in the controller UI. - Transform buttonHolder = - PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButtonHolder.transform; + // Trigger feedback that gets stronger over the animation. + PeltzerMain.Instance.paletteController.TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_1, 0.001f, pctDone == 0f ? 0f : pctDone * 0.1f); + } + } + else if (state == State.BUTTON_EXPAND_ANIMATING) + { + // Animate the button expanding. We want it to look like a ripple because the preview hit it so we + // use a Cubic Bezier curve to lerp the scale quickly at first before slowing down. + float pctDone = Math3d.CubicBezierEasing(0f, 1.0f, 1.0f, 1.0f, + (Time.time - operationStartTime) / BUTTON_EXPAND_ANIMATION_DURATION); - if (pctDone > 1.0f) { - buttonHolder.localScale = buttonMaxScale; - ChangeState(State.BUTTON_COLLAPSE_ANIMATING); - } else { - // Animate the scale. - buttonHolder.localScale = Vector3.Lerp(buttonStartScale, buttonMaxScale, pctDone); + // We need the holder which is centered over the button. The transform of the actual button is + // incorrect because of a flaw in the controller UI. + Transform buttonHolder = + PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButtonHolder.transform; - // Animate the glow on the actual button. - GameObject actualButton = - PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButton; + if (pctDone > 1.0f) + { + buttonHolder.localScale = buttonMaxScale; + ChangeState(State.BUTTON_COLLAPSE_ANIMATING); + } + else + { + // Animate the scale. + buttonHolder.localScale = Vector3.Lerp(buttonStartScale, buttonMaxScale, pctDone); - AttentionCaller.SetEmissiveFactor( - actualButton, - (Time.time - operationStartTime) / BUTTON_EXPAND_ANIMATION_DURATION, - actualButton.GetComponent().material.color); - } - } else if (state == State.BUTTON_COLLAPSE_ANIMATING) { - // No need to do anything fancy here. We scale down the button linearly and quickly. - float pctDone = (Time.time - operationStartTime) / BUTTON_COLLAPSE_ANIMATION_DURATION; - - Transform buttonHolder = - PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButtonHolder.transform; + // Animate the glow on the actual button. + GameObject actualButton = + PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButton; + + AttentionCaller.SetEmissiveFactor( + actualButton, + (Time.time - operationStartTime) / BUTTON_EXPAND_ANIMATION_DURATION, + actualButton.GetComponent().material.color); + } + } + else if (state == State.BUTTON_COLLAPSE_ANIMATING) + { + // No need to do anything fancy here. We scale down the button linearly and quickly. + float pctDone = (Time.time - operationStartTime) / BUTTON_COLLAPSE_ANIMATION_DURATION; + + Transform buttonHolder = + PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButtonHolder.transform; - if (pctDone > 1.0f) { - buttonHolder.localScale = buttonStartScale; + if (pctDone > 1.0f) + { + buttonHolder.localScale = buttonStartScale; - // We don't animate the glow away. We want it to be as luminous for as long as possible. - GameObject actualButton = - PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButton; - AttentionCaller.SetEmissiveFactor(actualButton, 0f, actualButton.GetComponent().material.color); - ChangeState(State.INACTIVE); - } else { - buttonHolder.localScale = Vector3.Lerp(buttonMaxScale, buttonStartScale, pctDone); + // We don't animate the glow away. We want it to be as luminous for as long as possible. + GameObject actualButton = + PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButton; + AttentionCaller.SetEmissiveFactor(actualButton, 0f, actualButton.GetComponent().material.color); + ChangeState(State.INACTIVE); + } + else + { + buttonHolder.localScale = Vector3.Lerp(buttonMaxScale, buttonStartScale, pctDone); + } + } } - } - } - public void ChangeState(State newState) { - // Don't try to show the preview if the meshes haven't been passed from ZandriaCreationsManager yet. - if (newState == State.SCALE_ANIMATING && state != State.WAITING) { - return; - } + public void ChangeState(State newState) + { + // Don't try to show the preview if the meshes haven't been passed from ZandriaCreationsManager yet. + if (newState == State.SCALE_ANIMATING && state != State.WAITING) + { + return; + } - state = newState; + state = newState; - switch (state) { - case State.INACTIVE: - break; - case State.WAITING: - // Do nothing. SetupBubble should be called to switch the state to WAITING. We need that method to be - // called externally by ZandriaCreationManager so that the meshes for the preview can be passed in. - break; - case State.ACTIVE: - operationStartTime = Time.time; - break; - case State.SCALE_ANIMATING: - // We can only switch to the SCALE_ANIMATING state when the last state was WAITING so that we know the - // mesh preview is ready to be shown. - preview.SetActive(true); - previewScale = previewSpace.scale; - previewSpace.scale = 0f; - operationStartTime = Time.time; - break; - case State.SUCTION_ANIMATING: - previewPosAtStart = preview.transform.localPosition; - operationStartTime = Time.time; - break; - case State.BUTTON_EXPAND_ANIMATING: - if (preview != null) { - preview.SetActive(false); - Destroy(preview); - previewSpace.scale = previewScale; - } + switch (state) + { + case State.INACTIVE: + break; + case State.WAITING: + // Do nothing. SetupBubble should be called to switch the state to WAITING. We need that method to be + // called externally by ZandriaCreationManager so that the meshes for the preview can be passed in. + break; + case State.ACTIVE: + operationStartTime = Time.time; + break; + case State.SCALE_ANIMATING: + // We can only switch to the SCALE_ANIMATING state when the last state was WAITING so that we know the + // mesh preview is ready to be shown. + preview.SetActive(true); + previewScale = previewSpace.scale; + previewSpace.scale = 0f; + operationStartTime = Time.time; + break; + case State.SUCTION_ANIMATING: + previewPosAtStart = preview.transform.localPosition; + operationStartTime = Time.time; + break; + case State.BUTTON_EXPAND_ANIMATING: + if (preview != null) + { + preview.SetActive(false); + Destroy(preview); + previewSpace.scale = previewScale; + } - operationStartTime = Time.time; - buttonStartScale = - PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButton.transform.parent.localScale; - buttonMaxScale = new Vector3( - buttonStartScale.x * BUTTON_SCALE_FACTOR, - buttonStartScale.y, - buttonStartScale.z * BUTTON_SCALE_FACTOR); - break; - case State.BUTTON_COLLAPSE_ANIMATING: - operationStartTime = Time.time; - break; - } - } + operationStartTime = Time.time; + buttonStartScale = + PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButton.transform.parent.localScale; + buttonMaxScale = new Vector3( + buttonStartScale.x * BUTTON_SCALE_FACTOR, + buttonStartScale.y, + buttonStartScale.z * BUTTON_SCALE_FACTOR); + break; + case State.BUTTON_COLLAPSE_ANIMATING: + operationStartTime = Time.time; + break; + } + } - public void SetupPreview(MeshWithMaterialRenderer mwmRenderer) { - if (state != State.INACTIVE && state != State.NONE) { - // If the save preview is currently active and in progress, skip the save preview for the new save. - // This happens when saves are in quick succession (usually for save selected), so the user should already - // know where the saved models are going from the in progress preview. - return; - } - preview = new GameObject(); - MeshWithMaterialRenderer clonedRenderer = preview.AddComponent(); - clonedRenderer.SetupAsCopyOf(mwmRenderer); - clonedRenderer.worldSpace = previewSpace; + public void SetupPreview(MeshWithMaterialRenderer mwmRenderer) + { + if (state != State.INACTIVE && state != State.NONE) + { + // If the save preview is currently active and in progress, skip the save preview for the new save. + // This happens when saves are in quick succession (usually for save selected), so the user should already + // know where the saved models are going from the in progress preview. + return; + } + preview = new GameObject(); + MeshWithMaterialRenderer clonedRenderer = preview.AddComponent(); + clonedRenderer.SetupAsCopyOf(mwmRenderer); + clonedRenderer.worldSpace = previewSpace; - preview.transform.parent = - PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButtonHolder.transform.parent; - preview.transform.position = ObjectFinder.ObjectById("ID_ProgressIndicatorPanel").transform.position; + preview.transform.parent = + PeltzerMain.Instance.paletteController.controllerGeometry.appMenuButtonHolder.transform.parent; + preview.transform.position = ObjectFinder.ObjectById("ID_ProgressIndicatorPanel").transform.position; - // Hide it until the progressIndicator is done. - preview.SetActive(false); + // Hide it until the progressIndicator is done. + preview.SetActive(false); - ChangeState(State.WAITING); + ChangeState(State.WAITING); + } } - } } diff --git a/Assets/Scripts/menu/StartupMenuItem.cs b/Assets/Scripts/menu/StartupMenuItem.cs index b20454e7..a67d173d 100644 --- a/Assets/Scripts/menu/StartupMenuItem.cs +++ b/Assets/Scripts/menu/StartupMenuItem.cs @@ -19,56 +19,67 @@ using com.google.apps.peltzer.client.model.util; using com.google.apps.peltzer.client.model.controller; -namespace com.google.apps.peltzer.client.menu { - /// - /// Represents one of the items in the startup menu. - /// - public class StartupMenuItem : SelectableMenuItem { - // Set from Unity. Can be null. - public GameObject normalObject; - // Set from Unity. Can be null. - public GameObject hoverObject; +namespace com.google.apps.peltzer.client.menu +{ + /// + /// Represents one of the items in the startup menu. + /// + public class StartupMenuItem : SelectableMenuItem + { + // Set from Unity. Can be null. + public GameObject normalObject; + // Set from Unity. Can be null. + public GameObject hoverObject; - private Collider ourCollider; - private Material ourMaterial; - public bool hovering { get; private set; } - public bool pointing { get; set; } + private Collider ourCollider; + private Material ourMaterial; + public bool hovering { get; private set; } + public bool pointing { get; set; } - private void Start() { - hovering = false; - pointing = false; - ourCollider = gameObject.GetComponent(); - AssertOrThrow.NotNull(ourCollider, "StartupMenuItem needs a Collider."); - - if (normalObject != null) { - normalObject.SetActive(true); - } - if (hoverObject != null) { - hoverObject.SetActive(false); - } - - } + private void Start() + { + hovering = false; + pointing = false; + ourCollider = gameObject.GetComponent(); + AssertOrThrow.NotNull(ourCollider, "StartupMenuItem needs a Collider."); - private void Update() { - if (!PeltzerMain.Instance.peltzerController) { - // PeltzerMain hasn't initialized the controller yet, so don't do anything for now. - return; - } + if (normalObject != null) + { + normalObject.SetActive(true); + } + if (hoverObject != null) + { + hoverObject.SetActive(false); + } - if (hovering != - (ourCollider.bounds.Contains(PeltzerMain.Instance.peltzerController.transform.position) || pointing)) { - hovering = !hovering; - if (hovering) { - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - } - if (hoverObject != null) { - hoverObject.SetActive(hovering); } - if (normalObject != null) { - normalObject.SetActive(!hovering); + + private void Update() + { + if (!PeltzerMain.Instance.peltzerController) + { + // PeltzerMain hasn't initialized the controller yet, so don't do anything for now. + return; + } + + if (hovering != + (ourCollider.bounds.Contains(PeltzerMain.Instance.peltzerController.transform.position) || pointing)) + { + hovering = !hovering; + if (hovering) + { + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + } + if (hoverObject != null) + { + hoverObject.SetActive(hovering); + } + if (normalObject != null) + { + normalObject.SetActive(!hovering); + } + } } - } } - } } diff --git a/Assets/Scripts/model/controller/BaseControllerAnimation.cs b/Assets/Scripts/model/controller/BaseControllerAnimation.cs index 193dfa03..4a050319 100644 --- a/Assets/Scripts/model/controller/BaseControllerAnimation.cs +++ b/Assets/Scripts/model/controller/BaseControllerAnimation.cs @@ -16,316 +16,365 @@ using System.Collections; using com.google.apps.peltzer.client.app; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Dumb wrapper around hardware-specific logic for controller animations. - /// - public class BaseControllerAnimation : MonoBehaviour { - // The physical controller responsible for input & pose. - protected ControllerDevice controller; - - /// - /// The local position pivot point to use when rotating the VIVE trigger mesh. - /// - private readonly Vector3 TRIGGER_PIVOT_POINT_VIVE = new Vector3(0f, -0.005f, -0.05f); - /// - /// The local position pivot point to use when rotating the RIFT trigger mesh. - /// - private readonly Vector3 TRIGGER_PIVOT_POINT_RIFT = new Vector3(0f, 0.01037f, -0.00451f); - /// Values used to calculate the LGripPressedLoc and RGripPressedLoc values. - private const float GRIP_PRESS_OFFSET_VIVE = 0.0005f; - private const float GRIP_PRESS_OFFSET_RIFT = 0.002f; - /// - /// The positional y-offset for the touchpad (or any other "button") mesh when the user has pressed the button. - /// - private const float BTN_PRESSED_LOCATION_Y_OFFSET = 0.001f; - /// - /// The positional y-offset for the touchpad (or any other "button") mesh when the user has pressed the button. - /// - private const float BTN_PRESSED_LOCATION_Y_OFFSET_RIFT = 0.001f; - /// - /// The maximum +/- angle to lerp through rotation along X and Z axis - /// for touchpad orientation as mappeed to user input. - /// - private const float TOUCHPAD_MAX_ANGLE = 4f; - /// - /// The maximum angle to lerp through for the angle of the trigger mesh as mapped to user input. - /// - private const float TRIGGER_MAX_ANGLE = 10f; - /// - /// A scale factor used when mapping the location of the user's thumb to our custom model - /// which also accounts for the size of the location visual. - /// - private const float USER_THUMB_LOCATION_SCALE_FACTOR = 0.019f; - /// - /// The positional offset for placing the thumb location highlight on the trackpad. - /// - private readonly Vector3 USER_THUMB_LOCATION_OFFSET = new Vector3(0f, 0.008f, 0f); - /// - /// A visual indicator placed on the touchpad to show the user's thumb location on touch. - /// - private GameObject userThumbLocation; - /// - /// A reference to the root of the touchpad GameObjects. - /// - public GameObject touchpads; - /// - /// A reference to the default / start position of the touchpad from the related controller model. - /// - private Vector3 touchpadDefaultLoc; - /// - /// A reference to the default / start rotation of the touchpad from the related controller model. - /// - private Vector3 touchpadDefaultRot; - /// - /// The local position pivot point to use when rotating the trigger mesh. - /// - private Vector3 triggerPivotPoint; - - /// - /// A reference to the the trigger mesh GameObject. - /// - public GameObject trigger; - /// - /// A reference to the default / start position of the trigger from the related controller model. - /// - private Vector3 triggerDefaultLoc; - /// - /// A reference to the default / start rotation of the trigger from the related controller model. - /// - private Vector3 triggerDefaultRot; - - /// - /// A reference to the left grip mesh GameObject. - /// - public GameObject LGrip; - /// - /// A reference to the right grip mesh GameObject. - /// - public GameObject RGrip; - /// - /// A rerence to the default / start position of the left grip from the related controller model - /// - private Vector3 LGripDefaultLoc; +namespace com.google.apps.peltzer.client.model.controller +{ /// - /// A rerence to the default / start position of the right grip from the related controller model + /// Dumb wrapper around hardware-specific logic for controller animations. /// - private Vector3 RGripDefaultLoc; - /// - /// A rerence to the position when pressed of the left grip from the related controller model - /// - private Vector3 LGripPressedLoc; - /// - /// A rerence to the position when pressed of the right grip from the related controller model - /// - private Vector3 RGripPressedLoc; + public class BaseControllerAnimation : MonoBehaviour + { + // The physical controller responsible for input & pose. + protected ControllerDevice controller; - /// - /// A reference to the App Menu button. - /// - public GameObject appMenuButton; - /// - /// A rerence to the default / start position of the App Menu button related controller model - /// - private Vector3 appMenuButtonDefaultLoc; - /// - /// A reference to the Secondary button (only applicable for Touch controllers) - /// - public GameObject secondaryButton; - /// - /// Contains the overlays for the touchpad - using this for Rift touchpad effect. - /// - public Transform touchpadOverlay; - /// - /// Touchpad icons around Rift stick. - /// - public GameObject touchpadIcon; - /// - /// A rerence to the default / start position of the App Menu button related controller model - /// - /// - private Vector3 secondaryButtonDefaultLoc; + /// + /// The local position pivot point to use when rotating the VIVE trigger mesh. + /// + private readonly Vector3 TRIGGER_PIVOT_POINT_VIVE = new Vector3(0f, -0.005f, -0.05f); + /// + /// The local position pivot point to use when rotating the RIFT trigger mesh. + /// + private readonly Vector3 TRIGGER_PIVOT_POINT_RIFT = new Vector3(0f, 0.01037f, -0.00451f); + /// Values used to calculate the LGripPressedLoc and RGripPressedLoc values. + private const float GRIP_PRESS_OFFSET_VIVE = 0.0005f; + private const float GRIP_PRESS_OFFSET_RIFT = 0.002f; + /// + /// The positional y-offset for the touchpad (or any other "button") mesh when the user has pressed the button. + /// + private const float BTN_PRESSED_LOCATION_Y_OFFSET = 0.001f; + /// + /// The positional y-offset for the touchpad (or any other "button") mesh when the user has pressed the button. + /// + private const float BTN_PRESSED_LOCATION_Y_OFFSET_RIFT = 0.001f; + /// + /// The maximum +/- angle to lerp through rotation along X and Z axis + /// for touchpad orientation as mappeed to user input. + /// + private const float TOUCHPAD_MAX_ANGLE = 4f; + /// + /// The maximum angle to lerp through for the angle of the trigger mesh as mapped to user input. + /// + private const float TRIGGER_MAX_ANGLE = 10f; + /// + /// A scale factor used when mapping the location of the user's thumb to our custom model + /// which also accounts for the size of the location visual. + /// + private const float USER_THUMB_LOCATION_SCALE_FACTOR = 0.019f; + /// + /// The positional offset for placing the thumb location highlight on the trackpad. + /// + private readonly Vector3 USER_THUMB_LOCATION_OFFSET = new Vector3(0f, 0.008f, 0f); + /// + /// A visual indicator placed on the touchpad to show the user's thumb location on touch. + /// + private GameObject userThumbLocation; + /// + /// A reference to the root of the touchpad GameObjects. + /// + public GameObject touchpads; + /// + /// A reference to the default / start position of the touchpad from the related controller model. + /// + private Vector3 touchpadDefaultLoc; + /// + /// A reference to the default / start rotation of the touchpad from the related controller model. + /// + private Vector3 touchpadDefaultRot; + /// + /// The local position pivot point to use when rotating the trigger mesh. + /// + private Vector3 triggerPivotPoint; - void Start() { - // Create the touchpad location visual. - touchpadDefaultLoc = touchpads.transform.localPosition; - touchpadDefaultRot = touchpads.transform.localEulerAngles; + /// + /// A reference to the the trigger mesh GameObject. + /// + public GameObject trigger; + /// + /// A reference to the default / start position of the trigger from the related controller model. + /// + private Vector3 triggerDefaultLoc; + /// + /// A reference to the default / start rotation of the trigger from the related controller model. + /// + private Vector3 triggerDefaultRot; - //UpdateRiftTouchPad(false); + /// + /// A reference to the left grip mesh GameObject. + /// + public GameObject LGrip; + /// + /// A reference to the right grip mesh GameObject. + /// + public GameObject RGrip; + /// + /// A rerence to the default / start position of the left grip from the related controller model + /// + private Vector3 LGripDefaultLoc; + /// + /// A rerence to the default / start position of the right grip from the related controller model + /// + private Vector3 RGripDefaultLoc; + /// + /// A rerence to the position when pressed of the left grip from the related controller model + /// + private Vector3 LGripPressedLoc; + /// + /// A rerence to the position when pressed of the right grip from the related controller model + /// + private Vector3 RGripPressedLoc; - if (Config.Instance.VrHardware == VrHardware.Vive) { - userThumbLocation = Instantiate(Resources.Load("Prefabs/userThumb")); - userThumbLocation.transform.SetParent(touchpads.transform, /* worldPositionStays */ true); - triggerPivotPoint = TRIGGER_PIVOT_POINT_VIVE; - } else if (Config.Instance.VrHardware == VrHardware.Rift) { - triggerPivotPoint = TRIGGER_PIVOT_POINT_RIFT; - } + /// + /// A reference to the App Menu button. + /// + public GameObject appMenuButton; + /// + /// A rerence to the default / start position of the App Menu button related controller model + /// + private Vector3 appMenuButtonDefaultLoc; + /// + /// A reference to the Secondary button (only applicable for Touch controllers) + /// + public GameObject secondaryButton; + /// + /// Contains the overlays for the touchpad - using this for Rift touchpad effect. + /// + public Transform touchpadOverlay; + /// + /// Touchpad icons around Rift stick. + /// + public GameObject touchpadIcon; + /// + /// A rerence to the default / start position of the App Menu button related controller model + /// + /// + private Vector3 secondaryButtonDefaultLoc; - // Get trigger defaults. - triggerDefaultLoc = trigger.transform.localPosition; - triggerDefaultRot = trigger.transform.localEulerAngles; + void Start() + { + // Create the touchpad location visual. + touchpadDefaultLoc = touchpads.transform.localPosition; + touchpadDefaultRot = touchpads.transform.localEulerAngles; - // Get grip defaults. - float gripPressOffset = Config.Instance.VrHardware == VrHardware.Vive ? GRIP_PRESS_OFFSET_VIVE : GRIP_PRESS_OFFSET_RIFT; - if (LGrip != null) { - LGripDefaultLoc = LGrip.transform.localPosition; - LGripPressedLoc = new Vector3(LGripDefaultLoc.x + gripPressOffset, LGripDefaultLoc.y, LGripDefaultLoc.z); - } - if (RGrip != null) { - RGripDefaultLoc = RGrip.transform.localPosition; - RGripPressedLoc = new Vector3(RGripDefaultLoc.x - gripPressOffset, RGripDefaultLoc.y, RGripDefaultLoc.z); - } + //UpdateRiftTouchPad(false); - // Get the reference to button defaults; - appMenuButtonDefaultLoc = appMenuButton.transform.localPosition; - if (secondaryButton != null) { - secondaryButtonDefaultLoc = secondaryButton.transform.localPosition; - } - } + if (Config.Instance.VrHardware == VrHardware.Vive) + { + userThumbLocation = Instantiate(Resources.Load("Prefabs/userThumb")); + userThumbLocation.transform.SetParent(touchpads.transform, /* worldPositionStays */ true); + triggerPivotPoint = TRIGGER_PIVOT_POINT_VIVE; + } + else if (Config.Instance.VrHardware == VrHardware.Rift) + { + triggerPivotPoint = TRIGGER_PIVOT_POINT_RIFT; + } - void Update() { - if (controller == null) { - return; - } - DetectTouchpad(controller); - DetectTrigger(controller); - DetectGrip(controller); - DetectAppMenu(controller); - if (secondaryButton != null) { - DetectSecondaryButton(controller); - } - } + // Get trigger defaults. + triggerDefaultLoc = trigger.transform.localPosition; + triggerDefaultRot = trigger.transform.localEulerAngles; - public void SetControllerDevice(ControllerDevice newControllerDevice) { - controller = newControllerDevice; - } + // Get grip defaults. + float gripPressOffset = Config.Instance.VrHardware == VrHardware.Vive ? GRIP_PRESS_OFFSET_VIVE : GRIP_PRESS_OFFSET_RIFT; + if (LGrip != null) + { + LGripDefaultLoc = LGrip.transform.localPosition; + LGripPressedLoc = new Vector3(LGripDefaultLoc.x + gripPressOffset, LGripDefaultLoc.y, LGripDefaultLoc.z); + } + if (RGrip != null) + { + RGripDefaultLoc = RGrip.transform.localPosition; + RGripPressedLoc = new Vector3(RGripDefaultLoc.x - gripPressOffset, RGripDefaultLoc.y, RGripDefaultLoc.z); + } - /// - /// Detect the state of the touchpad and set the orientation and thumb locator - /// orientation based on current value. - /// - /// ControllerDevice for referencing input. - private void DetectTouchpad(ControllerDevice controller) { - // Highlight - Vector2 loc = Vector2.zero; - if (Config.Instance.VrHardware == VrHardware.Vive) { - if (controller.IsTouched(ButtonId.Touchpad)) { - userThumbLocation.SetActive(true); - loc = controller.GetDirectionalAxis() * USER_THUMB_LOCATION_SCALE_FACTOR; - userThumbLocation.transform.localPosition = new Vector3( - USER_THUMB_LOCATION_OFFSET.x + loc.x, - USER_THUMB_LOCATION_OFFSET.y, - USER_THUMB_LOCATION_OFFSET.z + loc.y); - } else { - userThumbLocation.SetActive(false); + // Get the reference to button defaults; + appMenuButtonDefaultLoc = appMenuButton.transform.localPosition; + if (secondaryButton != null) + { + secondaryButtonDefaultLoc = secondaryButton.transform.localPosition; + } } - // Orientation - if (controller.IsPressed(ButtonId.Touchpad)) { - touchpads.transform.localPosition = new Vector3(touchpadDefaultLoc.x, - touchpadDefaultLoc.y - BTN_PRESSED_LOCATION_Y_OFFSET, touchpadDefaultLoc.z); - loc = controller.GetDirectionalAxis(); + void Update() + { + if (controller == null) + { + return; + } + DetectTouchpad(controller); + DetectTrigger(controller); + DetectGrip(controller); + DetectAppMenu(controller); + if (secondaryButton != null) + { + DetectSecondaryButton(controller); + } } - if (controller.WasJustReleased(ButtonId.Touchpad)) { - touchpads.transform.localPosition = touchpadDefaultLoc; - touchpads.transform.localEulerAngles = touchpadDefaultRot; + public void SetControllerDevice(ControllerDevice newControllerDevice) + { + controller = newControllerDevice; } - } else { // Oculus Touch. - loc = controller.GetDirectionalAxis(); - touchpads.transform.localRotation = Quaternion.Euler(loc.x*20,loc.y*-20,0); - } - } + /// + /// Detect the state of the touchpad and set the orientation and thumb locator + /// orientation based on current value. + /// + /// ControllerDevice for referencing input. + private void DetectTouchpad(ControllerDevice controller) + { + // Highlight + Vector2 loc = Vector2.zero; + if (Config.Instance.VrHardware == VrHardware.Vive) + { + if (controller.IsTouched(ButtonId.Touchpad)) + { + userThumbLocation.SetActive(true); + loc = controller.GetDirectionalAxis() * USER_THUMB_LOCATION_SCALE_FACTOR; + userThumbLocation.transform.localPosition = new Vector3( + USER_THUMB_LOCATION_OFFSET.x + loc.x, + USER_THUMB_LOCATION_OFFSET.y, + USER_THUMB_LOCATION_OFFSET.z + loc.y); + } + else + { + userThumbLocation.SetActive(false); + } - /// - /// Detect the state of the trigger and set the orientation of the mesh based on the reported value. - /// - /// ControllerDevice for referencing input. - private void DetectTrigger(ControllerDevice controller) { - Vector2 triggerRotationValue = controller.GetTriggerScale() * TRIGGER_MAX_ANGLE; - Vector3 dir = triggerDefaultLoc - triggerPivotPoint; // get point direction relative to pivot - dir = Quaternion.Euler(triggerRotationValue) * dir; // rotate it - trigger.transform.localPosition = dir + triggerPivotPoint; // calculate rotated point - trigger.transform.localEulerAngles = new Vector3( - triggerDefaultRot.x - triggerRotationValue.x, - triggerDefaultRot.y, - triggerDefaultRot.z); - } + // Orientation + if (controller.IsPressed(ButtonId.Touchpad)) + { + touchpads.transform.localPosition = new Vector3(touchpadDefaultLoc.x, + touchpadDefaultLoc.y - BTN_PRESSED_LOCATION_Y_OFFSET, touchpadDefaultLoc.z); + loc = controller.GetDirectionalAxis(); + } - /// - /// Detect the grip state and set the orientation of the mesh based on the reported value. - /// - /// ControllerDevice for referencing input. - private void DetectGrip(ControllerDevice controller) { - if (Config.Instance.VrHardware == VrHardware.Vive) { + if (controller.WasJustReleased(ButtonId.Touchpad)) + { + touchpads.transform.localPosition = touchpadDefaultLoc; + touchpads.transform.localEulerAngles = touchpadDefaultRot; + } - if (controller.WasJustPressed(ButtonId.Grip)) { - if (LGrip != null) { - LGrip.transform.localPosition = LGripPressedLoc; - } - if (RGrip != null) { - RGrip.transform.localPosition = RGripPressedLoc; - } - } else if (controller.WasJustReleased(ButtonId.Grip)) { - if (LGrip != null) { - LGrip.transform.localPosition = LGripDefaultLoc; - } - if (RGrip != null) { - RGrip.transform.localPosition = RGripDefaultLoc; - } + } + else + { // Oculus Touch. + loc = controller.GetDirectionalAxis(); + touchpads.transform.localRotation = Quaternion.Euler(loc.x * 20, loc.y * -20, 0); + } } - } - else { //Oculus Touch - if (controller.WasJustPressed(ButtonId.Grip)) { - if (LGrip != null) { - LGrip.transform.localPosition = new Vector3(0, 0, -.004f); - } - if (RGrip != null) { - RGrip.transform.localPosition = new Vector3(0, 0, -0.0145f); - } - } else if (controller.WasJustReleased(ButtonId.Grip)) { - if (LGrip != null) { - LGrip.transform.localPosition = LGripDefaultLoc; - } - if (RGrip != null) { - RGrip.transform.localPosition = RGripDefaultLoc; - } + /// + /// Detect the state of the trigger and set the orientation of the mesh based on the reported value. + /// + /// ControllerDevice for referencing input. + private void DetectTrigger(ControllerDevice controller) + { + Vector2 triggerRotationValue = controller.GetTriggerScale() * TRIGGER_MAX_ANGLE; + Vector3 dir = triggerDefaultLoc - triggerPivotPoint; // get point direction relative to pivot + dir = Quaternion.Euler(triggerRotationValue) * dir; // rotate it + trigger.transform.localPosition = dir + triggerPivotPoint; // calculate rotated point + trigger.transform.localEulerAngles = new Vector3( + triggerDefaultRot.x - triggerRotationValue.x, + triggerDefaultRot.y, + triggerDefaultRot.z); } - } - } + /// + /// Detect the grip state and set the orientation of the mesh based on the reported value. + /// + /// ControllerDevice for referencing input. + private void DetectGrip(ControllerDevice controller) + { + if (Config.Instance.VrHardware == VrHardware.Vive) + { - /// - /// Detect the App Menu button state and set the orientation of the mesh based on the reported value. - /// - /// ControllerDevice for referencing input. - private void DetectAppMenu(ControllerDevice controller) { - if (controller.WasJustPressed(ButtonId.ApplicationMenu)) { - appMenuButton.transform.localPosition = new Vector3(appMenuButton.transform.localPosition.x, - appMenuButton.transform.localPosition.y - BTN_PRESSED_LOCATION_Y_OFFSET, - appMenuButton.transform.localPosition.z); - } else if (controller.WasJustReleased(ButtonId.ApplicationMenu)) { - appMenuButton.transform.localPosition = appMenuButtonDefaultLoc; - } - } + if (controller.WasJustPressed(ButtonId.Grip)) + { + if (LGrip != null) + { + LGrip.transform.localPosition = LGripPressedLoc; + } + if (RGrip != null) + { + RGrip.transform.localPosition = RGripPressedLoc; + } + } + else if (controller.WasJustReleased(ButtonId.Grip)) + { + if (LGrip != null) + { + LGrip.transform.localPosition = LGripDefaultLoc; + } + if (RGrip != null) + { + RGrip.transform.localPosition = RGripDefaultLoc; + } + } + } - public void UpdateTouchpadOverlay(GameObject toolhead) { - } + else + { //Oculus Touch + if (controller.WasJustPressed(ButtonId.Grip)) + { + if (LGrip != null) + { + LGrip.transform.localPosition = new Vector3(0, 0, -.004f); + } + if (RGrip != null) + { + RGrip.transform.localPosition = new Vector3(0, 0, -0.0145f); + } + } + else if (controller.WasJustReleased(ButtonId.Grip)) + { + if (LGrip != null) + { + LGrip.transform.localPosition = LGripDefaultLoc; + } + if (RGrip != null) + { + RGrip.transform.localPosition = RGripDefaultLoc; + } + } + } - /// - /// Detect the secondary button state and set the orientation of the mesh based on the reported value. - /// - /// ControllerDevice for referencing input. - private void DetectSecondaryButton(ControllerDevice controller) { - if (controller.WasJustPressed(ButtonId.SecondaryButton)) { - secondaryButton.transform.localPosition = new Vector3(secondaryButton.transform.localPosition.x, - secondaryButton.transform.localPosition.y - BTN_PRESSED_LOCATION_Y_OFFSET, - secondaryButton.transform.localPosition.z); - } else if (controller.WasJustReleased(ButtonId.SecondaryButton)) { - secondaryButton.transform.localPosition = secondaryButtonDefaultLoc; - } + } + + /// + /// Detect the App Menu button state and set the orientation of the mesh based on the reported value. + /// + /// ControllerDevice for referencing input. + private void DetectAppMenu(ControllerDevice controller) + { + if (controller.WasJustPressed(ButtonId.ApplicationMenu)) + { + appMenuButton.transform.localPosition = new Vector3(appMenuButton.transform.localPosition.x, + appMenuButton.transform.localPosition.y - BTN_PRESSED_LOCATION_Y_OFFSET, + appMenuButton.transform.localPosition.z); + } + else if (controller.WasJustReleased(ButtonId.ApplicationMenu)) + { + appMenuButton.transform.localPosition = appMenuButtonDefaultLoc; + } + } + + public void UpdateTouchpadOverlay(GameObject toolhead) + { + } + + /// + /// Detect the secondary button state and set the orientation of the mesh based on the reported value. + /// + /// ControllerDevice for referencing input. + private void DetectSecondaryButton(ControllerDevice controller) + { + if (controller.WasJustPressed(ButtonId.SecondaryButton)) + { + secondaryButton.transform.localPosition = new Vector3(secondaryButton.transform.localPosition.x, + secondaryButton.transform.localPosition.y - BTN_PRESSED_LOCATION_Y_OFFSET, + secondaryButton.transform.localPosition.z); + } + else if (controller.WasJustReleased(ButtonId.SecondaryButton)) + { + secondaryButton.transform.localPosition = secondaryButtonDefaultLoc; + } + } } - } } diff --git a/Assets/Scripts/model/controller/ButtonAction.cs b/Assets/Scripts/model/controller/ButtonAction.cs index 1e757d59..7d727694 100644 --- a/Assets/Scripts/model/controller/ButtonAction.cs +++ b/Assets/Scripts/model/controller/ButtonAction.cs @@ -15,41 +15,43 @@ using UnityEngine; using System.Collections; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Descriptor for a button action. - /// - public enum ButtonAction { +namespace com.google.apps.peltzer.client.model.controller +{ /// - /// Null state. + /// Descriptor for a button action. /// - NONE, + public enum ButtonAction + { + /// + /// Null state. + /// + NONE, - /// - /// Button was pressed. - /// - DOWN, + /// + /// Button was pressed. + /// + DOWN, - /// - /// Button was unpressed. - /// - UP, + /// + /// Button was unpressed. + /// + UP, - /// - /// Touchpad is grazed. - /// - TOUCHPAD, + /// + /// Touchpad is grazed. + /// + TOUCHPAD, - /// - /// Primarily in service of the trigger button so that - /// an abstraction can be made to indicate a "light" trigger down. - /// - LIGHT_DOWN, + /// + /// Primarily in service of the trigger button so that + /// an abstraction can be made to indicate a "light" trigger down. + /// + LIGHT_DOWN, - /// - /// Primarily in service of the trigger button so that - /// an abstraction can be made to indicate a "light" trigger up. - /// - LIGHT_UP - } + /// + /// Primarily in service of the trigger button so that + /// an abstraction can be made to indicate a "light" trigger up. + /// + LIGHT_UP + } } diff --git a/Assets/Scripts/model/controller/ButtonId.cs b/Assets/Scripts/model/controller/ButtonId.cs index 8404ee34..e2e96cbb 100644 --- a/Assets/Scripts/model/controller/ButtonId.cs +++ b/Assets/Scripts/model/controller/ButtonId.cs @@ -15,31 +15,33 @@ using UnityEngine; using System.Collections; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Descriptor for a button action. - /// - public enum ButtonId { +namespace com.google.apps.peltzer.client.model.controller +{ /// - /// For now, these are duplicates of Valve.VR.EVRButtonId. + /// Descriptor for a button action. /// - //k_EButton_System = 0, // Unused. - ApplicationMenu = 1, - Grip = 2, - //k_EButton_DPad_Left = 3, // Unused. - //k_EButton_DPad_Up = 4, // Unused. - //k_EButton_DPad_Right = 5, // Unused. - //k_EButton_DPad_Down = 6, // Unused. - SecondaryButton = 7, // The second button on the Oculus Touch controller (B/Y) - //k_EButton_ProximitySensor = 31, // Unused. - //k_EButton_Axis0 = 32, // Unused. - //k_EButton_Axis1 = 33, // Unused. - //k_EButton_Axis2 = 34, // Unused. - //k_EButton_Axis3 = 35, // Unused. - //k_EButton_Axis4 = 36, // Unused. - Touchpad = 32, - Trigger = 33, - //k_EButton_Dashboard_Back = 2, // Unused. - //k_EButton_Max = 64, // Unused. - } + public enum ButtonId + { + /// + /// For now, these are duplicates of Valve.VR.EVRButtonId. + /// + //k_EButton_System = 0, // Unused. + ApplicationMenu = 1, + Grip = 2, + //k_EButton_DPad_Left = 3, // Unused. + //k_EButton_DPad_Up = 4, // Unused. + //k_EButton_DPad_Right = 5, // Unused. + //k_EButton_DPad_Down = 6, // Unused. + SecondaryButton = 7, // The second button on the Oculus Touch controller (B/Y) + //k_EButton_ProximitySensor = 31, // Unused. + //k_EButton_Axis0 = 32, // Unused. + //k_EButton_Axis1 = 33, // Unused. + //k_EButton_Axis2 = 34, // Unused. + //k_EButton_Axis3 = 35, // Unused. + //k_EButton_Axis4 = 36, // Unused. + Touchpad = 32, + Trigger = 33, + //k_EButton_Dashboard_Back = 2, // Unused. + //k_EButton_Max = 64, // Unused. + } } diff --git a/Assets/Scripts/model/controller/ButtonMode.cs b/Assets/Scripts/model/controller/ButtonMode.cs index 1c3aeafa..c600c769 100644 --- a/Assets/Scripts/model/controller/ButtonMode.cs +++ b/Assets/Scripts/model/controller/ButtonMode.cs @@ -12,28 +12,30 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace com.google.apps.peltzer.client.model.controller { - /// - /// The current mode of a button. - /// IMPORTANT: enum values are used as indices, so the values must be numbered sequentially from 0 - /// (default enum value assignment). - /// - public enum ButtonMode { +namespace com.google.apps.peltzer.client.model.controller +{ /// - /// Default + /// The current mode of a button. + /// IMPORTANT: enum values are used as indices, so the values must be numbered sequentially from 0 + /// (default enum value assignment). /// - NONE, - /// - /// The button is active and clickable. - /// - ACTIVE, - /// - /// The button is inactive and not clickable. - /// - INACTIVE, - /// - /// The button is active but not yet clickable. - /// - WAITING - } + public enum ButtonMode + { + /// + /// Default + /// + NONE, + /// + /// The button is active and clickable. + /// + ACTIVE, + /// + /// The button is inactive and not clickable. + /// + INACTIVE, + /// + /// The button is active but not yet clickable. + /// + WAITING + } } diff --git a/Assets/Scripts/model/controller/ChangeMaterialMenuItem.cs b/Assets/Scripts/model/controller/ChangeMaterialMenuItem.cs index d838ef86..cd0790ad 100644 --- a/Assets/Scripts/model/controller/ChangeMaterialMenuItem.cs +++ b/Assets/Scripts/model/controller/ChangeMaterialMenuItem.cs @@ -15,233 +15,271 @@ using UnityEngine; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.controller { - - /// - /// SelectableMenuItem that can be attached to a palette to change the current material. This represents one - /// colour swatch (square) on the colour-picker palette. - /// It is expected that these are configured in the Unity Editor with local: - /// - y position of DEFAULT_Y_POSITION - /// - y scale of DEFAULT_Y_SCALE - /// - public class ChangeMaterialMenuItem : SelectableMenuItem { - public int materialId; - - private readonly float BUMPED_Y_POSITION = 0.001f; - private readonly float BUMPED_Y_SCALE = 0.0025f; - - private readonly float HOVERED_Y_POSITION = -0.0025f; - private readonly float HOVERED_Y_SCALE = 0.01f; - - private readonly float BUMP_DURATION = 0.1f; - private readonly float HOVER_DURATION = 0.25f; - private readonly float RIPPLE_DURATION = 0.3f; // How long it takes to ripple out, or ripple in, a swatch. - private readonly float MAX_RIPPLE_HOLD_DURATION = 0.23f; // How long a swatch sticks out after rippling out. - - // Ripple animations. - private bool isRipplingOut; - - private struct RippleParams { - public Vector3 localPosition; - public Vector3 localScale; - public float initialDelay; - public float reverseDelay; - - public RippleParams(Vector3 localPosition, Vector3 localScale, float initialDelay, float reverseDelay) { - this.localPosition = localPosition; - this.localScale = localScale; - this.initialDelay = initialDelay; - this.reverseDelay = reverseDelay; - } - } +namespace com.google.apps.peltzer.client.model.controller +{ - private RippleParams currentRippleParams; - private Vector3 defaultLocalPosition; - private Vector3 defaultLocalScale; - private bool isBumping = false; - public bool isHovered = false; - private Vector3? targetPosition = null; - private Vector3? targetScale = null; - private float timeStartedLerping; - private float currentLerpDuration; - - void Start() { - defaultLocalPosition = transform.localPosition; - defaultLocalScale = transform.localScale; - } + /// + /// SelectableMenuItem that can be attached to a palette to change the current material. This represents one + /// colour swatch (square) on the colour-picker palette. + /// It is expected that these are configured in the Unity Editor with local: + /// - y position of DEFAULT_Y_POSITION + /// - y scale of DEFAULT_Y_SCALE + /// + public class ChangeMaterialMenuItem : SelectableMenuItem + { + public int materialId; + + private readonly float BUMPED_Y_POSITION = 0.001f; + private readonly float BUMPED_Y_SCALE = 0.0025f; + + private readonly float HOVERED_Y_POSITION = -0.0025f; + private readonly float HOVERED_Y_SCALE = 0.01f; - private RippleParams GetRippleParams() { - Vector3 rippleLocalPosition = defaultLocalPosition; - Vector3 rippleLocalScale = defaultLocalScale; - float initialRippleDelay = 0; - float reverseRippleDelay = 0; - - // Materials on the 'top' of the palette always animate up. - if (materialId <= 5) { - rippleLocalPosition += new Vector3(0f, 0f, 0.01f); - rippleLocalScale += new Vector3(0f, 0f, 0.02f); - - // Specify the delays based on the user's handedness. - if (PeltzerMain.Instance.peltzerController.handedness == Handedness.RIGHT) { - initialRippleDelay = materialId * 0.05f; - reverseRippleDelay = (5 - materialId) * 0.05f; - } else { - initialRippleDelay = (5 - materialId) * 0.05f; - reverseRippleDelay = materialId * 0.05f; + private readonly float BUMP_DURATION = 0.1f; + private readonly float HOVER_DURATION = 0.25f; + private readonly float RIPPLE_DURATION = 0.3f; // How long it takes to ripple out, or ripple in, a swatch. + private readonly float MAX_RIPPLE_HOLD_DURATION = 0.23f; // How long a swatch sticks out after rippling out. + + // Ripple animations. + private bool isRipplingOut; + + private struct RippleParams + { + public Vector3 localPosition; + public Vector3 localScale; + public float initialDelay; + public float reverseDelay; + + public RippleParams(Vector3 localPosition, Vector3 localScale, float initialDelay, float reverseDelay) + { + this.localPosition = localPosition; + this.localScale = localScale; + this.initialDelay = initialDelay; + this.reverseDelay = reverseDelay; + } } - } - - if (PeltzerMain.Instance.peltzerController.handedness == Handedness.RIGHT) { - // Materials on the 'right' of the palette animate out to the right if the user is right-handed. - if (materialId % 6 == 0) { - rippleLocalPosition += new Vector3(0.01f, 0f, 0f); - rippleLocalScale += new Vector3(0.02f, 0f, 0f); - int row = materialId / 6; - initialRippleDelay = row * 0.05f; - // Acting as if there are 6 rows, because there are 6 columns. - reverseRippleDelay = (6 - row) * 0.05f; + + private RippleParams currentRippleParams; + private Vector3 defaultLocalPosition; + private Vector3 defaultLocalScale; + private bool isBumping = false; + public bool isHovered = false; + private Vector3? targetPosition = null; + private Vector3? targetScale = null; + private float timeStartedLerping; + private float currentLerpDuration; + + void Start() + { + defaultLocalPosition = transform.localPosition; + defaultLocalScale = transform.localScale; } - } else { - // Materials on the 'left' of the palette animate out to the left if the user is left-handed. - if ((materialId + 1) % 6 == 0) { - rippleLocalPosition += new Vector3(-0.01f, 0f, 0f); - rippleLocalScale += new Vector3(0.02f, 0f, 0f); - int row = materialId / 6; - initialRippleDelay = row * 0.05f; - // Acting as if there are 6 rows, because there are 6 columns. - reverseRippleDelay = (6 - row) * 0.05f; + + private RippleParams GetRippleParams() + { + Vector3 rippleLocalPosition = defaultLocalPosition; + Vector3 rippleLocalScale = defaultLocalScale; + float initialRippleDelay = 0; + float reverseRippleDelay = 0; + + // Materials on the 'top' of the palette always animate up. + if (materialId <= 5) + { + rippleLocalPosition += new Vector3(0f, 0f, 0.01f); + rippleLocalScale += new Vector3(0f, 0f, 0.02f); + + // Specify the delays based on the user's handedness. + if (PeltzerMain.Instance.peltzerController.handedness == Handedness.RIGHT) + { + initialRippleDelay = materialId * 0.05f; + reverseRippleDelay = (5 - materialId) * 0.05f; + } + else + { + initialRippleDelay = (5 - materialId) * 0.05f; + reverseRippleDelay = materialId * 0.05f; + } + } + + if (PeltzerMain.Instance.peltzerController.handedness == Handedness.RIGHT) + { + // Materials on the 'right' of the palette animate out to the right if the user is right-handed. + if (materialId % 6 == 0) + { + rippleLocalPosition += new Vector3(0.01f, 0f, 0f); + rippleLocalScale += new Vector3(0.02f, 0f, 0f); + int row = materialId / 6; + initialRippleDelay = row * 0.05f; + // Acting as if there are 6 rows, because there are 6 columns. + reverseRippleDelay = (6 - row) * 0.05f; + } + } + else + { + // Materials on the 'left' of the palette animate out to the left if the user is left-handed. + if ((materialId + 1) % 6 == 0) + { + rippleLocalPosition += new Vector3(-0.01f, 0f, 0f); + rippleLocalScale += new Vector3(0.02f, 0f, 0f); + int row = materialId / 6; + initialRippleDelay = row * 0.05f; + // Acting as if there are 6 rows, because there are 6 columns. + reverseRippleDelay = (6 - row) * 0.05f; + } + } + + return new RippleParams(rippleLocalPosition, rippleLocalScale, initialRippleDelay, reverseRippleDelay); } - } - return new RippleParams(rippleLocalPosition, rippleLocalScale, initialRippleDelay, reverseRippleDelay); - } + /// + /// Lerps towards a target position and scale over BUMP_DURATION. If we are bumping, then when the 'bumped' + /// positions & scales are reached, reverts to the 'hovered' positions and scales. + /// + void Update() + { + if (targetPosition == null || targetScale == null) + { + return; + } - /// - /// Lerps towards a target position and scale over BUMP_DURATION. If we are bumping, then when the 'bumped' - /// positions & scales are reached, reverts to the 'hovered' positions and scales. - /// - void Update() { - if (targetPosition == null || targetScale == null) { - return; - } - - float timeDiff = Time.time - timeStartedLerping; - if (timeDiff < 0) { - // This implies we are rippling, but have not yet reached the delay threshold. - // We mess with timeStartedLerping to get the delay effect, is why this happens. - return; - } - - float pctDone = timeDiff / currentLerpDuration; - - if (pctDone >= 1) { - // If we're done, immediately set the position and scale. - gameObject.transform.localPosition = targetPosition.Value; - gameObject.transform.localScale = targetScale.Value; - if (isBumping) { - // If we're done and we were bumping down, revert to the expected position. - isBumping = false; - SetToDefaultPositionAndScale(HOVER_DURATION); - } else if (isRipplingOut) { - // If we're done and we were rippling out, then revert to the default position. - isRipplingOut = false; - SetToDefaultPositionAndScale(RIPPLE_DURATION, MAX_RIPPLE_HOLD_DURATION - currentRippleParams.reverseDelay); - } else { - // If we're done and were not bumping down, then there's no more lerping to do. - targetPosition = null; - targetScale = null; + float timeDiff = Time.time - timeStartedLerping; + if (timeDiff < 0) + { + // This implies we are rippling, but have not yet reached the delay threshold. + // We mess with timeStartedLerping to get the delay effect, is why this happens. + return; + } + + float pctDone = timeDiff / currentLerpDuration; + + if (pctDone >= 1) + { + // If we're done, immediately set the position and scale. + gameObject.transform.localPosition = targetPosition.Value; + gameObject.transform.localScale = targetScale.Value; + if (isBumping) + { + // If we're done and we were bumping down, revert to the expected position. + isBumping = false; + SetToDefaultPositionAndScale(HOVER_DURATION); + } + else if (isRipplingOut) + { + // If we're done and we were rippling out, then revert to the default position. + isRipplingOut = false; + SetToDefaultPositionAndScale(RIPPLE_DURATION, MAX_RIPPLE_HOLD_DURATION - currentRippleParams.reverseDelay); + } + else + { + // If we're done and were not bumping down, then there's no more lerping to do. + targetPosition = null; + targetScale = null; + } + } + else + { + pctDone = Mathf.SmoothStep(0f, 1f, pctDone); + + // If we're not done, lerp towards the target position and scale. + gameObject.transform.localPosition = + Vector3.Lerp(gameObject.transform.localPosition, targetPosition.Value, pctDone); + gameObject.transform.localScale = + Vector3.Lerp(gameObject.transform.localScale, targetScale.Value, pctDone); + } } - } else { - pctDone = Mathf.SmoothStep(0f, 1f, pctDone); - - // If we're not done, lerp towards the target position and scale. - gameObject.transform.localPosition = - Vector3.Lerp(gameObject.transform.localPosition, targetPosition.Value, pctDone); - gameObject.transform.localScale = - Vector3.Lerp(gameObject.transform.localScale, targetScale.Value, pctDone); - } - } - public override void ApplyMenuOptions(PeltzerMain main) { - // Return if you aren't allowed to change colors or you aren't allowed this color. - if (!PeltzerMain.Instance.restrictionManager.changingColorsAllowed - || !PeltzerMain.Instance.restrictionManager.IsColorAllowed(materialId)) { - return; - } - main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); - - // When clicked, we first update the material being used in Poly. - main.peltzerController.currentMaterial = materialId; - main.HasEverChangedColor = true; - main.peltzerController.ChangeToolColor(); - - // And we then 'bump' the swatch down slightly and back up to its position, to provide a visual indication that - // the user's click was registered. - StartBump(); - - // If the user changes colour, but was using a non-insertion tool, switch to the paintbrush. - ControllerMode currentMode = PeltzerMain.Instance.peltzerController.mode; - if (currentMode != ControllerMode.paintFace - && currentMode != ControllerMode.paintMesh - && currentMode != ControllerMode.insertVolume - && currentMode != ControllerMode.insertStroke) { - main.peltzerController.ChangeMode(ControllerMode.paintMesh, ObjectFinder.ObjectById("ID_ToolPaint")); - } - } + public override void ApplyMenuOptions(PeltzerMain main) + { + // Return if you aren't allowed to change colors or you aren't allowed this color. + if (!PeltzerMain.Instance.restrictionManager.changingColorsAllowed + || !PeltzerMain.Instance.restrictionManager.IsColorAllowed(materialId)) + { + return; + } + main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); + // When clicked, we first update the material being used in Poly. + main.peltzerController.currentMaterial = materialId; + main.HasEverChangedColor = true; + main.peltzerController.ChangeToolColor(); - /// - /// Briefly 'bump' the menu item to a middle state, before returning it to its default state. - /// Used to visually indicate to the user that a click was received. - /// - public void StartBump() { - isBumping = true; - StartLerp( - new Vector3(gameObject.transform.localPosition.x, BUMPED_Y_POSITION, gameObject.transform.localPosition.z), - new Vector3(gameObject.transform.localScale.x, BUMPED_Y_SCALE, gameObject.transform.localScale.z), - BUMP_DURATION); - } + // And we then 'bump' the swatch down slightly and back up to its position, to provide a visual indication that + // the user's click was registered. + StartBump(); - public void StartRipple() { - isRipplingOut = true; - currentRippleParams = GetRippleParams(); - StartLerp(currentRippleParams.localPosition, currentRippleParams.localScale, - RIPPLE_DURATION, currentRippleParams.initialDelay); - } + // If the user changes colour, but was using a non-insertion tool, switch to the paintbrush. + ControllerMode currentMode = PeltzerMain.Instance.peltzerController.mode; + if (currentMode != ControllerMode.paintFace + && currentMode != ControllerMode.paintMesh + && currentMode != ControllerMode.insertVolume + && currentMode != ControllerMode.insertStroke) + { + main.peltzerController.ChangeMode(ControllerMode.paintMesh, ObjectFinder.ObjectById("ID_ToolPaint")); + } + } - /// - /// Allows an external source to set whether this swatch is being hovered. - /// - public void SetHovered(bool isHovered) { - if (this.isHovered == isHovered) { - return; - } - this.isHovered = isHovered; - SetToDefaultPositionAndScale(HOVER_DURATION); - } - /// - /// Sets the position and scale to their default (non-bump-influenced) states. - /// - private void SetToDefaultPositionAndScale(float duration, float delay = 0) { - if (isHovered) { - StartLerp( - new Vector3(defaultLocalPosition.x, HOVERED_Y_POSITION, defaultLocalPosition.z), - new Vector3(defaultLocalScale.x, HOVERED_Y_SCALE, defaultLocalScale.z), - duration); - } else { - StartLerp(defaultLocalPosition, defaultLocalScale, duration, delay); - } - } + /// + /// Briefly 'bump' the menu item to a middle state, before returning it to its default state. + /// Used to visually indicate to the user that a click was received. + /// + public void StartBump() + { + isBumping = true; + StartLerp( + new Vector3(gameObject.transform.localPosition.x, BUMPED_Y_POSITION, gameObject.transform.localPosition.z), + new Vector3(gameObject.transform.localScale.x, BUMPED_Y_SCALE, gameObject.transform.localScale.z), + BUMP_DURATION); + } - /// - /// Lerps the position and scale to given states over a given duration with a given delay. - /// - private void StartLerp(Vector3 position, Vector3 scale, float duration, float delay = 0) { - targetPosition = position; - targetScale = scale; - currentLerpDuration = duration; - timeStartedLerping = Time.time + delay; + public void StartRipple() + { + isRipplingOut = true; + currentRippleParams = GetRippleParams(); + StartLerp(currentRippleParams.localPosition, currentRippleParams.localScale, + RIPPLE_DURATION, currentRippleParams.initialDelay); + } + + /// + /// Allows an external source to set whether this swatch is being hovered. + /// + public void SetHovered(bool isHovered) + { + if (this.isHovered == isHovered) + { + return; + } + this.isHovered = isHovered; + SetToDefaultPositionAndScale(HOVER_DURATION); + } + + /// + /// Sets the position and scale to their default (non-bump-influenced) states. + /// + private void SetToDefaultPositionAndScale(float duration, float delay = 0) + { + if (isHovered) + { + StartLerp( + new Vector3(defaultLocalPosition.x, HOVERED_Y_POSITION, defaultLocalPosition.z), + new Vector3(defaultLocalScale.x, HOVERED_Y_SCALE, defaultLocalScale.z), + duration); + } + else + { + StartLerp(defaultLocalPosition, defaultLocalScale, duration, delay); + } + } + + /// + /// Lerps the position and scale to given states over a given duration with a given delay. + /// + private void StartLerp(Vector3 position, Vector3 scale, float duration, float delay = 0) + { + targetPosition = position; + targetScale = scale; + currentLerpDuration = duration; + timeStartedLerping = Time.time + delay; + } } - } } diff --git a/Assets/Scripts/model/controller/ChangeModeMenuItem.cs b/Assets/Scripts/model/controller/ChangeModeMenuItem.cs index d1512091..124bc51a 100644 --- a/Assets/Scripts/model/controller/ChangeModeMenuItem.cs +++ b/Assets/Scripts/model/controller/ChangeModeMenuItem.cs @@ -17,157 +17,190 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.app; -namespace com.google.apps.peltzer.client.model.controller { - - /// - /// SelectableMenuItem that can be attached to a palette to change the current mode. - /// - public class ChangeModeMenuItem : SelectableMenuItem { - // Animation constants. - private static readonly float SCALE_CHANGE_DURATION = 0.25f; - private static readonly float POSITION_CHANGE_DURATION = 0.2f; - - // Where the toolhead should be, relative to the controller. - private readonly Vector3 TARGET_LOCAL_POSITION_VIVE = new Vector3(0.0004f, -0.0068f, -0.0037f); - private readonly Vector3 TARGET_LOCAL_POSITION_STEAM_RIFT_RIGHT = new Vector3(-.00404f, -.03399f, -.03312f); - private readonly Vector3 TARGET_LOCAL_ROTATION_STEAM_RIFT_RIGHT = new Vector3(39.194f, -4.73f, -3.063f); - private readonly Vector3 TARGET_LOCAL_POSITION_STEAM_RIFT_LEFT = new Vector3(-.0025f, -0.0325f, -.034f); - private readonly Vector3 TARGET_LOCAL_ROTATION_STEAM_RIFT_LEFT = new Vector3(39.259f, -6.693f, -5.973f); - - private readonly Vector3 TARGET_LOCAL_POSITION_OCULUS_RIGHT = new Vector3(.00031f, -.00835f, .0362f); - private readonly Vector3 TARGET_LOCAL_ROTATION_OCULUS_RIGHT = new Vector3(-5.056f,.547f,7.56f); - private readonly Vector3 TARGET_LOCAL_POSITION_OCULUS_LEFT = new Vector3(0.0015f, -0.0099f, 0.0338f); - private readonly Vector3 TARGET_LOCAL_ROTATION_OCULUS_LEFT = new Vector3(-5.9f, -2.18f, 5.889f); - - // The mode this item will change to. - public ControllerMode mode; - - // Animation details. - private Vector3? targetPosition = null; - private Vector3? targetScale = null; - private Vector3? targetRotation = null; - private float timeStartedLerping; - private float currentLerpDuration; - - public override void ApplyMenuOptions(PeltzerMain main) { - // Additionally, pass a reference to the gameobject for animation. - main.peltzerController.ChangeMode(mode, gameObject); - main.audioLibrary.PlayClip(main.audioLibrary.selectToolSound); - } +namespace com.google.apps.peltzer.client.model.controller +{ - private void Update() { - if (targetScale.HasValue) { - // Calculate progress through the animation. Values >1 may be meaningless, so this should be checked - // for below rather than blindly passed through. - float pctDone = (Time.time - timeStartedLerping) / SCALE_CHANGE_DURATION; - - if (pctDone >= 1) { - // If we're done, immediately set the scale. - transform.localScale = targetScale.Value; - targetScale = null; - } else { - // If we're not done, lerp towards the target scale. - transform.localScale = - Vector3.Lerp(gameObject.transform.localScale, targetScale.Value, pctDone); - } - } - - if (targetPosition.HasValue) { - // Calculate progress through the animation. Values >1 may be meaningless, so this should be checked - // for below rather than blindly passed through. - float pctDone = (Time.time - timeStartedLerping) / POSITION_CHANGE_DURATION; - - if (pctDone >= 1) { - // If we're done, immediately set the final position and activate any animations. - transform.localPosition = targetPosition.Value; - targetPosition = null; - ToolHeadAnimationEnd(); - } else { - // If we're not done, lerp towards the target position. - transform.localPosition = - Vector3.Lerp(gameObject.transform.localPosition, targetPosition.Value, pctDone); + /// + /// SelectableMenuItem that can be attached to a palette to change the current mode. + /// + public class ChangeModeMenuItem : SelectableMenuItem + { + // Animation constants. + private static readonly float SCALE_CHANGE_DURATION = 0.25f; + private static readonly float POSITION_CHANGE_DURATION = 0.2f; + + // Where the toolhead should be, relative to the controller. + private readonly Vector3 TARGET_LOCAL_POSITION_VIVE = new Vector3(0.0004f, -0.0068f, -0.0037f); + private readonly Vector3 TARGET_LOCAL_POSITION_STEAM_RIFT_RIGHT = new Vector3(-.00404f, -.03399f, -.03312f); + private readonly Vector3 TARGET_LOCAL_ROTATION_STEAM_RIFT_RIGHT = new Vector3(39.194f, -4.73f, -3.063f); + private readonly Vector3 TARGET_LOCAL_POSITION_STEAM_RIFT_LEFT = new Vector3(-.0025f, -0.0325f, -.034f); + private readonly Vector3 TARGET_LOCAL_ROTATION_STEAM_RIFT_LEFT = new Vector3(39.259f, -6.693f, -5.973f); + + private readonly Vector3 TARGET_LOCAL_POSITION_OCULUS_RIGHT = new Vector3(.00031f, -.00835f, .0362f); + private readonly Vector3 TARGET_LOCAL_ROTATION_OCULUS_RIGHT = new Vector3(-5.056f, .547f, 7.56f); + private readonly Vector3 TARGET_LOCAL_POSITION_OCULUS_LEFT = new Vector3(0.0015f, -0.0099f, 0.0338f); + private readonly Vector3 TARGET_LOCAL_ROTATION_OCULUS_LEFT = new Vector3(-5.9f, -2.18f, 5.889f); + + // The mode this item will change to. + public ControllerMode mode; + + // Animation details. + private Vector3? targetPosition = null; + private Vector3? targetScale = null; + private Vector3? targetRotation = null; + private float timeStartedLerping; + private float currentLerpDuration; + + public override void ApplyMenuOptions(PeltzerMain main) + { + // Additionally, pass a reference to the gameobject for animation. + main.peltzerController.ChangeMode(mode, gameObject); + main.audioLibrary.PlayClip(main.audioLibrary.selectToolSound); } - } - - if (targetRotation.HasValue) { - // Calculate progress through the animation. Values >1 may be meaningless, so this should be checked - // for below rather than blindly passed through. - float pctDone = (Time.time - timeStartedLerping) / POSITION_CHANGE_DURATION; - - if (pctDone >= 1) { - // If we're done, immediately set the final rotation and activate any animations. - transform.localRotation = Quaternion.Euler(targetRotation.Value); - targetRotation = null; - } else { - // If we're not done, lerp towards the target rotation. - transform.localRotation = - Quaternion.Lerp(gameObject.transform.localRotation, Quaternion.Euler(targetRotation.Value), pctDone); + + private void Update() + { + if (targetScale.HasValue) + { + // Calculate progress through the animation. Values >1 may be meaningless, so this should be checked + // for below rather than blindly passed through. + float pctDone = (Time.time - timeStartedLerping) / SCALE_CHANGE_DURATION; + + if (pctDone >= 1) + { + // If we're done, immediately set the scale. + transform.localScale = targetScale.Value; + targetScale = null; + } + else + { + // If we're not done, lerp towards the target scale. + transform.localScale = + Vector3.Lerp(gameObject.transform.localScale, targetScale.Value, pctDone); + } + } + + if (targetPosition.HasValue) + { + // Calculate progress through the animation. Values >1 may be meaningless, so this should be checked + // for below rather than blindly passed through. + float pctDone = (Time.time - timeStartedLerping) / POSITION_CHANGE_DURATION; + + if (pctDone >= 1) + { + // If we're done, immediately set the final position and activate any animations. + transform.localPosition = targetPosition.Value; + targetPosition = null; + ToolHeadAnimationEnd(); + } + else + { + // If we're not done, lerp towards the target position. + transform.localPosition = + Vector3.Lerp(gameObject.transform.localPosition, targetPosition.Value, pctDone); + } + } + + if (targetRotation.HasValue) + { + // Calculate progress through the animation. Values >1 may be meaningless, so this should be checked + // for below rather than blindly passed through. + float pctDone = (Time.time - timeStartedLerping) / POSITION_CHANGE_DURATION; + + if (pctDone >= 1) + { + // If we're done, immediately set the final rotation and activate any animations. + transform.localRotation = Quaternion.Euler(targetRotation.Value); + targetRotation = null; + } + else + { + // If we're not done, lerp towards the target rotation. + transform.localRotation = + Quaternion.Lerp(gameObject.transform.localRotation, Quaternion.Euler(targetRotation.Value), pctDone); + } + } } - } - } - /// - /// Moves this toolhead smoothly from the palette to the controller. - /// - public void BringToController() { - // Abandon the scale animation if we're activating a move animation. - if (targetScale.HasValue) { - transform.localScale = targetScale.Value; - targetScale = null; - } - - timeStartedLerping = Time.time; - if (Config.Instance.VrHardware == VrHardware.Vive) { - targetPosition = TARGET_LOCAL_POSITION_VIVE; - } else if (Config.Instance.VrHardware == VrHardware.Rift) { - if (Config.Instance.sdkMode == SdkMode.SteamVR) { - if (PeltzerMain.Instance.peltzerControllerInRightHand) { - targetPosition = TARGET_LOCAL_POSITION_STEAM_RIFT_RIGHT; - targetRotation = TARGET_LOCAL_ROTATION_STEAM_RIFT_RIGHT; - } else { - targetPosition = TARGET_LOCAL_POSITION_STEAM_RIFT_LEFT; - targetRotation = TARGET_LOCAL_ROTATION_STEAM_RIFT_LEFT; - } - } else { - if (PeltzerMain.Instance.peltzerControllerInRightHand) { - targetPosition = TARGET_LOCAL_POSITION_OCULUS_RIGHT; - targetRotation = TARGET_LOCAL_ROTATION_OCULUS_RIGHT; - } else { - targetPosition = TARGET_LOCAL_POSITION_OCULUS_LEFT; - targetRotation = TARGET_LOCAL_ROTATION_OCULUS_LEFT; - } + /// + /// Moves this toolhead smoothly from the palette to the controller. + /// + public void BringToController() + { + // Abandon the scale animation if we're activating a move animation. + if (targetScale.HasValue) + { + transform.localScale = targetScale.Value; + targetScale = null; + } + + timeStartedLerping = Time.time; + if (Config.Instance.VrHardware == VrHardware.Vive) + { + targetPosition = TARGET_LOCAL_POSITION_VIVE; + } + else if (Config.Instance.VrHardware == VrHardware.Rift) + { + if (Config.Instance.sdkMode == SdkMode.SteamVR) + { + if (PeltzerMain.Instance.peltzerControllerInRightHand) + { + targetPosition = TARGET_LOCAL_POSITION_STEAM_RIFT_RIGHT; + targetRotation = TARGET_LOCAL_ROTATION_STEAM_RIFT_RIGHT; + } + else + { + targetPosition = TARGET_LOCAL_POSITION_STEAM_RIFT_LEFT; + targetRotation = TARGET_LOCAL_ROTATION_STEAM_RIFT_LEFT; + } + } + else + { + if (PeltzerMain.Instance.peltzerControllerInRightHand) + { + targetPosition = TARGET_LOCAL_POSITION_OCULUS_RIGHT; + targetRotation = TARGET_LOCAL_ROTATION_OCULUS_RIGHT; + } + else + { + targetPosition = TARGET_LOCAL_POSITION_OCULUS_LEFT; + targetRotation = TARGET_LOCAL_ROTATION_OCULUS_LEFT; + } + } + } } - } - } - /// - /// Scale up this toolhead to make it appear from thin air. Used when a toolhead is re-added to the palette. - /// - public void ScaleFromNothing(Vector3 finalScale) { - transform.localScale = Vector3.zero; - targetScale = finalScale; - timeStartedLerping = Time.time; - } + /// + /// Scale up this toolhead to make it appear from thin air. Used when a toolhead is re-added to the palette. + /// + public void ScaleFromNothing(Vector3 finalScale) + { + transform.localScale = Vector3.zero; + targetScale = finalScale; + timeStartedLerping = Time.time; + } - /// - /// Activates animations for the toolhead once it is attached to the controller. - /// - private void ToolHeadAnimationEnd() { - switch (mode) { - case ControllerMode.extrude: - case ControllerMode.reshape: - case ControllerMode.subdivideFace: - case ControllerMode.subdivideMesh: - GetComponent().Activate(); - break; - case ControllerMode.move: - GetComponent().Activate(); - break; - case ControllerMode.paintFace: - case ControllerMode.paintMesh: - GetComponent().Activate(); - break; - } + /// + /// Activates animations for the toolhead once it is attached to the controller. + /// + private void ToolHeadAnimationEnd() + { + switch (mode) + { + case ControllerMode.extrude: + case ControllerMode.reshape: + case ControllerMode.subdivideFace: + case ControllerMode.subdivideMesh: + GetComponent().Activate(); + break; + case ControllerMode.move: + GetComponent().Activate(); + break; + case ControllerMode.paintFace: + case ControllerMode.paintMesh: + GetComponent().Activate(); + break; + } + } } - } } diff --git a/Assets/Scripts/model/controller/ControllerDevice.cs b/Assets/Scripts/model/controller/ControllerDevice.cs index 67e79262..18d91967 100644 --- a/Assets/Scripts/model/controller/ControllerDevice.cs +++ b/Assets/Scripts/model/controller/ControllerDevice.cs @@ -14,41 +14,43 @@ using UnityEngine; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Abstract wrapper for controller SDKs, to be implemented for Steam and Oculus. - /// - public interface ControllerDevice { - // Note that this is not a MonoBehavior, its Update must be called from one. - void Update(); - - // Get/Set whether this controller is in a valid state (if not, none of the methods below are meaningful). - bool IsTrackedObjectValid { get; set; } - - // Get the current velocity of this controller. - Vector3 GetVelocity(); - - // Check whether a given button is pressed, or has just been pressed/released. - bool IsPressed(ButtonId buttonId); - bool WasJustPressed(ButtonId buttonId); - bool WasJustReleased(ButtonId buttonId); - - // Check whether the trigger is half pressed or was just released from a half press. - bool IsTriggerHalfPressed(); - bool WasTriggerJustReleasedFromHalfPress(); - - // Check whether a given button is touched. - // Only valid where the given button supports capacative inputs in the hardware being used. - bool IsTouched(ButtonId buttonId); - - // Get the position of the user's thumb on the touchpad (Vive) or stick (Rift), as a vector or a TouchpadLocation. - Vector2 GetDirectionalAxis(); - TouchpadLocation GetTouchpadLocation(); - - // Get the position of the trigger. - Vector2 GetTriggerScale(); - - // Trigger a haptic pulse of the given duration. - void TriggerHapticPulse(ushort durationMicroSec = 500); - } +namespace com.google.apps.peltzer.client.model.controller +{ + /// + /// Abstract wrapper for controller SDKs, to be implemented for Steam and Oculus. + /// + public interface ControllerDevice + { + // Note that this is not a MonoBehavior, its Update must be called from one. + void Update(); + + // Get/Set whether this controller is in a valid state (if not, none of the methods below are meaningful). + bool IsTrackedObjectValid { get; set; } + + // Get the current velocity of this controller. + Vector3 GetVelocity(); + + // Check whether a given button is pressed, or has just been pressed/released. + bool IsPressed(ButtonId buttonId); + bool WasJustPressed(ButtonId buttonId); + bool WasJustReleased(ButtonId buttonId); + + // Check whether the trigger is half pressed or was just released from a half press. + bool IsTriggerHalfPressed(); + bool WasTriggerJustReleasedFromHalfPress(); + + // Check whether a given button is touched. + // Only valid where the given button supports capacative inputs in the hardware being used. + bool IsTouched(ButtonId buttonId); + + // Get the position of the user's thumb on the touchpad (Vive) or stick (Rift), as a vector or a TouchpadLocation. + Vector2 GetDirectionalAxis(); + TouchpadLocation GetTouchpadLocation(); + + // Get the position of the trigger. + Vector2 GetTriggerScale(); + + // Trigger a haptic pulse of the given duration. + void TriggerHapticPulse(ushort durationMicroSec = 500); + } } \ No newline at end of file diff --git a/Assets/Scripts/model/controller/ControllerDeviceOculus.cs b/Assets/Scripts/model/controller/ControllerDeviceOculus.cs index b5387aec..cb32d335 100644 --- a/Assets/Scripts/model/controller/ControllerDeviceOculus.cs +++ b/Assets/Scripts/model/controller/ControllerDeviceOculus.cs @@ -14,270 +14,304 @@ using UnityEngine; -namespace com.google.apps.peltzer.client.model.controller { - class ControllerDeviceOculus : ControllerDevice { - private const float LIGHT_TRIGGER_PULL_THRESHOLD = 0.01f; - // The most-recent thumbstick location. - private Vector2 currentPad = Vector2.zero; - - // Which controller is this (Touch Left, Touch Right)? - public OVRInput.Controller controllerType = OVRInput.Controller.None; - - // An Oculus controller's validity is determined in OculusHandTrackingManager. - private bool isValid; - private bool wasValidOnLastUpdate; - public bool IsTrackedObjectValid { get { return isValid && OVRManager.hasVrFocus; } set { isValid = value; } } - - // We must manually track velocity in the Oculus SDK. - private Transform transform; - private Vector3 worldPositionOnLastUpdate; - private Vector3 velocity; - - // We must manually track button releases in the Oculus SDK. - private bool triggerPressed; - private bool gripPressed; - private bool secondaryButtonPressed; - private bool applicationButtonPressed; - private bool touchpadPressed; - private bool triggerHalfPressed; - private bool triggerWasPressedOnLastUpdate; - private bool gripWasPressedOnLastUpdate; - private bool secondaryButtonWasPressedOnLastUpdate; - private bool applicationButtonWasPressedOnLastUpdate; - private bool touchpadWasPressedOnLastUpdate; - private bool triggerWasHalfPressedOnLastUpdate; - - // Haptics. - private OVRHapticsClip rumbleHapticsClip; - private AudioClip rumbleClip; - - // Constructor, taking in a transform such that it can be regularly updated. - public ControllerDeviceOculus(Transform transform) { - this.transform = transform; - if (rumbleClip != null) { - rumbleHapticsClip = new OVRHapticsClip(rumbleClip); - } - } +namespace com.google.apps.peltzer.client.model.controller +{ + class ControllerDeviceOculus : ControllerDevice + { + private const float LIGHT_TRIGGER_PULL_THRESHOLD = 0.01f; + // The most-recent thumbstick location. + private Vector2 currentPad = Vector2.zero; + + // Which controller is this (Touch Left, Touch Right)? + public OVRInput.Controller controllerType = OVRInput.Controller.None; + + // An Oculus controller's validity is determined in OculusHandTrackingManager. + private bool isValid; + private bool wasValidOnLastUpdate; + public bool IsTrackedObjectValid { get { return isValid && OVRManager.hasVrFocus; } set { isValid = value; } } + + // We must manually track velocity in the Oculus SDK. + private Transform transform; + private Vector3 worldPositionOnLastUpdate; + private Vector3 velocity; + + // We must manually track button releases in the Oculus SDK. + private bool triggerPressed; + private bool gripPressed; + private bool secondaryButtonPressed; + private bool applicationButtonPressed; + private bool touchpadPressed; + private bool triggerHalfPressed; + private bool triggerWasPressedOnLastUpdate; + private bool gripWasPressedOnLastUpdate; + private bool secondaryButtonWasPressedOnLastUpdate; + private bool applicationButtonWasPressedOnLastUpdate; + private bool touchpadWasPressedOnLastUpdate; + private bool triggerWasHalfPressedOnLastUpdate; + + // Haptics. + private OVRHapticsClip rumbleHapticsClip; + private AudioClip rumbleClip; + + // Constructor, taking in a transform such that it can be regularly updated. + public ControllerDeviceOculus(Transform transform) + { + this.transform = transform; + if (rumbleClip != null) + { + rumbleHapticsClip = new OVRHapticsClip(rumbleClip); + } + } - // Update loop (to be called manually, this is not a MonoBehavior). - public void Update() { - if (!isValid) { - // In an invalid state, nothing is pressed. - triggerPressed = false; - gripPressed = false; - secondaryButtonPressed = false; - applicationButtonPressed = false; - touchpadPressed = false; - velocity = Vector3.zero; - currentPad = Vector2.zero; - - // Return before calculating releases, and without updating any 'previous state' variables. - return; - } - - // Update the latest thumbstick location, if possible. - currentPad = OVRInput.Get(OVRInput.Axis2D.PrimaryThumbstick, controllerType); - - // Update velocity only when we have two subsequent valid updates. - if (wasValidOnLastUpdate) { - velocity = (transform.position - worldPositionOnLastUpdate) / Time.deltaTime; - } else { - velocity = Vector3.zero; - } - - // Update 'previous state' variables. - triggerWasPressedOnLastUpdate = triggerPressed; - gripWasPressedOnLastUpdate = gripPressed; - secondaryButtonWasPressedOnLastUpdate = secondaryButtonPressed; - applicationButtonWasPressedOnLastUpdate = applicationButtonPressed; - touchpadWasPressedOnLastUpdate = touchpadPressed; - worldPositionOnLastUpdate = transform.position; - wasValidOnLastUpdate = isValid; - triggerWasHalfPressedOnLastUpdate = triggerHalfPressed; - - - // Find which buttons are currently pressed. - triggerPressed = IsPressedInternal(ButtonId.Trigger); - gripPressed = IsPressedInternal(ButtonId.Grip); - secondaryButtonPressed = IsPressedInternal(ButtonId.SecondaryButton); - applicationButtonPressed = IsPressedInternal(ButtonId.ApplicationMenu); - touchpadPressed = IsPressedInternal(ButtonId.Touchpad); - triggerHalfPressed = IsTriggerHalfPressedInternal(); - } + // Update loop (to be called manually, this is not a MonoBehavior). + public void Update() + { + if (!isValid) + { + // In an invalid state, nothing is pressed. + triggerPressed = false; + gripPressed = false; + secondaryButtonPressed = false; + applicationButtonPressed = false; + touchpadPressed = false; + velocity = Vector3.zero; + currentPad = Vector2.zero; + + // Return before calculating releases, and without updating any 'previous state' variables. + return; + } + + // Update the latest thumbstick location, if possible. + currentPad = OVRInput.Get(OVRInput.Axis2D.PrimaryThumbstick, controllerType); + + // Update velocity only when we have two subsequent valid updates. + if (wasValidOnLastUpdate) + { + velocity = (transform.position - worldPositionOnLastUpdate) / Time.deltaTime; + } + else + { + velocity = Vector3.zero; + } + + // Update 'previous state' variables. + triggerWasPressedOnLastUpdate = triggerPressed; + gripWasPressedOnLastUpdate = gripPressed; + secondaryButtonWasPressedOnLastUpdate = secondaryButtonPressed; + applicationButtonWasPressedOnLastUpdate = applicationButtonPressed; + touchpadWasPressedOnLastUpdate = touchpadPressed; + worldPositionOnLastUpdate = transform.position; + wasValidOnLastUpdate = isValid; + triggerWasHalfPressedOnLastUpdate = triggerHalfPressed; + + + // Find which buttons are currently pressed. + triggerPressed = IsPressedInternal(ButtonId.Trigger); + gripPressed = IsPressedInternal(ButtonId.Grip); + secondaryButtonPressed = IsPressedInternal(ButtonId.SecondaryButton); + applicationButtonPressed = IsPressedInternal(ButtonId.ApplicationMenu); + touchpadPressed = IsPressedInternal(ButtonId.Touchpad); + triggerHalfPressed = IsTriggerHalfPressedInternal(); + } - // A mapping from ButtonId to OvrInput Button. - private static bool OvrButtonFromButtonId(ButtonId buttonId, out OVRInput.Button ovrButton) { - switch (buttonId) { - case ButtonId.ApplicationMenu: - ovrButton = OVRInput.Button.One; - return true; - case ButtonId.Touchpad: - ovrButton = OVRInput.Button.PrimaryThumbstick; - return true; - case ButtonId.Trigger: - ovrButton = OVRInput.Button.PrimaryIndexTrigger; - return true; - case ButtonId.Grip: - ovrButton = OVRInput.Button.PrimaryHandTrigger; - return true; - case ButtonId.SecondaryButton: - ovrButton = OVRInput.Button.Two; - return true; - } - - ovrButton = OVRInput.Button.Any; - return false; - } + // A mapping from ButtonId to OvrInput Button. + private static bool OvrButtonFromButtonId(ButtonId buttonId, out OVRInput.Button ovrButton) + { + switch (buttonId) + { + case ButtonId.ApplicationMenu: + ovrButton = OVRInput.Button.One; + return true; + case ButtonId.Touchpad: + ovrButton = OVRInput.Button.PrimaryThumbstick; + return true; + case ButtonId.Trigger: + ovrButton = OVRInput.Button.PrimaryIndexTrigger; + return true; + case ButtonId.Grip: + ovrButton = OVRInput.Button.PrimaryHandTrigger; + return true; + case ButtonId.SecondaryButton: + ovrButton = OVRInput.Button.Two; + return true; + } + + ovrButton = OVRInput.Button.Any; + return false; + } - // A mapping from ButtonId to OvrInput Touch. - private static bool OvrTouchFromButtonId(ButtonId buttonId, out OVRInput.Touch ovrTouch) { - switch (buttonId) { - case ButtonId.ApplicationMenu: - ovrTouch = OVRInput.Touch.One; - return true; - case ButtonId.Touchpad: - ovrTouch = OVRInput.Touch.PrimaryThumbstick; - return true; - case ButtonId.Trigger: - ovrTouch = OVRInput.Touch.PrimaryIndexTrigger; - return true; - case ButtonId.SecondaryButton: - ovrTouch = OVRInput.Touch.Two; - return true; - } - - ovrTouch = OVRInput.Touch.Any; - return false; - } + // A mapping from ButtonId to OvrInput Touch. + private static bool OvrTouchFromButtonId(ButtonId buttonId, out OVRInput.Touch ovrTouch) + { + switch (buttonId) + { + case ButtonId.ApplicationMenu: + ovrTouch = OVRInput.Touch.One; + return true; + case ButtonId.Touchpad: + ovrTouch = OVRInput.Touch.PrimaryThumbstick; + return true; + case ButtonId.Trigger: + ovrTouch = OVRInput.Touch.PrimaryIndexTrigger; + return true; + case ButtonId.SecondaryButton: + ovrTouch = OVRInput.Touch.Two; + return true; + } + + ovrTouch = OVRInput.Touch.Any; + return false; + } - private bool IsPressedInternal(ButtonId buttonId) { - if (!isValid) return false; - - OVRInput.Button ovrButton; - if (!OvrButtonFromButtonId(buttonId, out ovrButton)) return false; - - // The Touch thumbstick is considered 'pressed' is it is in one of the far quadrants, or if it is in the center - // and has actually been depressed. This allows users to simply flick the thumbstick to choose an option, rather - // than having to move and press-in the thumbstick, which is tiresome. - if (buttonId == ButtonId.Touchpad) { - TouchpadLocation touchpadLocation = GetTouchpadLocation(); - return (touchpadLocation == TouchpadLocation.CENTER && OVRInput.Get(ovrButton, controllerType)) || - (touchpadLocation != TouchpadLocation.CENTER && touchpadLocation != TouchpadLocation.NONE); - } else { - return OVRInput.Get(ovrButton, controllerType); - } - } + private bool IsPressedInternal(ButtonId buttonId) + { + if (!isValid) return false; + + OVRInput.Button ovrButton; + if (!OvrButtonFromButtonId(buttonId, out ovrButton)) return false; + + // The Touch thumbstick is considered 'pressed' is it is in one of the far quadrants, or if it is in the center + // and has actually been depressed. This allows users to simply flick the thumbstick to choose an option, rather + // than having to move and press-in the thumbstick, which is tiresome. + if (buttonId == ButtonId.Touchpad) + { + TouchpadLocation touchpadLocation = GetTouchpadLocation(); + return (touchpadLocation == TouchpadLocation.CENTER && OVRInput.Get(ovrButton, controllerType)) || + (touchpadLocation != TouchpadLocation.CENTER && touchpadLocation != TouchpadLocation.NONE); + } + else + { + return OVRInput.Get(ovrButton, controllerType); + } + } - private bool IsTriggerHalfPressedInternal() { - if (!isValid) return false; + private bool IsTriggerHalfPressedInternal() + { + if (!isValid) return false; - // Only record as half pressed if the trigger is not pressed. - return OVRInput.Get(OVRInput.Axis1D.PrimaryIndexTrigger) >= LIGHT_TRIGGER_PULL_THRESHOLD - && !triggerPressed; - } + // Only record as half pressed if the trigger is not pressed. + return OVRInput.Get(OVRInput.Axis1D.PrimaryIndexTrigger) >= LIGHT_TRIGGER_PULL_THRESHOLD + && !triggerPressed; + } - // Interface method implementations begin. + // Interface method implementations begin. - public Vector3 GetVelocity() { - return velocity; - } + public Vector3 GetVelocity() + { + return velocity; + } - public bool IsPressed(ButtonId buttonId) { - if (!isValid) return false; - - switch (buttonId) { - case ButtonId.ApplicationMenu: - return applicationButtonPressed; - case ButtonId.Touchpad: - return touchpadPressed; - case ButtonId.Trigger: - return triggerPressed; - case ButtonId.Grip: - return gripPressed; - case ButtonId.SecondaryButton: - return secondaryButtonPressed; - } - - return false; - } + public bool IsPressed(ButtonId buttonId) + { + if (!isValid) return false; + + switch (buttonId) + { + case ButtonId.ApplicationMenu: + return applicationButtonPressed; + case ButtonId.Touchpad: + return touchpadPressed; + case ButtonId.Trigger: + return triggerPressed; + case ButtonId.Grip: + return gripPressed; + case ButtonId.SecondaryButton: + return secondaryButtonPressed; + } + + return false; + } - public bool IsTriggerHalfPressed() { - if (!isValid) return false; - return triggerHalfPressed; - } + public bool IsTriggerHalfPressed() + { + if (!isValid) return false; + return triggerHalfPressed; + } - public bool WasTriggerJustReleasedFromHalfPress() { - if (!isValid) return false; - return !triggerHalfPressed && !triggerPressed && triggerWasHalfPressedOnLastUpdate; - } + public bool WasTriggerJustReleasedFromHalfPress() + { + if (!isValid) return false; + return !triggerHalfPressed && !triggerPressed && triggerWasHalfPressedOnLastUpdate; + } - public bool WasJustPressed(ButtonId buttonId) { - if (!isValid) return false; - - switch (buttonId) { - case ButtonId.ApplicationMenu: - return applicationButtonPressed && !applicationButtonWasPressedOnLastUpdate; - case ButtonId.Touchpad: - return touchpadPressed && !touchpadWasPressedOnLastUpdate; - case ButtonId.Trigger: - return triggerPressed && !triggerWasPressedOnLastUpdate; - case ButtonId.Grip: - return gripPressed && !gripWasPressedOnLastUpdate; - case ButtonId.SecondaryButton: - return secondaryButtonPressed && !secondaryButtonWasPressedOnLastUpdate; - } - - return false; - } + public bool WasJustPressed(ButtonId buttonId) + { + if (!isValid) return false; + + switch (buttonId) + { + case ButtonId.ApplicationMenu: + return applicationButtonPressed && !applicationButtonWasPressedOnLastUpdate; + case ButtonId.Touchpad: + return touchpadPressed && !touchpadWasPressedOnLastUpdate; + case ButtonId.Trigger: + return triggerPressed && !triggerWasPressedOnLastUpdate; + case ButtonId.Grip: + return gripPressed && !gripWasPressedOnLastUpdate; + case ButtonId.SecondaryButton: + return secondaryButtonPressed && !secondaryButtonWasPressedOnLastUpdate; + } + + return false; + } - public bool WasJustReleased(ButtonId buttonId) { - if (!isValid) return false; - - switch (buttonId) { - case ButtonId.ApplicationMenu: - return !applicationButtonPressed && applicationButtonWasPressedOnLastUpdate; - case ButtonId.Touchpad: - return !touchpadPressed && touchpadWasPressedOnLastUpdate; - case ButtonId.Trigger: - return !triggerPressed && triggerWasPressedOnLastUpdate; - case ButtonId.Grip: - return !gripPressed && gripWasPressedOnLastUpdate; - case ButtonId.SecondaryButton: - return !secondaryButtonPressed && secondaryButtonWasPressedOnLastUpdate; - } - - return false; - } + public bool WasJustReleased(ButtonId buttonId) + { + if (!isValid) return false; + + switch (buttonId) + { + case ButtonId.ApplicationMenu: + return !applicationButtonPressed && applicationButtonWasPressedOnLastUpdate; + case ButtonId.Touchpad: + return !touchpadPressed && touchpadWasPressedOnLastUpdate; + case ButtonId.Trigger: + return !triggerPressed && triggerWasPressedOnLastUpdate; + case ButtonId.Grip: + return !gripPressed && gripWasPressedOnLastUpdate; + case ButtonId.SecondaryButton: + return !secondaryButtonPressed && secondaryButtonWasPressedOnLastUpdate; + } + + return false; + } - public bool IsTouched(ButtonId buttonId) { - if (!isValid) return false; - OVRInput.Touch ovrTouch; - if (!OvrTouchFromButtonId(buttonId, out ovrTouch)) return false; - return OVRInput.Get(ovrTouch, controllerType); - } + public bool IsTouched(ButtonId buttonId) + { + if (!isValid) return false; + OVRInput.Touch ovrTouch; + if (!OvrTouchFromButtonId(buttonId, out ovrTouch)) return false; + return OVRInput.Get(ovrTouch, controllerType); + } - public Vector2 GetDirectionalAxis() { - return currentPad; - } + public Vector2 GetDirectionalAxis() + { + return currentPad; + } - public TouchpadLocation GetTouchpadLocation() { - return TouchpadLocationHelper.GetTouchpadLocation(currentPad); - } + public TouchpadLocation GetTouchpadLocation() + { + return TouchpadLocationHelper.GetTouchpadLocation(currentPad); + } - public Vector2 GetTriggerScale() { - return new Vector2(OVRInput.Get(OVRInput.Axis1D.PrimaryIndexTrigger, controllerType), 0); - } + public Vector2 GetTriggerScale() + { + return new Vector2(OVRInput.Get(OVRInput.Axis1D.PrimaryIndexTrigger, controllerType), 0); + } - public void TriggerHapticPulse(ushort durationMicroSec = 500) { - float length = durationMicroSec / 1000000f; - var channel = controllerType == OVRInput.Controller.LTouch ? OVRHaptics.LeftChannel : OVRHaptics.RightChannel; - if (rumbleHapticsClip != null) { - int count = (int)(length / rumbleClip.length); - channel.Preempt(rumbleHapticsClip); - for (int i = 1; i < count; i++) { - channel.Queue(rumbleHapticsClip); + public void TriggerHapticPulse(ushort durationMicroSec = 500) + { + float length = durationMicroSec / 1000000f; + var channel = controllerType == OVRInput.Controller.LTouch ? OVRHaptics.LeftChannel : OVRHaptics.RightChannel; + if (rumbleHapticsClip != null) + { + int count = (int)(length / rumbleClip.length); + channel.Preempt(rumbleHapticsClip); + for (int i = 1; i < count; i++) + { + channel.Queue(rumbleHapticsClip); + } + } } - } } - } } diff --git a/Assets/Scripts/model/controller/ControllerDeviceSteam.cs b/Assets/Scripts/model/controller/ControllerDeviceSteam.cs index 93615a0a..068e82c0 100644 --- a/Assets/Scripts/model/controller/ControllerDeviceSteam.cs +++ b/Assets/Scripts/model/controller/ControllerDeviceSteam.cs @@ -18,262 +18,298 @@ using com.google.apps.peltzer.client.app; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Controller SDK logic for Steam. Supports both Vive and Rift+Touch hardware. - /// +namespace com.google.apps.peltzer.client.model.controller +{ + /// + /// Controller SDK logic for Steam. Supports both Vive and Rift+Touch hardware. + /// #if STEAMVRBUILD - public class ControllerDeviceSteam : ControllerDevice { - // With latest update to SteamVR (12/9/16), Oculus Touch B/Y are now supported. The plugin doesn't have - // ButtonMask added to support this. - public const ulong ButtonMask_Button01 = (1ul << (int)Valve.VR.EVRButtonId.k_EButton_A); - public const ulong ButtonMask_Button02 = (1ul << (int)Valve.VR.EVRButtonId.k_EButton_ApplicationMenu); - private const float LIGHT_TRIGGER_PULL_THRESHOLD = 0.01f; - - // The TrackedObject for this controller, and its index. - private readonly SteamVR_TrackedObject trackedObject; - private int Index { - get { return (int)trackedObject.index; } - } + public class ControllerDeviceSteam : ControllerDevice + { + // With latest update to SteamVR (12/9/16), Oculus Touch B/Y are now supported. The plugin doesn't have + // ButtonMask added to support this. + public const ulong ButtonMask_Button01 = (1ul << (int)Valve.VR.EVRButtonId.k_EButton_A); + public const ulong ButtonMask_Button02 = (1ul << (int)Valve.VR.EVRButtonId.k_EButton_ApplicationMenu); + private const float LIGHT_TRIGGER_PULL_THRESHOLD = 0.01f; + + // The TrackedObject for this controller, and its index. + private readonly SteamVR_TrackedObject trackedObject; + private int Index + { + get { return (int)trackedObject.index; } + } - // The current and previous state of the controller. - private Valve.VR.VRControllerState_t currentState; - private Valve.VR.VRControllerState_t previousState; - // The current and previous most-recent non-zero touchpad locations. - private Vector2 currentPad; - private Vector2 previousPad; - - // Cache button presses for efficiency. - private bool triggerPressed; - private bool gripPressed; - private bool secondaryButtonPressed; - private bool applicationButtonPressed; - private bool touchpadPressed; - private bool triggerHalfPressed; - private bool triggerWasPressedOnLastUpdate; - private bool gripWasPressedOnLastUpdate; - private bool secondaryButtonWasPressedOnLastUpdate; - private bool applicationButtonWasPressedOnLastUpdate; - private bool touchpadWasPressedOnLastUpdate; - private bool triggerWasHalfPressedOnLastUpdate; - - // A Steam controller's validity is always determined by its TrackedObject's validity. - private bool isValid; - public bool IsTrackedObjectValid { - get { return trackedObject.isValid; } - set { Debug.Assert(value == trackedObject.isValid); } - } + // The current and previous state of the controller. + private Valve.VR.VRControllerState_t currentState; + private Valve.VR.VRControllerState_t previousState; + // The current and previous most-recent non-zero touchpad locations. + private Vector2 currentPad; + private Vector2 previousPad; + + // Cache button presses for efficiency. + private bool triggerPressed; + private bool gripPressed; + private bool secondaryButtonPressed; + private bool applicationButtonPressed; + private bool touchpadPressed; + private bool triggerHalfPressed; + private bool triggerWasPressedOnLastUpdate; + private bool gripWasPressedOnLastUpdate; + private bool secondaryButtonWasPressedOnLastUpdate; + private bool applicationButtonWasPressedOnLastUpdate; + private bool touchpadWasPressedOnLastUpdate; + private bool triggerWasHalfPressedOnLastUpdate; + + // A Steam controller's validity is always determined by its TrackedObject's validity. + private bool isValid; + public bool IsTrackedObjectValid + { + get { return trackedObject.isValid; } + set { Debug.Assert(value == trackedObject.isValid); } + } - // Constructor, taking in a transform such that it can find the SteamVRTrackedObject. - public ControllerDeviceSteam(Transform transform) { - trackedObject = Config.Instance.VrHardware == VrHardware.Rift ? - transform.GetComponent() : - transform.GetComponent(); - currentState = new Valve.VR.VRControllerState_t(); - previousState = new Valve.VR.VRControllerState_t(); - } + // Constructor, taking in a transform such that it can find the SteamVRTrackedObject. + public ControllerDeviceSteam(Transform transform) + { + trackedObject = Config.Instance.VrHardware == VrHardware.Rift ? + transform.GetComponent() : + transform.GetComponent(); + currentState = new Valve.VR.VRControllerState_t(); + previousState = new Valve.VR.VRControllerState_t(); + } - // Update loop (to be called manually, this is not a MonoBehavior). - public void Update() { - // Get current and previous controller state. - SteamVR steamVR = SteamVR.instance; - if (steamVR != null) { - previousState = currentState; - steamVR.hmd.GetControllerState((uint)Index, ref currentState, - (uint)System.Runtime.InteropServices.Marshal.SizeOf(typeof(Valve.VR.VRControllerState_t))); - } - - // Update the latest touchpad location, if possible. - previousPad = currentPad; - if (Index != (int)SteamVR_TrackedObject.EIndex.None) { - var currentPadV = currentState.rAxis0; - currentPad = new Vector2(currentPadV.x, currentPadV.y); - } else { - currentPad = Vector2.zero; - } - - // Update 'previous state' variables. - triggerWasPressedOnLastUpdate = triggerPressed; - gripWasPressedOnLastUpdate = gripPressed; - secondaryButtonWasPressedOnLastUpdate = secondaryButtonPressed; - applicationButtonWasPressedOnLastUpdate = applicationButtonPressed; - touchpadWasPressedOnLastUpdate = touchpadPressed; - triggerWasHalfPressedOnLastUpdate = triggerHalfPressed; - - // Find which buttons are currently pressed. - triggerPressed = ButtonIsPressedInternal(ButtonId.Trigger); - gripPressed = ButtonIsPressedInternal(ButtonId.Grip); - secondaryButtonPressed = ButtonIsPressedInternal(ButtonId.SecondaryButton); - applicationButtonPressed = ButtonIsPressedInternal(ButtonId.ApplicationMenu); - touchpadPressed = ButtonIsPressedInternal(ButtonId.Touchpad); - triggerHalfPressed = IsTriggerHalfPressedInternal(); - } + // Update loop (to be called manually, this is not a MonoBehavior). + public void Update() + { + // Get current and previous controller state. + SteamVR steamVR = SteamVR.instance; + if (steamVR != null) + { + previousState = currentState; + steamVR.hmd.GetControllerState((uint)Index, ref currentState, + (uint)System.Runtime.InteropServices.Marshal.SizeOf(typeof(Valve.VR.VRControllerState_t))); + } + + // Update the latest touchpad location, if possible. + previousPad = currentPad; + if (Index != (int)SteamVR_TrackedObject.EIndex.None) + { + var currentPadV = currentState.rAxis0; + currentPad = new Vector2(currentPadV.x, currentPadV.y); + } + else + { + currentPad = Vector2.zero; + } + + // Update 'previous state' variables. + triggerWasPressedOnLastUpdate = triggerPressed; + gripWasPressedOnLastUpdate = gripPressed; + secondaryButtonWasPressedOnLastUpdate = secondaryButtonPressed; + applicationButtonWasPressedOnLastUpdate = applicationButtonPressed; + touchpadWasPressedOnLastUpdate = touchpadPressed; + triggerWasHalfPressedOnLastUpdate = triggerHalfPressed; + + // Find which buttons are currently pressed. + triggerPressed = ButtonIsPressedInternal(ButtonId.Trigger); + gripPressed = ButtonIsPressedInternal(ButtonId.Grip); + secondaryButtonPressed = ButtonIsPressedInternal(ButtonId.SecondaryButton); + applicationButtonPressed = ButtonIsPressedInternal(ButtonId.ApplicationMenu); + touchpadPressed = ButtonIsPressedInternal(ButtonId.Touchpad); + triggerHalfPressed = IsTriggerHalfPressedInternal(); + } - // A mapping from ButtonId to SteamVR ButtonMask. - private static ulong GetMaskFromButtonId(ButtonId buttonId) { - ulong mask = 0; - switch (buttonId) { - case ButtonId.ApplicationMenu: - mask = Config.Instance.VrHardware == VrHardware.Rift - ? ButtonMask_Button01 - : SteamVR_Controller.ButtonMask.ApplicationMenu; - break; - case ButtonId.Touchpad: - mask = SteamVR_Controller.ButtonMask.Touchpad; - break; - case ButtonId.Trigger: - mask = SteamVR_Controller.ButtonMask.Trigger; - break; - case ButtonId.Grip: - mask = SteamVR_Controller.ButtonMask.Grip; - break; - case ButtonId.SecondaryButton: - mask = ButtonMask_Button02; - break; - } - return mask; - } + // A mapping from ButtonId to SteamVR ButtonMask. + private static ulong GetMaskFromButtonId(ButtonId buttonId) + { + ulong mask = 0; + switch (buttonId) + { + case ButtonId.ApplicationMenu: + mask = Config.Instance.VrHardware == VrHardware.Rift + ? ButtonMask_Button01 + : SteamVR_Controller.ButtonMask.ApplicationMenu; + break; + case ButtonId.Touchpad: + mask = SteamVR_Controller.ButtonMask.Touchpad; + break; + case ButtonId.Trigger: + mask = SteamVR_Controller.ButtonMask.Trigger; + break; + case ButtonId.Grip: + mask = SteamVR_Controller.ButtonMask.Grip; + break; + case ButtonId.SecondaryButton: + mask = ButtonMask_Button02; + break; + } + return mask; + } - // The trigger scale for the previous update, to aid with detecting trigger-released using our custom threshold. - private Vector2 GetPreviousTriggerScale() { - return new Vector2(previousState.rAxis1.x, previousState.rAxis1.y); - } + // The trigger scale for the previous update, to aid with detecting trigger-released using our custom threshold. + private Vector2 GetPreviousTriggerScale() + { + return new Vector2(previousState.rAxis1.x, previousState.rAxis1.y); + } - // Interface method implementations begin. - - public Vector3 GetVelocity() { - if (Index == (int)SteamVR_TrackedObject.EIndex.None) { return Vector3.zero; } - SteamVR_Controller.Device controller = SteamVR_Controller.Input((int)trackedObject.index); - return controller.velocity; - } + // Interface method implementations begin. - public bool IsTriggerHalfPressedInternal() { - if (Index == (int)SteamVR_TrackedObject.EIndex.None) { return false; } - SteamVR_Controller.Device controller = SteamVR_Controller.Input((int)trackedObject.index); - // Only record as half pressed if the trigger is not pressed. - return controller.GetAxis(Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger).x >= LIGHT_TRIGGER_PULL_THRESHOLD - && !triggerPressed; - } + public Vector3 GetVelocity() + { + if (Index == (int)SteamVR_TrackedObject.EIndex.None) { return Vector3.zero; } + SteamVR_Controller.Device controller = SteamVR_Controller.Input((int)trackedObject.index); + return controller.velocity; + } - public bool IsPressed(ButtonId buttonId) { - if (Index == (int)SteamVR_TrackedObject.EIndex.None) { return false; } - - switch (buttonId) { - case ButtonId.ApplicationMenu: - return applicationButtonPressed; - case ButtonId.Touchpad: - return touchpadPressed; - case ButtonId.Trigger: - return triggerPressed; - case ButtonId.Grip: - return gripPressed; - case ButtonId.SecondaryButton: - return secondaryButtonPressed; - } - - return false; - } + public bool IsTriggerHalfPressedInternal() + { + if (Index == (int)SteamVR_TrackedObject.EIndex.None) { return false; } + SteamVR_Controller.Device controller = SteamVR_Controller.Input((int)trackedObject.index); + // Only record as half pressed if the trigger is not pressed. + return controller.GetAxis(Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger).x >= LIGHT_TRIGGER_PULL_THRESHOLD + && !triggerPressed; + } - public bool IsTriggerHalfPressed() { - if (Index == (int)SteamVR_TrackedObject.EIndex.None) { return false; } - return triggerHalfPressed; - } + public bool IsPressed(ButtonId buttonId) + { + if (Index == (int)SteamVR_TrackedObject.EIndex.None) { return false; } + + switch (buttonId) + { + case ButtonId.ApplicationMenu: + return applicationButtonPressed; + case ButtonId.Touchpad: + return touchpadPressed; + case ButtonId.Trigger: + return triggerPressed; + case ButtonId.Grip: + return gripPressed; + case ButtonId.SecondaryButton: + return secondaryButtonPressed; + } + + return false; + } - public bool WasTriggerJustReleasedFromHalfPress() { - if (Index == (int)SteamVR_TrackedObject.EIndex.None) { return false; } - return !triggerHalfPressed && !triggerPressed && triggerWasHalfPressedOnLastUpdate; - } + public bool IsTriggerHalfPressed() + { + if (Index == (int)SteamVR_TrackedObject.EIndex.None) { return false; } + return triggerHalfPressed; + } - public bool WasJustPressed(ButtonId buttonId) { - if (Index == (int)SteamVR_TrackedObject.EIndex.None) { return false; } - - switch (buttonId) { - case ButtonId.ApplicationMenu: - return applicationButtonPressed && !applicationButtonWasPressedOnLastUpdate; - case ButtonId.Touchpad: - return touchpadPressed && !touchpadWasPressedOnLastUpdate; - case ButtonId.Trigger: - return triggerPressed && !triggerWasPressedOnLastUpdate; - case ButtonId.Grip: - return gripPressed && !gripWasPressedOnLastUpdate; - case ButtonId.SecondaryButton: - return secondaryButtonPressed && !secondaryButtonWasPressedOnLastUpdate; - } - - return false; - } + public bool WasTriggerJustReleasedFromHalfPress() + { + if (Index == (int)SteamVR_TrackedObject.EIndex.None) { return false; } + return !triggerHalfPressed && !triggerPressed && triggerWasHalfPressedOnLastUpdate; + } - public bool WasJustReleased(ButtonId buttonId) { - if (Index == (int)SteamVR_TrackedObject.EIndex.None) { return false; } - - switch (buttonId) { - case ButtonId.ApplicationMenu: - return !applicationButtonPressed && applicationButtonWasPressedOnLastUpdate; - case ButtonId.Touchpad: - return !touchpadPressed && touchpadWasPressedOnLastUpdate; - case ButtonId.Trigger: - return !triggerPressed && triggerWasPressedOnLastUpdate; - case ButtonId.Grip: - return !gripPressed && gripWasPressedOnLastUpdate; - case ButtonId.SecondaryButton: - return !secondaryButtonPressed && secondaryButtonWasPressedOnLastUpdate; - } - - return false; - } + public bool WasJustPressed(ButtonId buttonId) + { + if (Index == (int)SteamVR_TrackedObject.EIndex.None) { return false; } + + switch (buttonId) + { + case ButtonId.ApplicationMenu: + return applicationButtonPressed && !applicationButtonWasPressedOnLastUpdate; + case ButtonId.Touchpad: + return touchpadPressed && !touchpadWasPressedOnLastUpdate; + case ButtonId.Trigger: + return triggerPressed && !triggerWasPressedOnLastUpdate; + case ButtonId.Grip: + return gripPressed && !gripWasPressedOnLastUpdate; + case ButtonId.SecondaryButton: + return secondaryButtonPressed && !secondaryButtonWasPressedOnLastUpdate; + } + + return false; + } - private bool ButtonIsPressedInternal(ButtonId buttonId) { - if (buttonId == ButtonId.Trigger && GetTriggerScale().x <= PeltzerMain.TRIGGER_THRESHOLD) { - return false; - } - - ulong mask = GetMaskFromButtonId(buttonId); - - // The Touch thumbstick is considered 'pressed' is it is in one of the far quadrants, or if it is in the center - // and has actually been depressed. This allows users to simply flick the thumbstick to choose an option, rather - // than having to move and press-in the thumbstick, which is tiresome. - if (Config.Instance.VrHardware == VrHardware.Rift && buttonId == ButtonId.Touchpad) { - TouchpadLocation touchpadLocation = - TouchpadLocationHelper.GetTouchpadLocation(currentPad); - if (touchpadLocation == TouchpadLocation.CENTER) { - return (currentState.ulButtonPressed & mask) != 0; - } else if (touchpadLocation == TouchpadLocation.NONE) { - return false; - } else { - return true; + public bool WasJustReleased(ButtonId buttonId) + { + if (Index == (int)SteamVR_TrackedObject.EIndex.None) { return false; } + + switch (buttonId) + { + case ButtonId.ApplicationMenu: + return !applicationButtonPressed && applicationButtonWasPressedOnLastUpdate; + case ButtonId.Touchpad: + return !touchpadPressed && touchpadWasPressedOnLastUpdate; + case ButtonId.Trigger: + return !triggerPressed && triggerWasPressedOnLastUpdate; + case ButtonId.Grip: + return !gripPressed && gripWasPressedOnLastUpdate; + case ButtonId.SecondaryButton: + return !secondaryButtonPressed && secondaryButtonWasPressedOnLastUpdate; + } + + return false; } - } - return (currentState.ulButtonPressed & mask) != 0; - } + private bool ButtonIsPressedInternal(ButtonId buttonId) + { + if (buttonId == ButtonId.Trigger && GetTriggerScale().x <= PeltzerMain.TRIGGER_THRESHOLD) + { + return false; + } + + ulong mask = GetMaskFromButtonId(buttonId); + + // The Touch thumbstick is considered 'pressed' is it is in one of the far quadrants, or if it is in the center + // and has actually been depressed. This allows users to simply flick the thumbstick to choose an option, rather + // than having to move and press-in the thumbstick, which is tiresome. + if (Config.Instance.VrHardware == VrHardware.Rift && buttonId == ButtonId.Touchpad) + { + TouchpadLocation touchpadLocation = + TouchpadLocationHelper.GetTouchpadLocation(currentPad); + if (touchpadLocation == TouchpadLocation.CENTER) + { + return (currentState.ulButtonPressed & mask) != 0; + } + else if (touchpadLocation == TouchpadLocation.NONE) + { + return false; + } + else + { + return true; + } + } + + return (currentState.ulButtonPressed & mask) != 0; + } - public bool IsTouched(ButtonId buttonId) { - if (Index == (int)SteamVR_TrackedObject.EIndex.None) { return false; } + public bool IsTouched(ButtonId buttonId) + { + if (Index == (int)SteamVR_TrackedObject.EIndex.None) { return false; } - ulong mask = GetMaskFromButtonId(buttonId); - return (currentState.ulButtonTouched & mask) != 0; - } + ulong mask = GetMaskFromButtonId(buttonId); + return (currentState.ulButtonTouched & mask) != 0; + } - public Vector2 GetDirectionalAxis() { - return currentPad; - } + public Vector2 GetDirectionalAxis() + { + return currentPad; + } - public TouchpadLocation GetTouchpadLocation() { - return TouchpadLocationHelper.GetTouchpadLocation(currentPad); - } + public TouchpadLocation GetTouchpadLocation() + { + return TouchpadLocationHelper.GetTouchpadLocation(currentPad); + } - public Vector2 GetTriggerScale() { - return new Vector2(currentState.rAxis1.x, currentState.rAxis1.y); - } + public Vector2 GetTriggerScale() + { + return new Vector2(currentState.rAxis1.x, currentState.rAxis1.y); + } - public void TriggerHapticPulse(ushort durationMicroSec = 500) { - SteamVR steamVR = SteamVR.instance; - if (steamVR == null) { return; } - // Steam accepts a parameter for indicating the axis of the haptic pulse but 0 is the only - // one implemented now and this concept doesn't exist for Oculus controllers so it's being - // hardcoded to (uint)0 here. - steamVR.hmd.TriggerHapticPulse((uint)Index, (uint)0, (char)durationMicroSec); + public void TriggerHapticPulse(ushort durationMicroSec = 500) + { + SteamVR steamVR = SteamVR.instance; + if (steamVR == null) { return; } + // Steam accepts a parameter for indicating the axis of the haptic pulse but 0 is the only + // one implemented now and this concept doesn't exist for Oculus controllers so it's being + // hardcoded to (uint)0 here. + steamVR.hmd.TriggerHapticPulse((uint)Index, (uint)0, (char)durationMicroSec); + } } - } #endif } diff --git a/Assets/Scripts/model/controller/ControllerEventArgs.cs b/Assets/Scripts/model/controller/ControllerEventArgs.cs index 75dd63be..15ee62f9 100644 --- a/Assets/Scripts/model/controller/ControllerEventArgs.cs +++ b/Assets/Scripts/model/controller/ControllerEventArgs.cs @@ -14,32 +14,35 @@ using System; -namespace com.google.apps.peltzer.client.model.controller { +namespace com.google.apps.peltzer.client.model.controller +{ - /// - /// Payload for controller events. Contains relevant information to describe the event. - /// - public class ControllerEventArgs : EventArgs { - private readonly ControllerType controllerType; - private readonly ButtonId buttonId; - private readonly ButtonAction buttonAction; - private readonly TouchpadLocation touchpadLocation; - private readonly TouchpadOverlay overlay; + /// + /// Payload for controller events. Contains relevant information to describe the event. + /// + public class ControllerEventArgs : EventArgs + { + private readonly ControllerType controllerType; + private readonly ButtonId buttonId; + private readonly ButtonAction buttonAction; + private readonly TouchpadLocation touchpadLocation; + private readonly TouchpadOverlay overlay; - public ControllerEventArgs(ControllerType controllerType, ButtonId buttonId, - ButtonAction buttonAction, TouchpadLocation touchpadLocation, - TouchpadOverlay overlay) { - this.controllerType = controllerType; - this.buttonId = buttonId; - this.buttonAction = buttonAction; - this.touchpadLocation = touchpadLocation; - this.overlay = overlay; - } + public ControllerEventArgs(ControllerType controllerType, ButtonId buttonId, + ButtonAction buttonAction, TouchpadLocation touchpadLocation, + TouchpadOverlay overlay) + { + this.controllerType = controllerType; + this.buttonId = buttonId; + this.buttonAction = buttonAction; + this.touchpadLocation = touchpadLocation; + this.overlay = overlay; + } - public ControllerType ControllerType { get { return controllerType; } } - public ButtonId ButtonId { get { return buttonId; } } - public ButtonAction Action { get { return buttonAction; } } - public TouchpadLocation TouchpadLocation { get { return touchpadLocation; } } - public TouchpadOverlay TouchpadOverlay { get { return overlay; } } - } + public ControllerType ControllerType { get { return controllerType; } } + public ButtonId ButtonId { get { return buttonId; } } + public ButtonAction Action { get { return buttonAction; } } + public TouchpadLocation TouchpadLocation { get { return touchpadLocation; } } + public TouchpadOverlay TouchpadOverlay { get { return overlay; } } + } } diff --git a/Assets/Scripts/model/controller/ControllerGeometry.cs b/Assets/Scripts/model/controller/ControllerGeometry.cs index edefaea5..e211c39d 100644 --- a/Assets/Scripts/model/controller/ControllerGeometry.cs +++ b/Assets/Scripts/model/controller/ControllerGeometry.cs @@ -14,127 +14,129 @@ using UnityEngine; -namespace com.google.apps.peltzer.client.model.controller { - - /// - /// A class to abstract out controller geometry - /// - public class ControllerGeometry : MonoBehaviour { - public BaseControllerAnimation baseControllerAnimation; - - [Header("Geometry")] - public GameObject trigger; - public GameObject gripLeft; - public GameObject gripRight; - // The transforms on appMenuButton are completely wrong because of errors with the prefab. This - // holder is used so that we can correctly manipulate the transform. - public GameObject appMenuButtonHolder; - public GameObject appMenuButton; - public GameObject secondaryButton; - public GameObject systemButton; - public GameObject thumbstick; - public GameObject touchpad; - public GameObject segmentedTouchpad; - public GameObject touchpadLeft; - public GameObject touchpadRight; - public GameObject touchpadUp; - public GameObject touchpadDown; - public GameObject handleBase; - - [Header("Overlays")] - public GameObject volumeInserterOverlay; - public GameObject freeformOverlay; - public GameObject freeformChangeFaceOverlay; - public GameObject paintOverlay; - public GameObject modifyOverlay; - public GameObject moveOverlay; - public GameObject deleteOverlay; - public GameObject menuOverlay; - public GameObject undoRedoOverlay; - public GameObject resizeOverlay; - public GameObject resetZoomOverlay; - public GameObject OnMoveOverlay; - public GameObject OnMenuOverlay; - public GameObject OnUndoRedoOverlay; - - [Header("Tool Tips")] - public GameObject applicationButtonTooltipRoot; - public GameObject applicationButtonTooltipLeft; - public GameObject applicationButtonTooltipRight; - - public GameObject groupTooltipRoot; - public GameObject groupLeftTooltip; - public GameObject groupRightTooltip; - public GameObject ungroupLeftTooltip; - public GameObject ungroupRightTooltip; - - public GameObject shapeTooltips; - - public GameObject menuLeftTooltip; - public GameObject menuRightTooltip; - public GameObject menuUpTooltip; - public GameObject menuDownTooltip; - - public GameObject modifyTooltips; - public GameObject modifyTooltipLeft; - public GameObject modifyTooltipRight; - public GameObject modifyTooltipUp; - - public GameObject moverTooltips; - public GameObject moverTooltipLeft; - public GameObject moverTooltipRight; - public GameObject moverTooltipUp; - public GameObject moverTooltipDown; - - public GameObject freeformTooltips; - public GameObject freeformTooltipLeft; - public GameObject freeformTooltipRight; - public GameObject freeformTooltipUp; - public GameObject freeformTooltipDown; - public GameObject freeformTooltipCenter; - - public GameObject volumeInserterTooltips; - public GameObject volumeInserterTooltipLeft; - public GameObject volumeInserterTooltipRight; - public GameObject volumeInserterTooltipUp; - public GameObject volumeInserterTooltipDown; - - public GameObject paintTooltips; - public GameObject paintTooltipLeft; - public GameObject paintTooltipRight; - - public GameObject resizeUpTooltip; - public GameObject resizeDownTooltip; - - public GameObject undoRedoLeftTooltip; - public GameObject undoRedoRightTooltip; - - public GameObject grabTooltips; - public GameObject zoomLeftTooltip; - public GameObject zoomRightTooltip; - public GameObject moveLeftTooltip; - public GameObject moveRightTooltip; - public GameObject snapLeftTooltip; - public GameObject snapRightTooltip; - public GameObject straightenLeftTooltip; - public GameObject straightenRightTooltip; - - public GameObject snapGrabAssistLeftTooltip; - public GameObject snapGrabAssistRightTooltip; - public GameObject snapGrabHoldLeftTooltip; - public GameObject snapGrabHoldRightTooltip; - public GameObject snapStrokeLeftTooltip; - public GameObject snapStrokeRightTooltip; - public GameObject snapShapeInsertLeftTooltip; - public GameObject snapShapeInsertRightTooltip; - public GameObject snapModifyLeftTooltip; - public GameObject snapModifyRightTooltip; - public GameObject snapPaintOrEraseLeftTooltip; - public GameObject snapPaintOrEraseRightTooltip; - - [Header("IconLocations")] - public GameObject groupButtonIcon; - - public GameObject[] overlays; - } +namespace com.google.apps.peltzer.client.model.controller +{ + + /// + /// A class to abstract out controller geometry + /// + public class ControllerGeometry : MonoBehaviour + { + public BaseControllerAnimation baseControllerAnimation; + + [Header("Geometry")] + public GameObject trigger; + public GameObject gripLeft; + public GameObject gripRight; + // The transforms on appMenuButton are completely wrong because of errors with the prefab. This + // holder is used so that we can correctly manipulate the transform. + public GameObject appMenuButtonHolder; + public GameObject appMenuButton; + public GameObject secondaryButton; + public GameObject systemButton; + public GameObject thumbstick; + public GameObject touchpad; + public GameObject segmentedTouchpad; + public GameObject touchpadLeft; + public GameObject touchpadRight; + public GameObject touchpadUp; + public GameObject touchpadDown; + public GameObject handleBase; + + [Header("Overlays")] + public GameObject volumeInserterOverlay; + public GameObject freeformOverlay; + public GameObject freeformChangeFaceOverlay; + public GameObject paintOverlay; + public GameObject modifyOverlay; + public GameObject moveOverlay; + public GameObject deleteOverlay; + public GameObject menuOverlay; + public GameObject undoRedoOverlay; + public GameObject resizeOverlay; + public GameObject resetZoomOverlay; + public GameObject OnMoveOverlay; + public GameObject OnMenuOverlay; + public GameObject OnUndoRedoOverlay; + + [Header("Tool Tips")] + public GameObject applicationButtonTooltipRoot; + public GameObject applicationButtonTooltipLeft; + public GameObject applicationButtonTooltipRight; + + public GameObject groupTooltipRoot; + public GameObject groupLeftTooltip; + public GameObject groupRightTooltip; + public GameObject ungroupLeftTooltip; + public GameObject ungroupRightTooltip; + + public GameObject shapeTooltips; + + public GameObject menuLeftTooltip; + public GameObject menuRightTooltip; + public GameObject menuUpTooltip; + public GameObject menuDownTooltip; + + public GameObject modifyTooltips; + public GameObject modifyTooltipLeft; + public GameObject modifyTooltipRight; + public GameObject modifyTooltipUp; + + public GameObject moverTooltips; + public GameObject moverTooltipLeft; + public GameObject moverTooltipRight; + public GameObject moverTooltipUp; + public GameObject moverTooltipDown; + + public GameObject freeformTooltips; + public GameObject freeformTooltipLeft; + public GameObject freeformTooltipRight; + public GameObject freeformTooltipUp; + public GameObject freeformTooltipDown; + public GameObject freeformTooltipCenter; + + public GameObject volumeInserterTooltips; + public GameObject volumeInserterTooltipLeft; + public GameObject volumeInserterTooltipRight; + public GameObject volumeInserterTooltipUp; + public GameObject volumeInserterTooltipDown; + + public GameObject paintTooltips; + public GameObject paintTooltipLeft; + public GameObject paintTooltipRight; + + public GameObject resizeUpTooltip; + public GameObject resizeDownTooltip; + + public GameObject undoRedoLeftTooltip; + public GameObject undoRedoRightTooltip; + + public GameObject grabTooltips; + public GameObject zoomLeftTooltip; + public GameObject zoomRightTooltip; + public GameObject moveLeftTooltip; + public GameObject moveRightTooltip; + public GameObject snapLeftTooltip; + public GameObject snapRightTooltip; + public GameObject straightenLeftTooltip; + public GameObject straightenRightTooltip; + + public GameObject snapGrabAssistLeftTooltip; + public GameObject snapGrabAssistRightTooltip; + public GameObject snapGrabHoldLeftTooltip; + public GameObject snapGrabHoldRightTooltip; + public GameObject snapStrokeLeftTooltip; + public GameObject snapStrokeRightTooltip; + public GameObject snapShapeInsertLeftTooltip; + public GameObject snapShapeInsertRightTooltip; + public GameObject snapModifyLeftTooltip; + public GameObject snapModifyRightTooltip; + public GameObject snapPaintOrEraseLeftTooltip; + public GameObject snapPaintOrEraseRightTooltip; + + [Header("IconLocations")] + public GameObject groupButtonIcon; + + public GameObject[] overlays; + } } \ No newline at end of file diff --git a/Assets/Scripts/model/controller/ControllerMain.cs b/Assets/Scripts/model/controller/ControllerMain.cs index 479967bc..3ad20a63 100644 --- a/Assets/Scripts/model/controller/ControllerMain.cs +++ b/Assets/Scripts/model/controller/ControllerMain.cs @@ -16,29 +16,35 @@ using System.Collections; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Delegate method for controller clients to implement. - /// - public delegate void ControllerActionHandler(object sender, ControllerEventArgs args); - - public class ControllerMain { +namespace com.google.apps.peltzer.client.model.controller +{ /// - /// Clients must register themselves on this handler. + /// Delegate method for controller clients to implement. /// - public event ControllerActionHandler ControllerActionHandler; + public delegate void ControllerActionHandler(object sender, ControllerEventArgs args); - public ControllerMain(PeltzerController peltzerController, PaletteController paletteController) { - peltzerController.PeltzerControllerActionHandler += ControllerEventHandler; - paletteController.PaletteControllerActionHandler += ControllerEventHandler; - } + public class ControllerMain + { + /// + /// Clients must register themselves on this handler. + /// + public event ControllerActionHandler ControllerActionHandler; + + public ControllerMain(PeltzerController peltzerController, PaletteController paletteController) + { + peltzerController.PeltzerControllerActionHandler += ControllerEventHandler; + paletteController.PaletteControllerActionHandler += ControllerEventHandler; + } - private void ControllerEventHandler(object sender, ControllerEventArgs args) { - if (ControllerActionHandler != null) { - if (PeltzerMain.Instance.restrictionManager.controllerEventsAllowed) { - ControllerActionHandler(sender, args); + private void ControllerEventHandler(object sender, ControllerEventArgs args) + { + if (ControllerActionHandler != null) + { + if (PeltzerMain.Instance.restrictionManager.controllerEventsAllowed) + { + ControllerActionHandler(sender, args); + } + } } - } } - } } diff --git a/Assets/Scripts/model/controller/ControllerMode.cs b/Assets/Scripts/model/controller/ControllerMode.cs index 06c505cb..3197db39 100644 --- a/Assets/Scripts/model/controller/ControllerMode.cs +++ b/Assets/Scripts/model/controller/ControllerMode.cs @@ -12,64 +12,66 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace com.google.apps.peltzer.client.model.controller { - /// - /// The current mode of a controller. - /// IMPORTANT: enum values are used as indices, so the values must be numbered sequentially from 0 - /// (default enum value assignment). - /// - public enum ControllerMode { +namespace com.google.apps.peltzer.client.model.controller +{ /// - /// Mode for inserting new primitives and volumes. + /// The current mode of a controller. + /// IMPORTANT: enum values are used as indices, so the values must be numbered sequentially from 0 + /// (default enum value assignment). /// - insertVolume, - /// - /// Mode for inserting strokes. - /// - insertStroke, - /// - /// Mode for moving mesh components. - /// - reshape, - /// - /// Mode for extruding faces. - /// - extrude, - /// - /// Mode for subdividing faces. - /// - subdivideFace, - /// - /// Mode for subdividing entire meshes. - /// - subdivideMesh, - /// - /// Mode for deleting meshes. - /// - delete, - /// - /// Mode for moving meshes. - /// - move, - /// - /// Mode for painting meshes. - /// - paintMesh, - /// - /// Mode for painting faces. - /// - paintFace, - /// - /// Mode for selecting paint color from existing objects. - /// - paintDropper, - /// - /// Mode for deleting meshes via subtraction (csg). - /// - subtract, - /// - /// Mode for deleting edges. - /// - deletePart, - } + public enum ControllerMode + { + /// + /// Mode for inserting new primitives and volumes. + /// + insertVolume, + /// + /// Mode for inserting strokes. + /// + insertStroke, + /// + /// Mode for moving mesh components. + /// + reshape, + /// + /// Mode for extruding faces. + /// + extrude, + /// + /// Mode for subdividing faces. + /// + subdivideFace, + /// + /// Mode for subdividing entire meshes. + /// + subdivideMesh, + /// + /// Mode for deleting meshes. + /// + delete, + /// + /// Mode for moving meshes. + /// + move, + /// + /// Mode for painting meshes. + /// + paintMesh, + /// + /// Mode for painting faces. + /// + paintFace, + /// + /// Mode for selecting paint color from existing objects. + /// + paintDropper, + /// + /// Mode for deleting meshes via subtraction (csg). + /// + subtract, + /// + /// Mode for deleting edges. + /// + deletePart, + } } diff --git a/Assets/Scripts/model/controller/ControllerSwapDetector.cs b/Assets/Scripts/model/controller/ControllerSwapDetector.cs index a946495b..902886b4 100644 --- a/Assets/Scripts/model/controller/ControllerSwapDetector.cs +++ b/Assets/Scripts/model/controller/ControllerSwapDetector.cs @@ -18,273 +18,301 @@ using UnityEngine; using UnityEngine.UI; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Detects and handles the "controller bump" gesture to switch the left and right controllers. - /// - public class ControllerSwapDetector : MonoBehaviour { +namespace com.google.apps.peltzer.client.model.controller +{ /// - /// Minimum angle between controllers to allow swap. + /// Detects and handles the "controller bump" gesture to switch the left and right controllers. /// - private const float OPPOSED_ANGLE_THRESHOLD = 170.0f; - - /// - /// Minimum angle between controllers to allow swap when using Oculus SDK. - /// - private const float OPPOSED_ANGLE_THRESHOLD_OCULUS = 90f; - - /// - /// Maximum distance between controllers to allow swap. - /// - private const float MAX_DISTANCE_FOR_SWAP = 0.4f; - - /// - /// Maximum distance between controllers to allow swap when using Oculus SDK. - /// - private const float MAX_DISTANCE_FOR_SWAP_OCULUS = 0.04f; - - /// - /// Minimum magnitude of acceleration to consider controller to have bumped. - /// - private const float BUMP_ACCELERATION_THRESHOLD = 10.0f; - - /// - /// Maximum velocity of controller to consider controller to have bumped - /// (a bump happens when there is a sufficiently large acceleration and a - /// sufficiently small velocity, indicating that the controller has just stopped). - /// - private const float BUMP_VELOCITY_THRESHOLD = 0.5f; - - /// - /// Duration for which a bump remains active. This exists because controllers can detect - /// the bump at different times due to sensor imprecision, so we need the bump to remain - /// "valid" for a while in order to know that both controllers bumped. - /// - private const float BUMP_DURATION = 0.2f; - - /// - /// Minimum time between successive controller swaps. - /// - private const float MIN_TIME_BETWEEN_SWAPS = 2.0f; - - /// - /// Angle to adjust the Oculus controllers' forwards by when measuring if they are facing away - /// from each other. - /// - private static readonly Quaternion ADJUST_ANGLE_OCULUS = Quaternion.Euler(-45, 0, 0); - - /// - /// Whether setup is done or not. - /// - private bool setupDone; - - /// - /// We can't swap controllers until both are available: this tracks if we're waiting for a swap. - /// - private bool waitingToSwapControllers; - - /// - /// Peltzer controller. - /// - private PeltzerController peltzerController; - - /// - /// Palette controller. - /// - private PaletteController paletteController; - - /// - /// Time until which we do not detect the controller swap gesture. - /// - private float snoozeUntil; - - /// - /// Information about the position/velocity/acceleration of a particular controller. - /// - private class ControllerInfo { - public Vector3 position; - public Vector3 velocity; - public Vector3 acceleration; - public float lastBumpTime; - - public ControllerInfo(Vector3 initialPosition) { - position = initialPosition; - } - - public void UpdatePosition(Vector3 newPosition) { - Vector3 newVelocity = (newPosition - position) / Time.deltaTime; - acceleration = (newVelocity - velocity) / Time.deltaTime; - velocity = newVelocity; - position = newPosition; - - if (acceleration.magnitude > BUMP_ACCELERATION_THRESHOLD && velocity.magnitude < BUMP_VELOCITY_THRESHOLD) { - lastBumpTime = Time.time; + public class ControllerSwapDetector : MonoBehaviour + { + /// + /// Minimum angle between controllers to allow swap. + /// + private const float OPPOSED_ANGLE_THRESHOLD = 170.0f; + + /// + /// Minimum angle between controllers to allow swap when using Oculus SDK. + /// + private const float OPPOSED_ANGLE_THRESHOLD_OCULUS = 90f; + + /// + /// Maximum distance between controllers to allow swap. + /// + private const float MAX_DISTANCE_FOR_SWAP = 0.4f; + + /// + /// Maximum distance between controllers to allow swap when using Oculus SDK. + /// + private const float MAX_DISTANCE_FOR_SWAP_OCULUS = 0.04f; + + /// + /// Minimum magnitude of acceleration to consider controller to have bumped. + /// + private const float BUMP_ACCELERATION_THRESHOLD = 10.0f; + + /// + /// Maximum velocity of controller to consider controller to have bumped + /// (a bump happens when there is a sufficiently large acceleration and a + /// sufficiently small velocity, indicating that the controller has just stopped). + /// + private const float BUMP_VELOCITY_THRESHOLD = 0.5f; + + /// + /// Duration for which a bump remains active. This exists because controllers can detect + /// the bump at different times due to sensor imprecision, so we need the bump to remain + /// "valid" for a while in order to know that both controllers bumped. + /// + private const float BUMP_DURATION = 0.2f; + + /// + /// Minimum time between successive controller swaps. + /// + private const float MIN_TIME_BETWEEN_SWAPS = 2.0f; + + /// + /// Angle to adjust the Oculus controllers' forwards by when measuring if they are facing away + /// from each other. + /// + private static readonly Quaternion ADJUST_ANGLE_OCULUS = Quaternion.Euler(-45, 0, 0); + + /// + /// Whether setup is done or not. + /// + private bool setupDone; + + /// + /// We can't swap controllers until both are available: this tracks if we're waiting for a swap. + /// + private bool waitingToSwapControllers; + + /// + /// Peltzer controller. + /// + private PeltzerController peltzerController; + + /// + /// Palette controller. + /// + private PaletteController paletteController; + + /// + /// Time until which we do not detect the controller swap gesture. + /// + private float snoozeUntil; + + /// + /// Information about the position/velocity/acceleration of a particular controller. + /// + private class ControllerInfo + { + public Vector3 position; + public Vector3 velocity; + public Vector3 acceleration; + public float lastBumpTime; + + public ControllerInfo(Vector3 initialPosition) + { + position = initialPosition; + } + + public void UpdatePosition(Vector3 newPosition) + { + Vector3 newVelocity = (newPosition - position) / Time.deltaTime; + acceleration = (newVelocity - velocity) / Time.deltaTime; + velocity = newVelocity; + position = newPosition; + + if (acceleration.magnitude > BUMP_ACCELERATION_THRESHOLD && velocity.magnitude < BUMP_VELOCITY_THRESHOLD) + { + lastBumpTime = Time.time; + } + } + + public bool BumpedRecently() + { + return Time.time - lastBumpTime < BUMP_DURATION; + } + } + private ControllerInfo[] controllers = new ControllerInfo[2]; + + /// + /// Initial setup. Must be called once when initializing. + /// + public void Setup() + { + peltzerController = PeltzerMain.Instance.peltzerController; + paletteController = PeltzerMain.Instance.paletteController; + controllers[0] = new ControllerInfo(peltzerController.gameObject.transform.position); + controllers[1] = new ControllerInfo(paletteController.gameObject.transform.position); + setupDone = true; } - } - - public bool BumpedRecently() { - return Time.time - lastBumpTime < BUMP_DURATION; - } - } - private ControllerInfo[] controllers = new ControllerInfo[2]; - - /// - /// Initial setup. Must be called once when initializing. - /// - public void Setup() { - peltzerController = PeltzerMain.Instance.peltzerController; - paletteController = PeltzerMain.Instance.paletteController; - controllers[0] = new ControllerInfo(peltzerController.gameObject.transform.position); - controllers[1] = new ControllerInfo(paletteController.gameObject.transform.position); - setupDone = true; - } - - private void Update() { - if (!setupDone) return; - - if (waitingToSwapControllers) { - TrySwappingControllers(); - return; - } - - controllers[0].UpdatePosition(peltzerController.transform.position); - controllers[1].UpdatePosition(paletteController.transform.position); - - // If we're snoozing, just chill. - if (Time.time < snoozeUntil) return; - // If the controllers are appropriately positioned and oriented, and both have recently bumped, - // then the user has performed the "swap controller" gesture. - if (AreControllersOpposed() && AreControllersCloseEnough() && DidControllersBump()) { - // Swap controllers - TrySwappingControllers(); - snoozeUntil = Time.time + MIN_TIME_BETWEEN_SWAPS; - } - } + private void Update() + { + if (!setupDone) return; + + if (waitingToSwapControllers) + { + TrySwappingControllers(); + return; + } + + controllers[0].UpdatePosition(peltzerController.transform.position); + controllers[1].UpdatePosition(paletteController.transform.position); + + // If we're snoozing, just chill. + if (Time.time < snoozeUntil) return; + + // If the controllers are appropriately positioned and oriented, and both have recently bumped, + // then the user has performed the "swap controller" gesture. + if (AreControllersOpposed() && AreControllersCloseEnough() && DidControllersBump()) + { + // Swap controllers + TrySwappingControllers(); + snoozeUntil = Time.time + MIN_TIME_BETWEEN_SWAPS; + } + } - /// - /// Returns whether or not the controllers are facing opposite directions. - /// - private bool AreControllersOpposed() { - if (Config.Instance.sdkMode == SdkMode.Oculus) { - return Vector3.Angle(ADJUST_ANGLE_OCULUS * peltzerController.transform.forward, - ADJUST_ANGLE_OCULUS * paletteController.transform.forward) > OPPOSED_ANGLE_THRESHOLD_OCULUS; - } - return Vector3.Angle(peltzerController.transform.forward, paletteController.transform.forward) > - OPPOSED_ANGLE_THRESHOLD; - } + /// + /// Returns whether or not the controllers are facing opposite directions. + /// + private bool AreControllersOpposed() + { + if (Config.Instance.sdkMode == SdkMode.Oculus) + { + return Vector3.Angle(ADJUST_ANGLE_OCULUS * peltzerController.transform.forward, + ADJUST_ANGLE_OCULUS * paletteController.transform.forward) > OPPOSED_ANGLE_THRESHOLD_OCULUS; + } + return Vector3.Angle(peltzerController.transform.forward, paletteController.transform.forward) > + OPPOSED_ANGLE_THRESHOLD; + } - /// - /// Returns whether or not the controllers are close enough to swap. - /// - private bool AreControllersCloseEnough() { - if (Config.Instance.sdkMode == SdkMode.Oculus) { - return Vector3.Distance(peltzerController.controllerGeometry.handleBase.transform.position, - paletteController.controllerGeometry.handleBase.transform.position) < - MAX_DISTANCE_FOR_SWAP_OCULUS; - } - return Vector3.Distance(peltzerController.transform.position, paletteController.transform.position) < - MAX_DISTANCE_FOR_SWAP; - } + /// + /// Returns whether or not the controllers are close enough to swap. + /// + private bool AreControllersCloseEnough() + { + if (Config.Instance.sdkMode == SdkMode.Oculus) + { + return Vector3.Distance(peltzerController.controllerGeometry.handleBase.transform.position, + paletteController.controllerGeometry.handleBase.transform.position) < + MAX_DISTANCE_FOR_SWAP_OCULUS; + } + return Vector3.Distance(peltzerController.transform.position, paletteController.transform.position) < + MAX_DISTANCE_FOR_SWAP; + } - /// - /// Returns whether or not both controllers have recently bumped. - /// - private bool DidControllersBump() { - return controllers[0].BumpedRecently() && controllers[1].BumpedRecently(); - } + /// + /// Returns whether or not both controllers have recently bumped. + /// + private bool DidControllersBump() + { + return controllers[0].BumpedRecently() && controllers[1].BumpedRecently(); + } - /// - /// Swaps the controllers if both are available, else queues the swap until both are available. - /// - public void TrySwappingControllers() { - waitingToSwapControllers = true; - - if (!PeltzerController.AcquireIfNecessary(ref PeltzerMain.Instance.peltzerController) - || !PaletteController.AcquireIfNecessary(ref PeltzerMain.Instance.paletteController)) - return; - - bool userIsNowRightHanded = !PeltzerMain.Instance.peltzerControllerInRightHand; - PeltzerMain.Instance.peltzerControllerInRightHand = userIsNowRightHanded; - - // Record the user's preference for future sessions if the user is using Rift. - if (Config.Instance.VrHardware == VrHardware.Rift) { - PlayerPrefs.SetString(PeltzerMain.LEFT_HANDED_KEY, userIsNowRightHanded ? "false" : "true"); - - // Update the menu text & icon. - ObjectFinder.ObjectById("ID_user_is_right_handed").SetActive(userIsNowRightHanded); - ObjectFinder.ObjectById("ID_user_is_left_handed").SetActive(!userIsNowRightHanded); - } - - // Disable any active tooltips so they will not get stuck on the wrong controller. - PeltzerMain.Instance.peltzerController.HideTooltips(); - PeltzerMain.Instance.paletteController.HideTooltips(); - - // Set the application button and secondary button to be inactive on both controllers by default. - // If a tool wants it active it will set it itself. - PeltzerMain.Instance.peltzerController.SetApplicationButtonOverlay(ButtonMode.INACTIVE); - PeltzerMain.Instance.peltzerController.SetSecondaryButtonOverlay(/*active*/ false); - PeltzerMain.Instance.paletteController.SetApplicationButtonOverlay(/*active*/ false); - PeltzerMain.Instance.paletteController.SetSecondaryButtonOverlay(/*active*/ false); - - // Flip the Grab tool so it matches the current dominant hand. - GameObject grabTool = ObjectFinder.ObjectById("ID_ToolGrab"); - float grabToolScaleX = grabTool.transform.localScale.x; - grabTool.transform.localScale = new Vector3( - grabToolScaleX * -1, - grabTool.transform.localScale.y, - grabTool.transform.localScale.z); - - if (Config.Instance.sdkMode == SdkMode.SteamVR) { + /// + /// Swaps the controllers if both are available, else queues the swap until both are available. + /// + public void TrySwappingControllers() + { + waitingToSwapControllers = true; + + if (!PeltzerController.AcquireIfNecessary(ref PeltzerMain.Instance.peltzerController) + || !PaletteController.AcquireIfNecessary(ref PeltzerMain.Instance.paletteController)) + return; + + bool userIsNowRightHanded = !PeltzerMain.Instance.peltzerControllerInRightHand; + PeltzerMain.Instance.peltzerControllerInRightHand = userIsNowRightHanded; + + // Record the user's preference for future sessions if the user is using Rift. + if (Config.Instance.VrHardware == VrHardware.Rift) + { + PlayerPrefs.SetString(PeltzerMain.LEFT_HANDED_KEY, userIsNowRightHanded ? "false" : "true"); + + // Update the menu text & icon. + ObjectFinder.ObjectById("ID_user_is_right_handed").SetActive(userIsNowRightHanded); + ObjectFinder.ObjectById("ID_user_is_left_handed").SetActive(!userIsNowRightHanded); + } + + // Disable any active tooltips so they will not get stuck on the wrong controller. + PeltzerMain.Instance.peltzerController.HideTooltips(); + PeltzerMain.Instance.paletteController.HideTooltips(); + + // Set the application button and secondary button to be inactive on both controllers by default. + // If a tool wants it active it will set it itself. + PeltzerMain.Instance.peltzerController.SetApplicationButtonOverlay(ButtonMode.INACTIVE); + PeltzerMain.Instance.peltzerController.SetSecondaryButtonOverlay(/*active*/ false); + PeltzerMain.Instance.paletteController.SetApplicationButtonOverlay(/*active*/ false); + PeltzerMain.Instance.paletteController.SetSecondaryButtonOverlay(/*active*/ false); + + // Flip the Grab tool so it matches the current dominant hand. + GameObject grabTool = ObjectFinder.ObjectById("ID_ToolGrab"); + float grabToolScaleX = grabTool.transform.localScale.x; + grabTool.transform.localScale = new Vector3( + grabToolScaleX * -1, + grabTool.transform.localScale.y, + grabTool.transform.localScale.z); + + if (Config.Instance.sdkMode == SdkMode.SteamVR) + { #if STEAMVRBUILD - SteamVR_TrackedObject peltzerTrackedObj = peltzerController.GetComponent(); - SteamVR_TrackedObject paletteTrackedObj = paletteController.GetComponent(); - SteamVR_TrackedObject.EIndex tmp = peltzerTrackedObj.index; - peltzerTrackedObj.index = paletteTrackedObj.index; - paletteTrackedObj.index = tmp; + SteamVR_TrackedObject peltzerTrackedObj = peltzerController.GetComponent(); + SteamVR_TrackedObject paletteTrackedObj = paletteController.GetComponent(); + SteamVR_TrackedObject.EIndex tmp = peltzerTrackedObj.index; + peltzerTrackedObj.index = paletteTrackedObj.index; + paletteTrackedObj.index = tmp; #endif - } else if (Config.Instance.sdkMode == SdkMode.Oculus) { - ControllerDeviceOculus peltzerControllerDeviceOculus = (ControllerDeviceOculus)peltzerController.controller; - ControllerDeviceOculus paletteControllerDeviceOculus = (ControllerDeviceOculus)paletteController.controller; - if (peltzerControllerDeviceOculus.controllerType == OVRInput.Controller.LTouch) { - peltzerControllerDeviceOculus.controllerType = OVRInput.Controller.RTouch; - paletteControllerDeviceOculus.controllerType = OVRInput.Controller.LTouch; - } else { - peltzerControllerDeviceOculus.controllerType = OVRInput.Controller.LTouch; - paletteControllerDeviceOculus.controllerType = OVRInput.Controller.RTouch; + } + else if (Config.Instance.sdkMode == SdkMode.Oculus) + { + ControllerDeviceOculus peltzerControllerDeviceOculus = (ControllerDeviceOculus)peltzerController.controller; + ControllerDeviceOculus paletteControllerDeviceOculus = (ControllerDeviceOculus)paletteController.controller; + if (peltzerControllerDeviceOculus.controllerType == OVRInput.Controller.LTouch) + { + peltzerControllerDeviceOculus.controllerType = OVRInput.Controller.RTouch; + paletteControllerDeviceOculus.controllerType = OVRInput.Controller.LTouch; + } + else + { + peltzerControllerDeviceOculus.controllerType = OVRInput.Controller.LTouch; + paletteControllerDeviceOculus.controllerType = OVRInput.Controller.RTouch; + } + + Transform temp = Config.Instance.oculusHandTrackingManager.leftTransform; + Config.Instance.oculusHandTrackingManager.leftTransform = Config.Instance.oculusHandTrackingManager.rightTransform; + Config.Instance.oculusHandTrackingManager.rightTransform = temp; + } + + // For the Rift, we need to swap-back the controller geometry, such that the physical appearance of the + // controllers doesn't change. + if (Config.Instance.VrHardware == VrHardware.Rift) + { + if (Config.Instance.sdkMode == SdkMode.SteamVR) + { + paletteController.controllerGeometry.gameObject.transform.SetParent(peltzerController.steamRiftHolder.transform, /* worldPositionStays */ false); + peltzerController.controllerGeometry.gameObject.transform.SetParent(paletteController.steamRiftHolder.transform, /* worldPositionStays */ false); + } + else + { + paletteController.controllerGeometry.gameObject.transform.SetParent(peltzerController.oculusRiftHolder.transform, /* worldPositionStays */ false); + peltzerController.controllerGeometry.gameObject.transform.SetParent(paletteController.oculusRiftHolder.transform, /* worldPositionStays */ false); + } + + ControllerGeometry oldPaletteControllerGeometry = paletteController.controllerGeometry; + paletteController.controllerGeometry = peltzerController.controllerGeometry; + peltzerController.controllerGeometry = oldPaletteControllerGeometry; + + paletteController.controllerGeometry.baseControllerAnimation.SetControllerDevice(paletteController.controller); + peltzerController.controllerGeometry.baseControllerAnimation.SetControllerDevice(peltzerController.controller); + + peltzerController.ResetTouchpadOverlay(); + paletteController.ResetTouchpadOverlay(); + + peltzerController.BringAttachedToolheadToController(); + + PeltzerMain.Instance.ResolveControllerHandedness(); + } + waitingToSwapControllers = false; } - - Transform temp = Config.Instance.oculusHandTrackingManager.leftTransform; - Config.Instance.oculusHandTrackingManager.leftTransform = Config.Instance.oculusHandTrackingManager.rightTransform; - Config.Instance.oculusHandTrackingManager.rightTransform = temp; - } - - // For the Rift, we need to swap-back the controller geometry, such that the physical appearance of the - // controllers doesn't change. - if (Config.Instance.VrHardware == VrHardware.Rift) { - if (Config.Instance.sdkMode == SdkMode.SteamVR) { - paletteController.controllerGeometry.gameObject.transform.SetParent(peltzerController.steamRiftHolder.transform, /* worldPositionStays */ false); - peltzerController.controllerGeometry.gameObject.transform.SetParent(paletteController.steamRiftHolder.transform, /* worldPositionStays */ false); - } else { - paletteController.controllerGeometry.gameObject.transform.SetParent(peltzerController.oculusRiftHolder.transform, /* worldPositionStays */ false); - peltzerController.controllerGeometry.gameObject.transform.SetParent(paletteController.oculusRiftHolder.transform, /* worldPositionStays */ false); - } - - ControllerGeometry oldPaletteControllerGeometry = paletteController.controllerGeometry; - paletteController.controllerGeometry = peltzerController.controllerGeometry; - peltzerController.controllerGeometry = oldPaletteControllerGeometry; - - paletteController.controllerGeometry.baseControllerAnimation.SetControllerDevice(paletteController.controller); - peltzerController.controllerGeometry.baseControllerAnimation.SetControllerDevice(peltzerController.controller); - - peltzerController.ResetTouchpadOverlay(); - paletteController.ResetTouchpadOverlay(); - - peltzerController.BringAttachedToolheadToController(); - - PeltzerMain.Instance.ResolveControllerHandedness(); - } - waitingToSwapControllers = false; } - } } diff --git a/Assets/Scripts/model/controller/ControllerType.cs b/Assets/Scripts/model/controller/ControllerType.cs index 6e2df5d7..230eb5c5 100644 --- a/Assets/Scripts/model/controller/ControllerType.cs +++ b/Assets/Scripts/model/controller/ControllerType.cs @@ -15,24 +15,26 @@ using UnityEngine; using System.Collections; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Descriptor for types of controllers. - /// - public enum ControllerType { +namespace com.google.apps.peltzer.client.model.controller +{ /// - /// Null state. + /// Descriptor for types of controllers. /// - NONE, + public enum ControllerType + { + /// + /// Null state. + /// + NONE, - /// - /// The main peltzer controller. - /// - PELTZER, + /// + /// The main peltzer controller. + /// + PELTZER, - /// - /// The palette controller. - /// - PALETTE - } + /// + /// The palette controller. + /// + PALETTE + } } \ No newline at end of file diff --git a/Assets/Scripts/model/controller/DetailsMenuActionItem.cs b/Assets/Scripts/model/controller/DetailsMenuActionItem.cs index b5a0996e..9c532e26 100644 --- a/Assets/Scripts/model/controller/DetailsMenuActionItem.cs +++ b/Assets/Scripts/model/controller/DetailsMenuActionItem.cs @@ -15,23 +15,27 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.menu; -namespace com.google.apps.peltzer.client.model.controller { +namespace com.google.apps.peltzer.client.model.controller +{ - /// - /// SelectableMenuItem that can be attached to items on the palette file menu. - /// - public class DetailsMenuActionItem : PolyMenuButton { - public PolyMenuMain.DetailsMenuAction action; + /// + /// SelectableMenuItem that can be attached to items on the palette file menu. + /// + public class DetailsMenuActionItem : PolyMenuButton + { + public PolyMenuMain.DetailsMenuAction action; - public override void ApplyMenuOptions(PeltzerMain main) { - if (isActive) { - PeltzerMain.Instance.GetPolyMenuMain().InvokeDetailsMenuAction(action); - main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); + public override void ApplyMenuOptions(PeltzerMain main) + { + if (isActive) + { + PeltzerMain.Instance.GetPolyMenuMain().InvokeDetailsMenuAction(action); + main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); - // Bump down slightly and back up to its position, to provide a visual indication that - // the user's click was registered. - StartBump(); - } + // Bump down slightly and back up to its position, to provide a visual indication that + // the user's click was registered. + StartBump(); + } + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/controller/EmptyMenuItem.cs b/Assets/Scripts/model/controller/EmptyMenuItem.cs index 8b1e1190..970e2d96 100644 --- a/Assets/Scripts/model/controller/EmptyMenuItem.cs +++ b/Assets/Scripts/model/controller/EmptyMenuItem.cs @@ -14,15 +14,18 @@ using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.controller { +namespace com.google.apps.peltzer.client.model.controller +{ - /// - /// SelectableMenuItem that can be attached to items on the palette menu that are empty space. - /// This will allow us to hide the toolhead when hovering over nonbutton portions of the menu. - /// - public class EmptyMenuItem : SelectableMenuItem { - public override void ApplyMenuOptions(PeltzerMain main) { + /// + /// SelectableMenuItem that can be attached to items on the palette menu that are empty space. + /// This will allow us to hide the toolhead when hovering over nonbutton portions of the menu. + /// + public class EmptyMenuItem : SelectableMenuItem + { + public override void ApplyMenuOptions(PeltzerMain main) + { + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/controller/EnvironmentMenuItem.cs b/Assets/Scripts/model/controller/EnvironmentMenuItem.cs index e3588779..d946a8d1 100644 --- a/Assets/Scripts/model/controller/EnvironmentMenuItem.cs +++ b/Assets/Scripts/model/controller/EnvironmentMenuItem.cs @@ -15,35 +15,42 @@ using com.google.apps.peltzer.client.model.main; using UnityEngine; -namespace com.google.apps.peltzer.client.model.controller { +namespace com.google.apps.peltzer.client.model.controller +{ - /// - /// EnvironmentMenuItem that can be attached to an object that will trigger an environment change. - /// - public class EnvironmentMenuItem : SelectableMenuItem { - public EnvironmentThemeManager.EnvironmentTheme theme; - private GameObject selectedBorder; + /// + /// EnvironmentMenuItem that can be attached to an object that will trigger an environment change. + /// + public class EnvironmentMenuItem : SelectableMenuItem + { + public EnvironmentThemeManager.EnvironmentTheme theme; + private GameObject selectedBorder; - public void Start() { - selectedBorder = transform.Find("Selected").gameObject; - PeltzerMain.Instance.environmentThemeManager.EnvironmentThemeActionHandler += EnvironmentThemeActionHandler; - // We persist the last chosen environment in user prefs and set that during setup, - // however at that point this component has not started and thusly - // will not respond to the EnvironmentThemeActionHandler event. So we check here. - if (PeltzerMain.Instance.environmentThemeManager.currentTheme == theme) { - selectedBorder.SetActive(true); - } - } + public void Start() + { + selectedBorder = transform.Find("Selected").gameObject; + PeltzerMain.Instance.environmentThemeManager.EnvironmentThemeActionHandler += EnvironmentThemeActionHandler; + // We persist the last chosen environment in user prefs and set that during setup, + // however at that point this component has not started and thusly + // will not respond to the EnvironmentThemeActionHandler event. So we check here. + if (PeltzerMain.Instance.environmentThemeManager.currentTheme == theme) + { + selectedBorder.SetActive(true); + } + } - public override void ApplyMenuOptions(PeltzerMain main) { - if (main.environmentThemeManager != null) { - main.SetEnvironmentTheme(theme); - main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); - } - } + public override void ApplyMenuOptions(PeltzerMain main) + { + if (main.environmentThemeManager != null) + { + main.SetEnvironmentTheme(theme); + main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); + } + } - public void EnvironmentThemeActionHandler(object sender, EnvironmentThemeManager.EnvironmentTheme selectedTheme) { - selectedBorder.SetActive(selectedTheme == theme); + public void EnvironmentThemeActionHandler(object sender, EnvironmentThemeManager.EnvironmentTheme selectedTheme) + { + selectedBorder.SetActive(selectedTheme == theme); + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/controller/GrabToolheadAnimation.cs b/Assets/Scripts/model/controller/GrabToolheadAnimation.cs index ecab154c..293784c7 100644 --- a/Assets/Scripts/model/controller/GrabToolheadAnimation.cs +++ b/Assets/Scripts/model/controller/GrabToolheadAnimation.cs @@ -16,44 +16,51 @@ using System.Collections; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Animates the GrabToolhead in response to the controller's trigger value. - /// - public class GrabToolheadAnimation : MonoBehaviour { +namespace com.google.apps.peltzer.client.model.controller +{ + /// + /// Animates the GrabToolhead in response to the controller's trigger value. + /// + public class GrabToolheadAnimation : MonoBehaviour + { - PeltzerMain peltzerMain; - private Animator animator; - bool animationActive = false; + PeltzerMain peltzerMain; + private Animator animator; + bool animationActive = false; - void Start() { - animator = GetComponentInChildren(); - peltzerMain = FindObjectOfType(); - animator.speed = 0; - } + void Start() + { + animator = GetComponentInChildren(); + peltzerMain = FindObjectOfType(); + animator.speed = 0; + } - /// - /// Scrubs through grabbing animation based on controller's trigger value. - /// - private void Update() { - if (animationActive) { - animator.Play("grab", -1, - peltzerMain.peltzerController.controller.GetTriggerScale().x * .4f + .1f); - } - } + /// + /// Scrubs through grabbing animation based on controller's trigger value. + /// + private void Update() + { + if (animationActive) + { + animator.Play("grab", -1, + peltzerMain.peltzerController.controller.GetTriggerScale().x * .4f + .1f); + } + } - /// - /// Activate the animation logic by setting flag. - /// - public void Activate() { - animationActive = true; - } + /// + /// Activate the animation logic by setting flag. + /// + public void Activate() + { + animationActive = true; + } - /// - /// Deactivates the animation logic by setting flag. - /// - public void Deactivate() { - animationActive = false; + /// + /// Deactivates the animation logic by setting flag. + /// + public void Deactivate() + { + animationActive = false; + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/controller/HapticFeedback.cs b/Assets/Scripts/model/controller/HapticFeedback.cs index 55cae8c6..c39fa34a 100644 --- a/Assets/Scripts/model/controller/HapticFeedback.cs +++ b/Assets/Scripts/model/controller/HapticFeedback.cs @@ -15,398 +15,445 @@ using UnityEngine; using System.Collections; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Haptic feedback implementations for the Peltzer app. - /// * Holds a reference to associated SteamVR_Controller.Device. - /// * Abstracts complex vibration pattern on device. - /// - public class HapticFeedback : MonoBehaviour { +namespace com.google.apps.peltzer.client.model.controller +{ /// - /// Placeholder enum for various feedback types. As feedback becomes - /// better characterized via ongoing testing to more meaningful names, - /// these will be updated e.g. CAUTION, CONFIRMATION, etc. + /// Haptic feedback implementations for the Peltzer app. + /// * Holds a reference to associated SteamVR_Controller.Device. + /// * Abstracts complex vibration pattern on device. /// - public enum HapticFeedbackType { - FEEDBACK_1, - FEEDBACK_2, - FEEDBACK_3, - FEEDBACK_4, - FEEDBACK_5, - FEEDBACK_6, - FEEDBACK_7, - FEEDBACK_8, - FEEDBACK_9, - FEEDBACK_10, - FEEDBACK_11, - FEEDBACK_12, - FEEDBACK_13, - FEEDBACK_14, - FEEDBACK_15, - FEEDBACK_16, - FEEDBACK_17, - FEEDBACK_18 - } - - // Device instance. - public ControllerDevice controller; - private bool startVibration = false; + public class HapticFeedback : MonoBehaviour + { + /// + /// Placeholder enum for various feedback types. As feedback becomes + /// better characterized via ongoing testing to more meaningful names, + /// these will be updated e.g. CAUTION, CONFIRMATION, etc. + /// + public enum HapticFeedbackType + { + FEEDBACK_1, + FEEDBACK_2, + FEEDBACK_3, + FEEDBACK_4, + FEEDBACK_5, + FEEDBACK_6, + FEEDBACK_7, + FEEDBACK_8, + FEEDBACK_9, + FEEDBACK_10, + FEEDBACK_11, + FEEDBACK_12, + FEEDBACK_13, + FEEDBACK_14, + FEEDBACK_15, + FEEDBACK_16, + FEEDBACK_17, + FEEDBACK_18 + } - //length is how long the vibration should go for - //strength is vibration strength from 0-1 - IEnumerator LongVibration(float length, float strength) { - for (float i = 0; i < length; i += Time.deltaTime) { - if (controller != null) { - controller.TriggerHapticPulse((ushort)Mathf.Lerp(0, 3999, strength)); + // Device instance. + public ControllerDevice controller; + private bool startVibration = false; + + //length is how long the vibration should go for + //strength is vibration strength from 0-1 + IEnumerator LongVibration(float length, float strength) + { + for (float i = 0; i < length; i += Time.deltaTime) + { + if (controller != null) + { + controller.TriggerHapticPulse((ushort)Mathf.Lerp(0, 3999, strength)); + } + yield return null; + } } - yield return null; - } - } - //vibrationCount is how many vibrations - //vibrationLength is how long each vibration should go for - //gapLength is how long to wait between vibrations - //strength is vibration strength from 0-1 - IEnumerator LongVibration(int vibrationCount, float[] vibrationLength, float gapLength, float[] strength) { - for (int i = 0; i < vibrationCount; i++) { - if (i != 0) yield return new WaitForSeconds(gapLength); - yield return StartCoroutine(LongVibration(vibrationLength[i], Mathf.Clamp01(strength[i]))); - } - } + //vibrationCount is how many vibrations + //vibrationLength is how long each vibration should go for + //gapLength is how long to wait between vibrations + //strength is vibration strength from 0-1 + IEnumerator LongVibration(int vibrationCount, float[] vibrationLength, float gapLength, float[] strength) + { + for (int i = 0; i < vibrationCount; i++) + { + if (i != 0) yield return new WaitForSeconds(gapLength); + yield return StartCoroutine(LongVibration(vibrationLength[i], Mathf.Clamp01(strength[i]))); + } + } - /// - /// Signal A - /// * Atomic Signals from which larger patterns are constructed. - /// - IEnumerator SignalA(float length, float strength) { - float[] l = { length }; - float[] s = { strength }; - StartCoroutine(LongVibration(1, l, 0, s)); - yield return null; - } + /// + /// Signal A + /// * Atomic Signals from which larger patterns are constructed. + /// + IEnumerator SignalA(float length, float strength) + { + float[] l = { length }; + float[] s = { strength }; + StartCoroutine(LongVibration(1, l, 0, s)); + yield return null; + } - /// - /// Signal B - /// * Atomic Signals from which larger patterns are constructed. - /// - IEnumerator SignalB(float length, float strength) { - for (float i = 0; i < length; i += Time.deltaTime) { - ushort s = (ushort)Mathf.Lerp(0, 3999, Mathf.Lerp(0, strength, (i / length))); - controller.TriggerHapticPulse(s); - yield return null; - } - } + /// + /// Signal B + /// * Atomic Signals from which larger patterns are constructed. + /// + IEnumerator SignalB(float length, float strength) + { + for (float i = 0; i < length; i += Time.deltaTime) + { + ushort s = (ushort)Mathf.Lerp(0, 3999, Mathf.Lerp(0, strength, (i / length))); + controller.TriggerHapticPulse(s); + yield return null; + } + } - /// - /// Signal C - /// * Atomic Signals from which larger patterns are constructed. - /// - IEnumerator SignalC(float length, float strength) { - for (float i = length; i > 0; i -= Time.deltaTime) { - ushort s = (ushort)Mathf.Lerp(0, 3999, Mathf.Lerp(0, strength, (i / length))); - controller.TriggerHapticPulse(s); - yield return null; - } - } + /// + /// Signal C + /// * Atomic Signals from which larger patterns are constructed. + /// + IEnumerator SignalC(float length, float strength) + { + for (float i = length; i > 0; i -= Time.deltaTime) + { + ushort s = (ushort)Mathf.Lerp(0, 3999, Mathf.Lerp(0, strength, (i / length))); + controller.TriggerHapticPulse(s); + yield return null; + } + } - /// - /// Signal D - /// * Atomic Signals from which larger patterns are constructed. - /// - IEnumerator SignalD(float length, float strength) { - for (float i = 0; i < length; i += Time.deltaTime) { - ushort s = (ushort)Mathf.Lerp(0, 3999, Mathf.Lerp(0, strength, (i / length) * (i / length))); - controller.TriggerHapticPulse(s); - yield return null; - } - } + /// + /// Signal D + /// * Atomic Signals from which larger patterns are constructed. + /// + IEnumerator SignalD(float length, float strength) + { + for (float i = 0; i < length; i += Time.deltaTime) + { + ushort s = (ushort)Mathf.Lerp(0, 3999, Mathf.Lerp(0, strength, (i / length) * (i / length))); + controller.TriggerHapticPulse(s); + yield return null; + } + } - /// - /// Signal E - /// * Atomic Signals from which larger patterns are constructed. - /// - IEnumerator SignalE(float length, float strength) { - for (float i = length; i > 0; i -= Time.deltaTime) { - ushort s = (ushort)Mathf.Lerp(0, 3999, Mathf.Lerp(0, strength, (i / length) * (i / length))); - controller.TriggerHapticPulse(s); - yield return null; - } - } + /// + /// Signal E + /// * Atomic Signals from which larger patterns are constructed. + /// + IEnumerator SignalE(float length, float strength) + { + for (float i = length; i > 0; i -= Time.deltaTime) + { + ushort s = (ushort)Mathf.Lerp(0, 3999, Mathf.Lerp(0, strength, (i / length) * (i / length))); + controller.TriggerHapticPulse(s); + yield return null; + } + } - /// - /// Calls patterns in sequence. - /// - private static IEnumerator Sequence(params IEnumerator[] sequence) { - for (int i = 0; i < sequence.Length; ++i) { - while (sequence[i].MoveNext()) - yield return sequence[i].Current; - } - } + /// + /// Calls patterns in sequence. + /// + private static IEnumerator Sequence(params IEnumerator[] sequence) + { + for (int i = 0; i < sequence.Length; ++i) + { + while (sequence[i].MoveNext()) + yield return sequence[i].Current; + } + } - /// - /// Pattern 7 - /// * Pattern constructed from Signal B -> Signal B. - /// - private void Pattern_7(float length, float strength) { - StartCoroutine(Sequence(SignalB(length, strength), SignalB(length, strength))); - } + /// + /// Pattern 7 + /// * Pattern constructed from Signal B -> Signal B. + /// + private void Pattern_7(float length, float strength) + { + StartCoroutine(Sequence(SignalB(length, strength), SignalB(length, strength))); + } - /// - /// Pattern 8 - /// * Pattern constructed from Signal C. - /// - private void Pattern_8(float length, float strength) { - StartCoroutine(SignalC(length, strength)); - } + /// + /// Pattern 8 + /// * Pattern constructed from Signal C. + /// + private void Pattern_8(float length, float strength) + { + StartCoroutine(SignalC(length, strength)); + } - /// - /// Pattern 9 - /// * Pattern constructed from Signal C -> Signal C. - /// - private void Pattern_9(float length, float strength) { - StartCoroutine(Sequence(SignalC(length, strength), SignalC(length, strength))); - } + /// + /// Pattern 9 + /// * Pattern constructed from Signal C -> Signal C. + /// + private void Pattern_9(float length, float strength) + { + StartCoroutine(Sequence(SignalC(length, strength), SignalC(length, strength))); + } - /// - /// Pattern 10 - /// * Pattern constructed from Signal B -> Signal C. - /// - private void Pattern_10(float length, float strength) { - StartCoroutine(Sequence(SignalB(length, strength), SignalC(length, strength))); - } + /// + /// Pattern 10 + /// * Pattern constructed from Signal B -> Signal C. + /// + private void Pattern_10(float length, float strength) + { + StartCoroutine(Sequence(SignalB(length, strength), SignalC(length, strength))); + } - /// - /// Pattern 11 - /// * Pattern constructed from Signal C -> Signal B. - /// - private void Pattern_11(float length, float strength) { - StartCoroutine(Sequence(SignalC(length, strength), SignalB(length, strength))); - } + /// + /// Pattern 11 + /// * Pattern constructed from Signal C -> Signal B. + /// + private void Pattern_11(float length, float strength) + { + StartCoroutine(Sequence(SignalC(length, strength), SignalB(length, strength))); + } - /// - /// Pattern 12 - /// * Pattern constructed from Signal A -> Signal A. - /// - private void Pattern_12(float length, float strength) { - StartCoroutine(Sequence(SignalA(length, strength / 2), SignalA(length, strength))); - } + /// + /// Pattern 12 + /// * Pattern constructed from Signal A -> Signal A. + /// + private void Pattern_12(float length, float strength) + { + StartCoroutine(Sequence(SignalA(length, strength / 2), SignalA(length, strength))); + } - /// - /// Pattern 13 - /// * Pattern constructed from Signal A -> Signal A. - /// - private void Pattern_13(float length, float strength) { - StartCoroutine(Sequence(SignalA(length, strength), SignalA(length, strength / 2))); - } + /// + /// Pattern 13 + /// * Pattern constructed from Signal A -> Signal A. + /// + private void Pattern_13(float length, float strength) + { + StartCoroutine(Sequence(SignalA(length, strength), SignalA(length, strength / 2))); + } - /// - /// Stops vibration of undetermined length. - /// - private void StopVibration() { - startVibration = false; - } + /// + /// Stops vibration of undetermined length. + /// + private void StopVibration() + { + startVibration = false; + } - /// - /// Plays back the specified the feedback type at the given length and strength specified. - /// - /// The feedback pattern type. - /// Duration of feedback in seconds. - /// Strength value normaled between 0 and 1. - public void PlayHapticFeedback(HapticFeedbackType feedbackType, float length, float strength) { - if (controller == null || !gameObject.activeInHierarchy) return; - - switch (feedbackType) { - case HapticFeedbackType.FEEDBACK_1: - Feedback1(length, strength); - break; - case HapticFeedbackType.FEEDBACK_2: - Feedback2(length, strength); - break; - case HapticFeedbackType.FEEDBACK_3: - Feedback3(length, strength); - break; - case HapticFeedbackType.FEEDBACK_4: - Feedback4(length, strength); - break; - case HapticFeedbackType.FEEDBACK_5: - Feedback5(length, strength); - break; - case HapticFeedbackType.FEEDBACK_6: - Feedback6(length, strength); - break; - case HapticFeedbackType.FEEDBACK_7: - Feedback7(length, strength); - break; - case HapticFeedbackType.FEEDBACK_8: - Feedback8(length, strength); - break; - case HapticFeedbackType.FEEDBACK_9: - Feedback9(length, strength); - break; - case HapticFeedbackType.FEEDBACK_10: - Feedback10(length, strength); - break; - case HapticFeedbackType.FEEDBACK_11: - Feedback11(length, strength); - break; - case HapticFeedbackType.FEEDBACK_12: - Feedback12(length, strength); - break; - case HapticFeedbackType.FEEDBACK_13: - Feedback13(length, strength); - break; - case HapticFeedbackType.FEEDBACK_14: - Feedback14(length, strength); - break; - case HapticFeedbackType.FEEDBACK_15: - Feedback15(length, strength); - break; - case HapticFeedbackType.FEEDBACK_16: - Feedback16(length, strength); - break; - case HapticFeedbackType.FEEDBACK_17: - Feedback17(length, strength); - break; - case HapticFeedbackType.FEEDBACK_18: - Feedback18(length, strength); - break; - default: - break; - } - } + /// + /// Plays back the specified the feedback type at the given length and strength specified. + /// + /// The feedback pattern type. + /// Duration of feedback in seconds. + /// Strength value normaled between 0 and 1. + public void PlayHapticFeedback(HapticFeedbackType feedbackType, float length, float strength) + { + if (controller == null || !gameObject.activeInHierarchy) return; + + switch (feedbackType) + { + case HapticFeedbackType.FEEDBACK_1: + Feedback1(length, strength); + break; + case HapticFeedbackType.FEEDBACK_2: + Feedback2(length, strength); + break; + case HapticFeedbackType.FEEDBACK_3: + Feedback3(length, strength); + break; + case HapticFeedbackType.FEEDBACK_4: + Feedback4(length, strength); + break; + case HapticFeedbackType.FEEDBACK_5: + Feedback5(length, strength); + break; + case HapticFeedbackType.FEEDBACK_6: + Feedback6(length, strength); + break; + case HapticFeedbackType.FEEDBACK_7: + Feedback7(length, strength); + break; + case HapticFeedbackType.FEEDBACK_8: + Feedback8(length, strength); + break; + case HapticFeedbackType.FEEDBACK_9: + Feedback9(length, strength); + break; + case HapticFeedbackType.FEEDBACK_10: + Feedback10(length, strength); + break; + case HapticFeedbackType.FEEDBACK_11: + Feedback11(length, strength); + break; + case HapticFeedbackType.FEEDBACK_12: + Feedback12(length, strength); + break; + case HapticFeedbackType.FEEDBACK_13: + Feedback13(length, strength); + break; + case HapticFeedbackType.FEEDBACK_14: + Feedback14(length, strength); + break; + case HapticFeedbackType.FEEDBACK_15: + Feedback15(length, strength); + break; + case HapticFeedbackType.FEEDBACK_16: + Feedback16(length, strength); + break; + case HapticFeedbackType.FEEDBACK_17: + Feedback17(length, strength); + break; + case HapticFeedbackType.FEEDBACK_18: + Feedback18(length, strength); + break; + default: + break; + } + } - /// - /// Feedback 1 - /// - private void Feedback1(float length, float strength) { - StartCoroutine(SignalA(length, strength)); - } + /// + /// Feedback 1 + /// + private void Feedback1(float length, float strength) + { + StartCoroutine(SignalA(length, strength)); + } - /// - /// Feedback 2 - /// - private void Feedback2(float length, float strength) { - StartCoroutine(SignalB(length, strength)); - } + /// + /// Feedback 2 + /// + private void Feedback2(float length, float strength) + { + StartCoroutine(SignalB(length, strength)); + } - /// - /// Feedback 3 - /// - private void Feedback3(float length, float strength) { - StartCoroutine(SignalC(length, strength)); - } + /// + /// Feedback 3 + /// + private void Feedback3(float length, float strength) + { + StartCoroutine(SignalC(length, strength)); + } - /// - /// Feedback 4 - /// - private void Feedback4(float length, float strength) { - StartCoroutine(SignalD(length, strength)); - } + /// + /// Feedback 4 + /// + private void Feedback4(float length, float strength) + { + StartCoroutine(SignalD(length, strength)); + } - /// - /// Feedback 5 - /// - private void Feedback5(float length, float strength) { - StartCoroutine(SignalE(length, strength)); - } + /// + /// Feedback 5 + /// + private void Feedback5(float length, float strength) + { + StartCoroutine(SignalE(length, strength)); + } - /// - /// Feedback 6 - /// - private void Feedback6(float length, float strength) { - StartCoroutine(SignalA(length, strength)); - } + /// + /// Feedback 6 + /// + private void Feedback6(float length, float strength) + { + StartCoroutine(SignalA(length, strength)); + } - /// - /// Feedback 7 - /// - private void Feedback7(float length, float strength) { - float[] _l = { length, length }; - float[] _s = { strength, strength }; - StartCoroutine(LongVibration(2, _l, .05f, _s)); - } + /// + /// Feedback 7 + /// + private void Feedback7(float length, float strength) + { + float[] _l = { length, length }; + float[] _s = { strength, strength }; + StartCoroutine(LongVibration(2, _l, .05f, _s)); + } - /// - /// Feedback 8 - /// - private void Feedback8(float length, float strength) { - float[] _l = { length, length, length }; - float[] _s = { strength, strength, strength }; - StartCoroutine(LongVibration(3, _l, .05f, _s)); - } + /// + /// Feedback 8 + /// + private void Feedback8(float length, float strength) + { + float[] _l = { length, length, length }; + float[] _s = { strength, strength, strength }; + StartCoroutine(LongVibration(3, _l, .05f, _s)); + } - /// - /// Feedback 9 - /// - private void Feedback9(float length, float strength) { - float[] _l = { length * 2, length }; - float[] _s = { strength, strength }; - StartCoroutine(LongVibration(2, _l, .05f, _s)); - } + /// + /// Feedback 9 + /// + private void Feedback9(float length, float strength) + { + float[] _l = { length * 2, length }; + float[] _s = { strength, strength }; + StartCoroutine(LongVibration(2, _l, .05f, _s)); + } - /// - /// Feedback 10 - /// - private void Feedback10(float length, float strength) { - float[] _l = { length, length * 2 }; - float[] _s = { strength, strength }; - StartCoroutine(LongVibration(2, _l, .05f, _s)); - } + /// + /// Feedback 10 + /// + private void Feedback10(float length, float strength) + { + float[] _l = { length, length * 2 }; + float[] _s = { strength, strength }; + StartCoroutine(LongVibration(2, _l, .05f, _s)); + } - /// - /// Feedback 11 - /// - private void Feedback11(float length, float strength) { - StartCoroutine(SignalB(length, strength)); - } + /// + /// Feedback 11 + /// + private void Feedback11(float length, float strength) + { + StartCoroutine(SignalB(length, strength)); + } - /// - /// Feedback 12 - /// - private void Feedback12(float length, float strength) { - Pattern_7(length, strength); - } + /// + /// Feedback 12 + /// + private void Feedback12(float length, float strength) + { + Pattern_7(length, strength); + } - /// - /// Feedback 13 - /// - private void Feedback13(float length, float strength) { - Pattern_8(length, strength); - } + /// + /// Feedback 13 + /// + private void Feedback13(float length, float strength) + { + Pattern_8(length, strength); + } - /// - /// Feedback 14 - /// - private void Feedback14(float length, float strength) { - Pattern_9(length, strength); - } + /// + /// Feedback 14 + /// + private void Feedback14(float length, float strength) + { + Pattern_9(length, strength); + } - /// - /// Feedback 15 - /// - private void Feedback15(float length, float strength) { - Pattern_10(length, strength); - } + /// + /// Feedback 15 + /// + private void Feedback15(float length, float strength) + { + Pattern_10(length, strength); + } - /// - /// Feedback 16 - /// - private void Feedback16(float length, float strength) { - Pattern_11(length, strength); - } + /// + /// Feedback 16 + /// + private void Feedback16(float length, float strength) + { + Pattern_11(length, strength); + } - /// - /// Feedback 17 - /// - private void Feedback17(float length, float strength) { - Pattern_12(length, strength); - } + /// + /// Feedback 17 + /// + private void Feedback17(float length, float strength) + { + Pattern_12(length, strength); + } - /// - /// Feedback 18 - /// - private void Feedback18(float length, float strength) { - Pattern_13(length, strength); + /// + /// Feedback 18 + /// + private void Feedback18(float length, float strength) + { + Pattern_13(length, strength); + } } - } } diff --git a/Assets/Scripts/model/controller/MenuActionItem.cs b/Assets/Scripts/model/controller/MenuActionItem.cs index 24a02139..23470910 100644 --- a/Assets/Scripts/model/controller/MenuActionItem.cs +++ b/Assets/Scripts/model/controller/MenuActionItem.cs @@ -15,29 +15,33 @@ using System; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.controller { +namespace com.google.apps.peltzer.client.model.controller +{ - /// - /// SelectableMenuItem that can be attached to items on the palette file menu. - /// - public class MenuActionItem : PolyMenuButton { - public MenuAction action; /// - /// Returns whether or not action is currently allowed by the restriction manager. + /// SelectableMenuItem that can be attached to items on the palette file menu. /// - /// Whether or not action is allowed. - internal bool ActionIsAllowed() { - return (PeltzerMain.Instance.restrictionManager.tutorialMenuActionsAllowed - && PeltzerMain.TUTORIAL_MENU_ACTIONS.Contains(action)) - || PeltzerMain.Instance.restrictionManager.menuActionsAllowed; - } + public class MenuActionItem : PolyMenuButton + { + public MenuAction action; + /// + /// Returns whether or not action is currently allowed by the restriction manager. + /// + /// Whether or not action is allowed. + internal bool ActionIsAllowed() + { + return (PeltzerMain.Instance.restrictionManager.tutorialMenuActionsAllowed + && PeltzerMain.TUTORIAL_MENU_ACTIONS.Contains(action)) + || PeltzerMain.Instance.restrictionManager.menuActionsAllowed; + } - public override void ApplyMenuOptions(PeltzerMain main) { - if (!ActionIsAllowed()) return; + public override void ApplyMenuOptions(PeltzerMain main) + { + if (!ActionIsAllowed()) return; - main.InvokeMenuAction(action); - main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); - StartBump(); + main.InvokeMenuAction(action); + main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); + StartBump(); + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/controller/ModifyToolheadAnimation.cs b/Assets/Scripts/model/controller/ModifyToolheadAnimation.cs index 4979483e..4dfb02ba 100644 --- a/Assets/Scripts/model/controller/ModifyToolheadAnimation.cs +++ b/Assets/Scripts/model/controller/ModifyToolheadAnimation.cs @@ -16,76 +16,91 @@ using System.Collections; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Animates the ModifyToolhead in response to our event system. - /// - public class ModifyToolheadAnimation : MonoBehaviour { +namespace com.google.apps.peltzer.client.model.controller +{ + /// + /// Animates the ModifyToolhead in response to our event system. + /// + public class ModifyToolheadAnimation : MonoBehaviour + { - public PeltzerMain peltzerMain; - public ControllerMain controllerMain; - public PeltzerController peltzerController; - private Transform leftArm, rightArm; - private Vector3 leftArmOpen = new Vector3(0f, -8.025001f, 0f); - private Vector3 rightArmOpen = new Vector3(0f, 8.025001f, 0f); + public PeltzerMain peltzerMain; + public ControllerMain controllerMain; + public PeltzerController peltzerController; + private Transform leftArm, rightArm; + private Vector3 leftArmOpen = new Vector3(0f, -8.025001f, 0f); + private Vector3 rightArmOpen = new Vector3(0f, 8.025001f, 0f); - // Use this for initialization - void Start() { - leftArm = transform.Find("modifyTool_GEO/Plier_Geo/Plier_L_Geo"); - rightArm = transform.Find("modifyTool_GEO/Plier_Geo/Plier_R_Geo"); - peltzerMain = FindObjectOfType(); - controllerMain = peltzerMain.controllerMain; - } + // Use this for initialization + void Start() + { + leftArm = transform.Find("modifyTool_GEO/Plier_Geo/Plier_L_Geo"); + rightArm = transform.Find("modifyTool_GEO/Plier_Geo/Plier_R_Geo"); + peltzerMain = FindObjectOfType(); + controllerMain = peltzerMain.controllerMain; + } - /// - /// An event handler that listens for controller input and delegates accordingly. - /// - /// The sender of the controller event. - /// The controller event arguments. - private void ControllerEventHandler(object sender, ControllerEventArgs args) { - if(args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger) { - if(args.Action == ButtonAction.DOWN) { - StartAnimation(); - } else if(args.Action == ButtonAction.UP) { - StopAnimation(); + /// + /// An event handler that listens for controller input and delegates accordingly. + /// + /// The sender of the controller event. + /// The controller event arguments. + private void ControllerEventHandler(object sender, ControllerEventArgs args) + { + if (args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger) + { + if (args.Action == ButtonAction.DOWN) + { + StartAnimation(); + } + else if (args.Action == ButtonAction.UP) + { + StopAnimation(); + } + } } - } - } - /// - /// Activates the animation logic by attaching the event handler for input. - /// - public void Activate() { - controllerMain.ControllerActionHandler += ControllerEventHandler; - } + /// + /// Activates the animation logic by attaching the event handler for input. + /// + public void Activate() + { + controllerMain.ControllerActionHandler += ControllerEventHandler; + } - /// - /// Deactivates the animation logic by removing the event handler for input. - /// - public void Deactivate() { - controllerMain.ControllerActionHandler -= ControllerEventHandler; - } + /// + /// Deactivates the animation logic by removing the event handler for input. + /// + public void Deactivate() + { + controllerMain.ControllerActionHandler -= ControllerEventHandler; + } - /// - /// Entry point for actual animation which is to "close" the head grips of the tool. - /// - private void StartAnimation() { - if (leftArm) { - leftArm.localEulerAngles = rightArm.localEulerAngles = Vector3.zero; - } - } + /// + /// Entry point for actual animation which is to "close" the head grips of the tool. + /// + private void StartAnimation() + { + if (leftArm) + { + leftArm.localEulerAngles = rightArm.localEulerAngles = Vector3.zero; + } + } - /// - /// Entry point for the animation which is to "open" / "relax" the head grips of the tool. - /// - private void StopAnimation() { - if (leftArm) { - leftArm.localEulerAngles = leftArmOpen; - } - if (rightArm) { - rightArm.localEulerAngles = rightArmOpen; - } + /// + /// Entry point for the animation which is to "open" / "relax" the head grips of the tool. + /// + private void StopAnimation() + { + if (leftArm) + { + leftArm.localEulerAngles = leftArmOpen; + } + if (rightArm) + { + rightArm.localEulerAngles = rightArmOpen; + } + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/controller/OculusHandTrackingManager.cs b/Assets/Scripts/model/controller/OculusHandTrackingManager.cs index 514df4e0..290efc75 100644 --- a/Assets/Scripts/model/controller/OculusHandTrackingManager.cs +++ b/Assets/Scripts/model/controller/OculusHandTrackingManager.cs @@ -15,45 +15,50 @@ using com.google.apps.peltzer.client.model.main; using UnityEngine; -namespace com.google.apps.peltzer.client.model.controller { - public class OculusHandTrackingManager : MonoBehaviour { - // These are set to null to remove warning log. - public Transform leftTransform = null; - public Transform rightTransform = null; +namespace com.google.apps.peltzer.client.model.controller +{ + public class OculusHandTrackingManager : MonoBehaviour + { + // These are set to null to remove warning log. + public Transform leftTransform = null; + public Transform rightTransform = null; - // This class tries to mimic the tracking done in SteamVR's SteamVR_TrackedObject, which updates - // poses in response to the event "new_poses". This event is sent in - // SteamVR_UpdatePoses.OnPreCull(). But OnPreCull() is only available to components attached to - // the camera, which this class is not. So this public OnPreCull() is called exactly once - // from the OculusVideoRendering.OnPreCull() which is attached to the camera. - void Update() { - // Adding in additional checks to make sure for each controller instead of a single check for - // both so the player will know if either controller is having problems. - bool touchControllersConnected = - (OVRInput.GetConnectedControllers() & OVRInput.Controller.Touch) - == OVRInput.Controller.Touch; - bool leftTouchValid = false; - if (OVRInput.GetControllerOrientationTracked(OVRInput.Controller.LTouch) || - OVRInput.GetControllerPositionTracked(OVRInput.Controller.LTouch) && - touchControllersConnected) { - leftTouchValid = true; - } + // This class tries to mimic the tracking done in SteamVR's SteamVR_TrackedObject, which updates + // poses in response to the event "new_poses". This event is sent in + // SteamVR_UpdatePoses.OnPreCull(). But OnPreCull() is only available to components attached to + // the camera, which this class is not. So this public OnPreCull() is called exactly once + // from the OculusVideoRendering.OnPreCull() which is attached to the camera. + void Update() + { + // Adding in additional checks to make sure for each controller instead of a single check for + // both so the player will know if either controller is having problems. + bool touchControllersConnected = + (OVRInput.GetConnectedControllers() & OVRInput.Controller.Touch) + == OVRInput.Controller.Touch; + bool leftTouchValid = false; + if (OVRInput.GetControllerOrientationTracked(OVRInput.Controller.LTouch) || + OVRInput.GetControllerPositionTracked(OVRInput.Controller.LTouch) && + touchControllersConnected) + { + leftTouchValid = true; + } - bool rightTouchValid = false; - if (OVRInput.GetControllerOrientationTracked(OVRInput.Controller.RTouch) || - OVRInput.GetControllerPositionTracked(OVRInput.Controller.RTouch) && - touchControllersConnected) { - rightTouchValid = true; - } + bool rightTouchValid = false; + if (OVRInput.GetControllerOrientationTracked(OVRInput.Controller.RTouch) || + OVRInput.GetControllerPositionTracked(OVRInput.Controller.RTouch) && + touchControllersConnected) + { + rightTouchValid = true; + } - PeltzerMain.Instance.paletteController.controller.IsTrackedObjectValid = leftTouchValid; - PeltzerMain.Instance.peltzerController.controller.IsTrackedObjectValid = rightTouchValid; + PeltzerMain.Instance.paletteController.controller.IsTrackedObjectValid = leftTouchValid; + PeltzerMain.Instance.peltzerController.controller.IsTrackedObjectValid = rightTouchValid; - leftTransform.localRotation = OVRInput.GetLocalControllerRotation(OVRInput.Controller.LTouch); - rightTransform.localRotation = OVRInput.GetLocalControllerRotation(OVRInput.Controller.RTouch); + leftTransform.localRotation = OVRInput.GetLocalControllerRotation(OVRInput.Controller.LTouch); + rightTransform.localRotation = OVRInput.GetLocalControllerRotation(OVRInput.Controller.RTouch); - leftTransform.localPosition = OVRInput.GetLocalControllerPosition(OVRInput.Controller.LTouch); - rightTransform.localPosition = OVRInput.GetLocalControllerPosition(OVRInput.Controller.RTouch); + leftTransform.localPosition = OVRInput.GetLocalControllerPosition(OVRInput.Controller.LTouch); + rightTransform.localPosition = OVRInput.GetLocalControllerPosition(OVRInput.Controller.RTouch); + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/controller/PaintToolheadAnimation.cs b/Assets/Scripts/model/controller/PaintToolheadAnimation.cs index c851d19c..8b91be74 100644 --- a/Assets/Scripts/model/controller/PaintToolheadAnimation.cs +++ b/Assets/Scripts/model/controller/PaintToolheadAnimation.cs @@ -15,130 +15,161 @@ using com.google.apps.peltzer.client.model.main; using UnityEngine; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Animates the PaintToolhead in response to our event system. - /// - public class PaintToolheadAnimation : MonoBehaviour { - - public PeltzerMain peltzerMain; - public ControllerMain controllerMain; - public PeltzerController peltzerController; - private Animator animator; - private bool isActivated = false; - private bool isPointed = false; - private Vector3 lastPosition; - private float lastVelocity = 0f; - - private readonly float MAX_VELOCITY_THRESHOLD = 3; - - private enum Direction { - UP, DOWN, LEFT, RIGHT, NONE - } +namespace com.google.apps.peltzer.client.model.controller +{ + /// + /// Animates the PaintToolhead in response to our event system. + /// + public class PaintToolheadAnimation : MonoBehaviour + { - // Use this for initialization - void Start() { - animator = transform.Find("BrushRigMesh").GetComponent(); - peltzerMain = FindObjectOfType(); - controllerMain = peltzerMain.controllerMain; - } + public PeltzerMain peltzerMain; + public ControllerMain controllerMain; + public PeltzerController peltzerController; + private Animator animator; + private bool isActivated = false; + private bool isPointed = false; + private Vector3 lastPosition; + private float lastVelocity = 0f; - void Update() { - if (isActivated) { - float dist = Vector3.Distance(lastPosition, peltzerController.transform.position); - float velocity = dist / Time.deltaTime; - Vector3 direction = transform.InverseTransformDirection(peltzerController.transform.position - lastPosition); - - Direction dir = Direction.NONE; - - if (Mathf.Abs(direction.x) > Mathf.Abs(direction.y)) { - if (direction.x < 0) { - dir = Direction.LEFT; - } else if (direction.x > 0) { - dir = Direction.RIGHT; - } - } else if (Mathf.Abs(direction.x) < Mathf.Abs(direction.y)) { - if (direction.y < 0) { - dir = Direction.DOWN; - } else if (direction.y > 0) { - dir = Direction.UP; - } + private readonly float MAX_VELOCITY_THRESHOLD = 3; + + private enum Direction + { + UP, DOWN, LEFT, RIGHT, NONE } - - lastPosition = peltzerController.transform.position; - - if (animator != null) { - switch (dir) { - case Direction.DOWN: - animator.Play("Up", 0, (velocity / MAX_VELOCITY_THRESHOLD > 1 ? 1 : velocity / MAX_VELOCITY_THRESHOLD)); - break; - case Direction.UP: - animator.Play("Down", 0, (velocity / MAX_VELOCITY_THRESHOLD > 1 ? 1 : velocity / MAX_VELOCITY_THRESHOLD)); - break; - case Direction.LEFT: - animator.Play("Left", 0, (velocity / MAX_VELOCITY_THRESHOLD > 1 ? 1 : velocity / MAX_VELOCITY_THRESHOLD)); - break; - case Direction.RIGHT: - animator.Play("Right", 0, (velocity / MAX_VELOCITY_THRESHOLD > 1 ? 1 : velocity / MAX_VELOCITY_THRESHOLD)); - break; - } + + // Use this for initialization + void Start() + { + animator = transform.Find("BrushRigMesh").GetComponent(); + peltzerMain = FindObjectOfType(); + controllerMain = peltzerMain.controllerMain; } - } else { - if (animator != null) { - animator.SetTrigger("Release"); + + void Update() + { + if (isActivated) + { + float dist = Vector3.Distance(lastPosition, peltzerController.transform.position); + float velocity = dist / Time.deltaTime; + Vector3 direction = transform.InverseTransformDirection(peltzerController.transform.position - lastPosition); + + Direction dir = Direction.NONE; + + if (Mathf.Abs(direction.x) > Mathf.Abs(direction.y)) + { + if (direction.x < 0) + { + dir = Direction.LEFT; + } + else if (direction.x > 0) + { + dir = Direction.RIGHT; + } + } + else if (Mathf.Abs(direction.x) < Mathf.Abs(direction.y)) + { + if (direction.y < 0) + { + dir = Direction.DOWN; + } + else if (direction.y > 0) + { + dir = Direction.UP; + } + } + + lastPosition = peltzerController.transform.position; + + if (animator != null) + { + switch (dir) + { + case Direction.DOWN: + animator.Play("Up", 0, (velocity / MAX_VELOCITY_THRESHOLD > 1 ? 1 : velocity / MAX_VELOCITY_THRESHOLD)); + break; + case Direction.UP: + animator.Play("Down", 0, (velocity / MAX_VELOCITY_THRESHOLD > 1 ? 1 : velocity / MAX_VELOCITY_THRESHOLD)); + break; + case Direction.LEFT: + animator.Play("Left", 0, (velocity / MAX_VELOCITY_THRESHOLD > 1 ? 1 : velocity / MAX_VELOCITY_THRESHOLD)); + break; + case Direction.RIGHT: + animator.Play("Right", 0, (velocity / MAX_VELOCITY_THRESHOLD > 1 ? 1 : velocity / MAX_VELOCITY_THRESHOLD)); + break; + } + } + } + else + { + if (animator != null) + { + animator.SetTrigger("Release"); + } + } } - } - } - /// - /// An event handler that listens for controller input and delegates accordingly. - /// - /// The sender of the controller event. - /// The controller event arguments. - private void ControllerEventHandler(object sender, ControllerEventArgs args) { - if(args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger) { - if(args.Action == ButtonAction.DOWN) { - StartAnimation(); - } else if(args.Action == ButtonAction.UP) { - StopAnimation(); + /// + /// An event handler that listens for controller input and delegates accordingly. + /// + /// The sender of the controller event. + /// The controller event arguments. + private void ControllerEventHandler(object sender, ControllerEventArgs args) + { + if (args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger) + { + if (args.Action == ButtonAction.DOWN) + { + StartAnimation(); + } + else if (args.Action == ButtonAction.UP) + { + StopAnimation(); + } + } } - } - } - /// - /// Activates the animation logic by attaching the event handler for input. - /// - public void Activate() { - isActivated = true; - controllerMain.ControllerActionHandler += ControllerEventHandler; - } + /// + /// Activates the animation logic by attaching the event handler for input. + /// + public void Activate() + { + isActivated = true; + controllerMain.ControllerActionHandler += ControllerEventHandler; + } - /// - /// Deactivates the animation logic by removing the event handler for input. - /// - public void Deactivate() { - isActivated = false; - controllerMain.ControllerActionHandler -= ControllerEventHandler; - } + /// + /// Deactivates the animation logic by removing the event handler for input. + /// + public void Deactivate() + { + isActivated = false; + controllerMain.ControllerActionHandler -= ControllerEventHandler; + } - /// - /// Entry point for actual animation associated with the current active state of the tool. - /// - private void StartAnimation() { - if (animator != null) { - animator.SetTrigger("Active"); - } - } + /// + /// Entry point for actual animation associated with the current active state of the tool. + /// + private void StartAnimation() + { + if (animator != null) + { + animator.SetTrigger("Active"); + } + } - /// - /// Entry point for the animation associated with the current dormant state of the tool. - /// - private void StopAnimation() { - if (animator != null) { - animator.SetTrigger("Dormant"); - } - } + /// + /// Entry point for the animation associated with the current dormant state of the tool. + /// + private void StopAnimation() + { + if (animator != null) + { + animator.SetTrigger("Dormant"); + } + } - } + } } diff --git a/Assets/Scripts/model/controller/PaletteController.cs b/Assets/Scripts/model/controller/PaletteController.cs index 35b2eb9f..efdec4c3 100644 --- a/Assets/Scripts/model/controller/PaletteController.cs +++ b/Assets/Scripts/model/controller/PaletteController.cs @@ -23,1111 +23,1260 @@ using com.google.apps.peltzer.client.app; using com.google.apps.peltzer.client.tutorial; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Delegate method for controller clients to implement. - /// - public delegate void PaletteControllerActionHandler(object sender, ControllerEventArgs args); - - /// - /// Delegate method for undo listeners. - /// - public delegate void UndoActionHandler(); - - /// - /// Script for the palette controller. - /// - public class PaletteController : MonoBehaviour { - // The physical controller responsible for input & pose. - public ControllerDevice controller; - public ControllerGeometry controllerGeometry; - - // The toolheads - [Header("Toolheads")] - public GameObject shapeToolhead; - public GameObject freeformToolhead; - public GameObject paintToolhead; - public GameObject grabToolhead; - public GameObject modifyToolhead; - public GameObject eraseToolhead; - - public GameObject steamRiftHolder; - public GameObject oculusRiftHolder; - - /// - /// Occasionally, the controller is not set when our app starts. This method - /// will find the controller if it's null, and will return false if the - /// controller is not found. - /// - /// A reference to the controller that will be set if it's null. - /// Whether or not the controller was acquired and set. - public static bool AcquireIfNecessary(ref PaletteController paletteController) { - if (paletteController == null) { - paletteController = FindObjectOfType(); - if (paletteController == null) { - return false; - } - } - return true; - } - - /// - /// Clients must register themselves on this handler. - /// - public event PaletteControllerActionHandler PaletteControllerActionHandler; - - /// - /// Clients must register themselves on this handler. - /// - public event UndoActionHandler UndoActionHandler; - - public GameObject tutorialButton; - public GameObject UIPanels; - public float speed; - +namespace com.google.apps.peltzer.client.model.controller +{ /// - /// List of objects that will change color when the selected color changes. + /// Delegate method for controller clients to implement. /// - private List colorChangers = new List(); + public delegate void PaletteControllerActionHandler(object sender, ControllerEventArgs args); - private static readonly float TWO_PI = Mathf.PI * 2f; - private static readonly float PI_OVER_4 = Mathf.PI * .25f; - private readonly Color BASE_COLOR = new Color(0.9927992f, 1f, 0.4779411f); // Yellowish highlight. - private Transform peltzerControllerTouchpad; - private AudioLibrary audioLibrary; - - /// - /// Red color for the active state of the app menu button. - /// - private readonly Color ACTIVE_BUTTON_COLOR = new Color(244f / 255f, 67f / 255f, 54f / 255f, 1); /// - /// Grey color for the inactive state of the app menu button. + /// Delegate method for undo listeners. /// - private readonly Color INACTIVE_BUTTON_COLOR = new Color(114f / 255f, 115f / 255f, 118f / 255f, 1f); + public delegate void UndoActionHandler(); /// - /// Reference to the menu root node GameObject. - /// - public GameObject menuPanel; - /// - /// Reference to the Zandria menu root node GameObject. - /// - private GameObject polyMenuPanel; - /// - /// Reference to the Details menu root node GameObject. + /// Script for the palette controller. /// - private GameObject detailsMenuPanel; - - /// - /// Position reference for the menu panel when in the right hand. - /// - private readonly Vector3 menuPanelRightPos = new Vector3(-0.29f, 0f, 0f); - /// - /// Position reference for the menu panel when in the right hand. - /// - private readonly Vector3 menuPanelZandriaRightPos = new Vector3(-0.34f, 0f, 0f); - /// - /// Position reference for the details panel when in the left hand. - /// - private readonly Vector3 detailsPanelZandriaLeftPos = new Vector3(0.18f, 0f, -0.085f); - /// - /// Position reference for the details panel when in the right hand. - /// - private readonly Vector3 detailsPanelZandriaRightPos = new Vector3(-0.18f, 0f, -0.085f); - /// - /// Position reference for the menu panel when in the left hand and using Oculus. - /// - private readonly Vector3 menuPanelLeftPosOculus = new Vector3(0f, 0.025f, 0.025f); - /// - /// Position reference for the menu panel when in the right hand and using Oculus. - /// - private readonly Vector3 menuPanelRightPosOculus = new Vector3(-0.29f, 0.025f, 0.025f); - /// - /// Position reference for the Zandria menu panel when in the left hand and using Oculus. - /// - private readonly Vector3 menuPanelZandriaLeftPosOculus = new Vector3(0f, 0.025f, 0.025f); - /// - /// Position reference for the Zandria menu panel when in the right hand and using Oculus. - /// - private readonly Vector3 menuPanelZandriaRightPosOculus = new Vector3(-0.35f, 0.025f, 0.025f); - /// - /// Position reference for the details panel when in the left hand and using Oculus. - /// - private readonly Vector3 detailsPanelZandriaLeftPosOculus = new Vector3(0.18f, -0.03f, -0.03f); - /// - /// Position reference for the details panel when in the right hand and using Oculus. - /// - private readonly Vector3 detailsPanelZandriaRightPosOculus = new Vector3(-0.18f, -0.03f, -0.03f); - /// - /// Tilt angle of the palette for controllers when using the Oculus SDK. - /// - private readonly Quaternion menuRotationOculus = Quaternion.Euler(-45, 0, 0); - - // Snap tooltip gameObjects and text - private const int OPERATIONS_BEFORE_DISABLING_SNAP_TOOLTIPS = 3; - - public Handedness handedness = Handedness.LEFT; - - // Pop-up dialogs. - public GameObject newModelPrompt; - public GameObject publishedTakeOffHeadsetPrompt; - public GameObject tutorialBeginPrompt; - public GameObject tutorialSavePrompt; - public GameObject publishAfterSavePrompt; - public GameObject publishSignInPrompt; - public GameObject tutorialExitPrompt; - public GameObject saveLocallyPrompt; - - /// - /// Library to generate haptic feedback. - /// - private HapticFeedback hapticsLibrary; - - private Objectionary objectionary; - - private TouchpadOverlay currentOverlay; - private Overlay overlay; - - private static readonly float DELAY_UNTIL_EVENT_REPEATS = 0.5f; - private static readonly float DELAY_BETWEEN_REPEATING_EVENTS = 0.2f; - private float? touchpadPressDownTime; - private bool touchpadRepeating; - private bool touchpadTouched = false; - private float lastTouchpadRepeatTime; - private ControllerEventArgs eventToRepeat; - private TouchpadHoverState lastTouchpadHoverState = TouchpadHoverState.NONE; - - // Undo/Redo logic. - private static readonly int MIN_REPEAT_SCALE = 1; - private static readonly int MAX_REPEAT_SCALE = 8; - private static readonly int REPEAT_SCALE_MULTIPLIER = 2; - private int repeatScale = MIN_REPEAT_SCALE; + public class PaletteController : MonoBehaviour + { + // The physical controller responsible for input & pose. + public ControllerDevice controller; + public ControllerGeometry controllerGeometry; + + // The toolheads + [Header("Toolheads")] + public GameObject shapeToolhead; + public GameObject freeformToolhead; + public GameObject paintToolhead; + public GameObject grabToolhead; + public GameObject modifyToolhead; + public GameObject eraseToolhead; + + public GameObject steamRiftHolder; + public GameObject oculusRiftHolder; + + /// + /// Occasionally, the controller is not set when our app starts. This method + /// will find the controller if it's null, and will return false if the + /// controller is not found. + /// + /// A reference to the controller that will be set if it's null. + /// Whether or not the controller was acquired and set. + public static bool AcquireIfNecessary(ref PaletteController paletteController) + { + if (paletteController == null) + { + paletteController = FindObjectOfType(); + if (paletteController == null) + { + return false; + } + } + return true; + } - /// - /// Local position for overlay icon - default - VIVE. - /// - Vector3 LEFT_OVERLAY_ICON_DEFAULT_POSITION_VIVE = new Vector3(-2.5f, 0f, 0f); - Vector3 RIGHT_OVERLAY_ICON_DEFAULT_POSITION_VIVE = new Vector3(2.5f, 0f, 0f); - Vector3 UP_OVERLAY_ICON_DEFAULT_POSITION_VIVE = new Vector3(0f, 2.5f, 0f); - Vector3 DOWN_OVERLAY_ICON_DEFAULT_POSITION_VIVE = new Vector3(0f, -2.5f, 0f); + /// + /// Clients must register themselves on this handler. + /// + public event PaletteControllerActionHandler PaletteControllerActionHandler; + + /// + /// Clients must register themselves on this handler. + /// + public event UndoActionHandler UndoActionHandler; + + public GameObject tutorialButton; + public GameObject UIPanels; + public float speed; + + /// + /// List of objects that will change color when the selected color changes. + /// + private List colorChangers = new List(); + + private static readonly float TWO_PI = Mathf.PI * 2f; + private static readonly float PI_OVER_4 = Mathf.PI * .25f; + private readonly Color BASE_COLOR = new Color(0.9927992f, 1f, 0.4779411f); // Yellowish highlight. + private Transform peltzerControllerTouchpad; + private AudioLibrary audioLibrary; + + /// + /// Red color for the active state of the app menu button. + /// + private readonly Color ACTIVE_BUTTON_COLOR = new Color(244f / 255f, 67f / 255f, 54f / 255f, 1); + /// + /// Grey color for the inactive state of the app menu button. + /// + private readonly Color INACTIVE_BUTTON_COLOR = new Color(114f / 255f, 115f / 255f, 118f / 255f, 1f); + + /// + /// Reference to the menu root node GameObject. + /// + public GameObject menuPanel; + /// + /// Reference to the Zandria menu root node GameObject. + /// + private GameObject polyMenuPanel; + /// + /// Reference to the Details menu root node GameObject. + /// + private GameObject detailsMenuPanel; + + /// + /// Position reference for the menu panel when in the right hand. + /// + private readonly Vector3 menuPanelRightPos = new Vector3(-0.29f, 0f, 0f); + /// + /// Position reference for the menu panel when in the right hand. + /// + private readonly Vector3 menuPanelZandriaRightPos = new Vector3(-0.34f, 0f, 0f); + /// + /// Position reference for the details panel when in the left hand. + /// + private readonly Vector3 detailsPanelZandriaLeftPos = new Vector3(0.18f, 0f, -0.085f); + /// + /// Position reference for the details panel when in the right hand. + /// + private readonly Vector3 detailsPanelZandriaRightPos = new Vector3(-0.18f, 0f, -0.085f); + /// + /// Position reference for the menu panel when in the left hand and using Oculus. + /// + private readonly Vector3 menuPanelLeftPosOculus = new Vector3(0f, 0.025f, 0.025f); + /// + /// Position reference for the menu panel when in the right hand and using Oculus. + /// + private readonly Vector3 menuPanelRightPosOculus = new Vector3(-0.29f, 0.025f, 0.025f); + /// + /// Position reference for the Zandria menu panel when in the left hand and using Oculus. + /// + private readonly Vector3 menuPanelZandriaLeftPosOculus = new Vector3(0f, 0.025f, 0.025f); + /// + /// Position reference for the Zandria menu panel when in the right hand and using Oculus. + /// + private readonly Vector3 menuPanelZandriaRightPosOculus = new Vector3(-0.35f, 0.025f, 0.025f); + /// + /// Position reference for the details panel when in the left hand and using Oculus. + /// + private readonly Vector3 detailsPanelZandriaLeftPosOculus = new Vector3(0.18f, -0.03f, -0.03f); + /// + /// Position reference for the details panel when in the right hand and using Oculus. + /// + private readonly Vector3 detailsPanelZandriaRightPosOculus = new Vector3(-0.18f, -0.03f, -0.03f); + /// + /// Tilt angle of the palette for controllers when using the Oculus SDK. + /// + private readonly Quaternion menuRotationOculus = Quaternion.Euler(-45, 0, 0); + + // Snap tooltip gameObjects and text + private const int OPERATIONS_BEFORE_DISABLING_SNAP_TOOLTIPS = 3; + + public Handedness handedness = Handedness.LEFT; + + // Pop-up dialogs. + public GameObject newModelPrompt; + public GameObject publishedTakeOffHeadsetPrompt; + public GameObject tutorialBeginPrompt; + public GameObject tutorialSavePrompt; + public GameObject publishAfterSavePrompt; + public GameObject publishSignInPrompt; + public GameObject tutorialExitPrompt; + public GameObject saveLocallyPrompt; + + /// + /// Library to generate haptic feedback. + /// + private HapticFeedback hapticsLibrary; + + private Objectionary objectionary; + + private TouchpadOverlay currentOverlay; + private Overlay overlay; + + private static readonly float DELAY_UNTIL_EVENT_REPEATS = 0.5f; + private static readonly float DELAY_BETWEEN_REPEATING_EVENTS = 0.2f; + private float? touchpadPressDownTime; + private bool touchpadRepeating; + private bool touchpadTouched = false; + private float lastTouchpadRepeatTime; + private ControllerEventArgs eventToRepeat; + private TouchpadHoverState lastTouchpadHoverState = TouchpadHoverState.NONE; + + // Undo/Redo logic. + private static readonly int MIN_REPEAT_SCALE = 1; + private static readonly int MAX_REPEAT_SCALE = 8; + private static readonly int REPEAT_SCALE_MULTIPLIER = 2; + private int repeatScale = MIN_REPEAT_SCALE; + + /// + /// Local position for overlay icon - default - VIVE. + /// + Vector3 LEFT_OVERLAY_ICON_DEFAULT_POSITION_VIVE = new Vector3(-2.5f, 0f, 0f); + Vector3 RIGHT_OVERLAY_ICON_DEFAULT_POSITION_VIVE = new Vector3(2.5f, 0f, 0f); + Vector3 UP_OVERLAY_ICON_DEFAULT_POSITION_VIVE = new Vector3(0f, 2.5f, 0f); + Vector3 DOWN_OVERLAY_ICON_DEFAULT_POSITION_VIVE = new Vector3(0f, -2.5f, 0f); + + /// + /// Local position for overlay icon - hover - VIVE. + /// + Vector3 LEFT_OVERLAY_ICON_HOVER_POSITION = new Vector3(-2.5f, 0f, -0.6f); + Vector3 RIGHT_OVERLAY_ICON_HOVER_POSITION = new Vector3(2.5f, 0f, -0.6f); + Vector3 UP_OVERLAY_ICON_HOVER_POSITION = new Vector3(0f, 2.5f, -0.6f); + Vector3 DOWN_OVERLAY_ICON_HOVER_POSITION = new Vector3(0f, -2.5f, -0.6f); + + /// + /// Records the time the publish dialog is opened so it can be expired after a set time. + /// + private float publishDialogStartTime = 0f; + + private static readonly float PUBLISH_DIALOG_LIFETIME = 10f; + + private bool setupDone; + + void Start() + { + PaletteControllerActionHandler += ControllerEventHandler; + } - /// - /// Local position for overlay icon - hover - VIVE. - /// - Vector3 LEFT_OVERLAY_ICON_HOVER_POSITION = new Vector3(-2.5f, 0f, -0.6f); - Vector3 RIGHT_OVERLAY_ICON_HOVER_POSITION = new Vector3(2.5f, 0f, -0.6f); - Vector3 UP_OVERLAY_ICON_HOVER_POSITION = new Vector3(0f, 2.5f, -0.6f); - Vector3 DOWN_OVERLAY_ICON_HOVER_POSITION = new Vector3(0f, -2.5f, -0.6f); + public void Setup() + { + if (Config.Instance.sdkMode == SdkMode.SteamVR) + { +#if STEAMVRBUILD + controller = new ControllerDeviceSteam(transform); +#endif + } + else + { + ControllerDeviceOculus oculusController = new ControllerDeviceOculus(transform); + oculusController.controllerType = OVRInput.Controller.LTouch; + controller = oculusController; + } + controllerGeometry.baseControllerAnimation.SetControllerDevice(controller); + + hapticsLibrary = GetComponent(); + audioLibrary = FindObjectOfType(); + + menuPanel = transform.Find("ID_PanelTools").gameObject; + polyMenuPanel = transform.Find("Panel-Menu").gameObject; + detailsMenuPanel = transform.Find("Model-Details").gameObject; + tutorialButton = transform.Find("ID_PanelTools/ToolSide/Actions/Tutorial").gameObject; + + if (Config.Instance.sdkMode == SdkMode.Oculus) + { + menuPanel.transform.localPosition = menuPanelLeftPosOculus; + menuPanel.transform.localRotation = menuRotationOculus; + polyMenuPanel.transform.localRotation = menuRotationOculus; + detailsMenuPanel.transform.localRotation = menuRotationOculus; + } - /// - /// Records the time the publish dialog is opened so it can be expired after a set time. - /// - private float publishDialogStartTime = 0f; + HideTooltips(); + + newModelPrompt = transform.Find("ID_PanelTools/ToolSide/NewModelPrompt").gameObject; + publishedTakeOffHeadsetPrompt = transform.Find("ID_PanelTools/ToolSide/TakeOffHeadsetPrompt").gameObject; + tutorialSavePrompt = transform.Find("ID_PanelTools/ToolSide/TutorialSavePrompt").gameObject; + tutorialBeginPrompt = transform.Find("ID_PanelTools/ToolSide/TutorialPrompt").gameObject; + tutorialExitPrompt = transform.Find("ID_PanelTools/ToolSide/TutorialExitPrompt").gameObject; + publishAfterSavePrompt = transform.Find("ID_PanelTools/ToolSide/PublishAfterSavePrompt").gameObject; + publishSignInPrompt = transform.Find("ID_PanelTools/ToolSide/PublishSignInPrompt").gameObject; + saveLocallyPrompt = transform.Find("ID_PanelTools/ToolSide/SaveLocallyPrompt").gameObject; + + bool shouldNagForTutorial = !PlayerPrefs.HasKey(TutorialManager.HAS_EVER_STARTED_TUTORIAL_KEY); + if (shouldNagForTutorial) + { + tutorialButton.GetComponent().material.color = PeltzerController.MENU_BUTTON_GREEN; + tutorialBeginPrompt.SetActive(true); + } + else + { + tutorialBeginPrompt.SetActive(false); + } - private static readonly float PUBLISH_DIALOG_LIFETIME = 10f; + colorChangers.AddRange(GetComponentsInChildren(/* includeInactive */ true)); - private bool setupDone; + // Turn on the paletteController menu overlay. + controllerGeometry.menuOverlay.SetActive(true); + if (controllerGeometry.OnMenuOverlay != null) controllerGeometry.OnMenuOverlay.SetActive(true); - void Start() { - PaletteControllerActionHandler += ControllerEventHandler; - } + ResetTouchpadOverlay(); - public void Setup() { - if (Config.Instance.sdkMode == SdkMode.SteamVR) { -#if STEAMVRBUILD - controller = new ControllerDeviceSteam(transform); -#endif - } else { - ControllerDeviceOculus oculusController = new ControllerDeviceOculus(transform); - oculusController.controllerType = OVRInput.Controller.LTouch; - controller = oculusController; - } - controllerGeometry.baseControllerAnimation.SetControllerDevice(controller); - - hapticsLibrary = GetComponent(); - audioLibrary = FindObjectOfType(); - - menuPanel = transform.Find("ID_PanelTools").gameObject; - polyMenuPanel = transform.Find("Panel-Menu").gameObject; - detailsMenuPanel = transform.Find("Model-Details").gameObject; - tutorialButton = transform.Find("ID_PanelTools/ToolSide/Actions/Tutorial").gameObject; - - if (Config.Instance.sdkMode == SdkMode.Oculus) { - menuPanel.transform.localPosition = menuPanelLeftPosOculus; - menuPanel.transform.localRotation = menuRotationOculus; - polyMenuPanel.transform.localRotation = menuRotationOculus; - detailsMenuPanel.transform.localRotation = menuRotationOculus; - } - - HideTooltips(); - - newModelPrompt = transform.Find("ID_PanelTools/ToolSide/NewModelPrompt").gameObject; - publishedTakeOffHeadsetPrompt = transform.Find("ID_PanelTools/ToolSide/TakeOffHeadsetPrompt").gameObject; - tutorialSavePrompt = transform.Find("ID_PanelTools/ToolSide/TutorialSavePrompt").gameObject; - tutorialBeginPrompt = transform.Find("ID_PanelTools/ToolSide/TutorialPrompt").gameObject; - tutorialExitPrompt = transform.Find("ID_PanelTools/ToolSide/TutorialExitPrompt").gameObject; - publishAfterSavePrompt = transform.Find("ID_PanelTools/ToolSide/PublishAfterSavePrompt").gameObject; - publishSignInPrompt = transform.Find("ID_PanelTools/ToolSide/PublishSignInPrompt").gameObject; - saveLocallyPrompt = transform.Find("ID_PanelTools/ToolSide/SaveLocallyPrompt").gameObject; - - bool shouldNagForTutorial = !PlayerPrefs.HasKey(TutorialManager.HAS_EVER_STARTED_TUTORIAL_KEY); - if (shouldNagForTutorial) { - tutorialButton.GetComponent().material.color = PeltzerController.MENU_BUTTON_GREEN; - tutorialBeginPrompt.SetActive(true); - } else { - tutorialBeginPrompt.SetActive(false); - } - - colorChangers.AddRange(GetComponentsInChildren(/* includeInactive */ true)); - - // Turn on the paletteController menu overlay. - controllerGeometry.menuOverlay.SetActive(true); - if (controllerGeometry.OnMenuOverlay != null) controllerGeometry.OnMenuOverlay.SetActive(true); - - ResetTouchpadOverlay(); - - // Put everything in the default handedness position. - ControllerHandednessChanged(); - ResetTouchpadOverlay(); - - setupDone = true; - } + // Put everything in the default handedness position. + ControllerHandednessChanged(); + ResetTouchpadOverlay(); - void Update() { - if (!setupDone) return; + setupDone = true; + } - controller.Update(); + void Update() + { + if (!setupDone) return; - // If the publish dialog has been opened for more than 10 seconds, close it. - if (publishedTakeOffHeadsetPrompt.activeSelf && Time.time - publishDialogStartTime > PUBLISH_DIALOG_LIFETIME) { - publishedTakeOffHeadsetPrompt.SetActive(false); - } + controller.Update(); - menuPanel.SetActive(PeltzerMain.Instance.GetPolyMenuMain().ToolMenuIsActive() - && PeltzerMain.Instance.restrictionManager.paletteAllowed); + // If the publish dialog has been opened for more than 10 seconds, close it. + if (publishedTakeOffHeadsetPrompt.activeSelf && Time.time - publishDialogStartTime > PUBLISH_DIALOG_LIFETIME) + { + publishedTakeOffHeadsetPrompt.SetActive(false); + } - if (PeltzerMain.Instance.introChoreographer.introIsComplete) { - ProcessButtonEvents(); - } - } + menuPanel.SetActive(PeltzerMain.Instance.GetPolyMenuMain().ToolMenuIsActive() + && PeltzerMain.Instance.restrictionManager.paletteAllowed); - // Process user interaction. - private void ProcessButtonEvents() { - if (controller.IsTrackedObjectValid) { - hapticsLibrary.controller = controller; - SetGripTooltip(); - - if (controller.WasJustPressed(ButtonId.Touchpad)) { - if (PaletteControllerActionHandler != null) { - Vector2 axis = controller.GetDirectionalAxis(); - TouchpadLocation location = controller.GetTouchpadLocation(); - - ControllerEventArgs eventArgs = new ControllerEventArgs(ControllerType.PALETTE, - ButtonId.Touchpad, ButtonAction.DOWN, location, TouchpadOverlay.NONE); - PaletteControllerActionHandler(this, eventArgs); - - // Queue up this event to repeat in the case that the touchpad is not released - if (location != TouchpadLocation.CENTER) { - touchpadPressDownTime = Time.time; - eventToRepeat = eventArgs; - repeatScale = MIN_REPEAT_SCALE; + if (PeltzerMain.Instance.introChoreographer.introIsComplete) + { + ProcessButtonEvents(); } - } - // If the touchpad was released, then stop repeating events. - } else if (controller.WasJustReleased(ButtonId.Touchpad)) { - touchpadPressDownTime = null; - touchpadRepeating = false; - repeatScale = MIN_REPEAT_SCALE; - // If the user stops touching the touchpad, sent out a 'cancel' event. - PaletteControllerActionHandler(this, new ControllerEventArgs(ControllerType.PALETTE, ButtonId.Touchpad, - ButtonAction.NONE, TouchpadLocation.NONE, currentOverlay)); } - if (controller.WasJustPressed(ButtonId.ApplicationMenu)) { - if (PaletteControllerActionHandler != null) { - ControllerEventArgs eventArgs = new ControllerEventArgs(ControllerType.PALETTE, - ButtonId.ApplicationMenu, ButtonAction.DOWN, TouchpadLocation.NONE, - TouchpadOverlay.NONE); - PaletteControllerActionHandler(this, eventArgs); - } + // Process user interaction. + private void ProcessButtonEvents() + { + if (controller.IsTrackedObjectValid) + { + hapticsLibrary.controller = controller; + SetGripTooltip(); + + if (controller.WasJustPressed(ButtonId.Touchpad)) + { + if (PaletteControllerActionHandler != null) + { + Vector2 axis = controller.GetDirectionalAxis(); + TouchpadLocation location = controller.GetTouchpadLocation(); + + ControllerEventArgs eventArgs = new ControllerEventArgs(ControllerType.PALETTE, + ButtonId.Touchpad, ButtonAction.DOWN, location, TouchpadOverlay.NONE); + PaletteControllerActionHandler(this, eventArgs); + + // Queue up this event to repeat in the case that the touchpad is not released + if (location != TouchpadLocation.CENTER) + { + touchpadPressDownTime = Time.time; + eventToRepeat = eventArgs; + repeatScale = MIN_REPEAT_SCALE; + } + } + // If the touchpad was released, then stop repeating events. + } + else if (controller.WasJustReleased(ButtonId.Touchpad)) + { + touchpadPressDownTime = null; + touchpadRepeating = false; + repeatScale = MIN_REPEAT_SCALE; + // If the user stops touching the touchpad, sent out a 'cancel' event. + PaletteControllerActionHandler(this, new ControllerEventArgs(ControllerType.PALETTE, ButtonId.Touchpad, + ButtonAction.NONE, TouchpadLocation.NONE, currentOverlay)); + } + + if (controller.WasJustPressed(ButtonId.ApplicationMenu)) + { + if (PaletteControllerActionHandler != null) + { + ControllerEventArgs eventArgs = new ControllerEventArgs(ControllerType.PALETTE, + ButtonId.ApplicationMenu, ButtonAction.DOWN, TouchpadLocation.NONE, + TouchpadOverlay.NONE); + PaletteControllerActionHandler(this, eventArgs); + } + } + + if (controller.WasJustPressed(ButtonId.SecondaryButton)) + { + if (PaletteControllerActionHandler != null) + { + ControllerEventArgs eventArgs = new ControllerEventArgs(ControllerType.PALETTE, + ButtonId.SecondaryButton, ButtonAction.DOWN, TouchpadLocation.NONE, + TouchpadOverlay.NONE); + PaletteControllerActionHandler(this, eventArgs); + } + } + + if (Features.useContinuousSnapDetection && controller.IsTriggerHalfPressed()) + { + if (PaletteControllerActionHandler != null) + { + PaletteControllerActionHandler(this, new ControllerEventArgs(ControllerType.PALETTE, + ButtonId.Trigger, + ButtonAction.LIGHT_DOWN, + TouchpadLocation.NONE, + currentOverlay)); + } + } + + if (Features.useContinuousSnapDetection && controller.WasTriggerJustReleasedFromHalfPress()) + { + if (PaletteControllerActionHandler != null) + { + PaletteControllerActionHandler(this, new ControllerEventArgs(ControllerType.PALETTE, + ButtonId.Trigger, + ButtonAction.LIGHT_UP, + TouchpadLocation.NONE, + currentOverlay)); + } + } + + if (controller.WasJustPressed(ButtonId.Trigger)) + { + if (PaletteControllerActionHandler != null) + { + ControllerEventArgs eventArgs = new ControllerEventArgs(ControllerType.PALETTE, + ButtonId.Trigger, ButtonAction.DOWN, TouchpadLocation.NONE, + TouchpadOverlay.NONE); + PaletteControllerActionHandler(this, eventArgs); + } + } + + if (controller.WasJustReleased(ButtonId.Trigger)) + { + if (PaletteControllerActionHandler != null) + { + ControllerEventArgs eventArgs = new ControllerEventArgs(ControllerType.PALETTE, + ButtonId.Trigger, ButtonAction.UP, TouchpadLocation.NONE, + TouchpadOverlay.NONE); + PaletteControllerActionHandler(this, eventArgs); + } + } + + if (controller.IsPressed(ButtonId.Touchpad)) + { + // If the touchpad is held down, repeat events if appropriate. + // Start repeating if it's been long enough since the initial press-down. + SetHoverTooltips(isTouched: true); + if (touchpadPressDownTime.HasValue && !touchpadRepeating && + (Time.time - touchpadPressDownTime.Value) > DELAY_UNTIL_EVENT_REPEATS) + { + touchpadRepeating = true; + PaletteControllerActionHandler(this, eventToRepeat); + lastTouchpadRepeatTime = Time.time; + // Keep repeating if it's been long enough since the last + // repeated event was sent. + } + else if (touchpadRepeating && + (Time.time - lastTouchpadRepeatTime) > + DELAY_BETWEEN_REPEATING_EVENTS) + { + if (repeatScale < MAX_REPEAT_SCALE) + { + repeatScale *= REPEAT_SCALE_MULTIPLIER; + } + for (int i = 1; i <= repeatScale; i++) + { + PaletteControllerActionHandler(this, eventToRepeat); + } + lastTouchpadRepeatTime = Time.time; + } + } + else if (controller.IsTouched(ButtonId.Touchpad)) + { + SetHoverTooltips(isTouched: true); + } + else + { + SetHoverTooltips(isTouched: false); + } + } } - if (controller.WasJustPressed(ButtonId.SecondaryButton)) { - if (PaletteControllerActionHandler != null) { - ControllerEventArgs eventArgs = new ControllerEventArgs(ControllerType.PALETTE, - ButtonId.SecondaryButton, ButtonAction.DOWN, TouchpadLocation.NONE, - TouchpadOverlay.NONE); - PaletteControllerActionHandler(this, eventArgs); - } + /// + /// Sets the hover tooltips on the touchpad. + /// + /// Whether the touchpad is currently touched (hovered over) + private void SetHoverTooltips(bool isTouched) + { + // Show the menu tooltips if the user is touching, but not if we're zooming or all tooltips are disabled. + bool touchedAndEnabled = !PeltzerMain.Instance.HasDisabledTooltips + && !PeltzerMain.Instance.Zoomer.Zooming + && isTouched; + bool showPolyMenu = Features.showModelsMenuTooltips + && touchedAndEnabled && PeltzerMain.Instance.polyMenuMain.polyMenu.activeInHierarchy; + bool showToolMenu = touchedAndEnabled && PeltzerMain.Instance.polyMenuMain.toolMenu.activeInHierarchy; + + // Get the correct references to tooltips. + GameObject pageLeftRightTooltip = PeltzerMain.Instance.paletteController.handedness == Handedness.RIGHT ? + PeltzerMain.Instance.paletteController.controllerGeometry.menuRightTooltip : + PeltzerMain.Instance.paletteController.controllerGeometry.menuLeftTooltip; + GameObject undoRedoTooltip = PeltzerMain.Instance.paletteController.handedness == Handedness.RIGHT ? + PeltzerMain.Instance.paletteController.controllerGeometry.undoRedoRightTooltip : + PeltzerMain.Instance.paletteController.controllerGeometry.undoRedoLeftTooltip; + + PeltzerMain.Instance.paletteController.controllerGeometry.menuDownTooltip.SetActive(showPolyMenu); + PeltzerMain.Instance.paletteController.controllerGeometry.menuUpTooltip.SetActive(showPolyMenu); + pageLeftRightTooltip.SetActive(showPolyMenu); + // Reset both instances to false before setting the state of shown tooltip. + PeltzerMain.Instance.paletteController.controllerGeometry.undoRedoRightTooltip.SetActive(false); + PeltzerMain.Instance.paletteController.controllerGeometry.undoRedoLeftTooltip.SetActive(false); + undoRedoTooltip.SetActive(showToolMenu); + if (touchedAndEnabled) + { + TouchpadHoverState touchpadHoverState; + switch (controller.GetTouchpadLocation()) + { + case TouchpadLocation.TOP: + touchpadHoverState = TouchpadHoverState.UP; + break; + case TouchpadLocation.BOTTOM: + touchpadHoverState = TouchpadHoverState.DOWN; + break; + case TouchpadLocation.LEFT: + touchpadHoverState = TouchpadHoverState.LEFT; + break; + case TouchpadLocation.RIGHT: + touchpadHoverState = TouchpadHoverState.RIGHT; + break; + default: + touchpadHoverState = TouchpadHoverState.NONE; + break; + } + SetTouchpadHoverTexture(touchpadHoverState); + } + else + { + SetTouchpadHoverTexture(TouchpadHoverState.NONE); + } } - if (Features.useContinuousSnapDetection && controller.IsTriggerHalfPressed()) { - if (PaletteControllerActionHandler != null) { - PaletteControllerActionHandler(this, new ControllerEventArgs(ControllerType.PALETTE, - ButtonId.Trigger, - ButtonAction.LIGHT_DOWN, - TouchpadLocation.NONE, - currentOverlay)); - } + public GameObject GetToolheadForMode(ControllerMode mode) + { + switch (mode) + { + case ControllerMode.insertVolume: + case ControllerMode.subtract: + return shapeToolhead; + case ControllerMode.insertStroke: + return freeformToolhead; + case ControllerMode.paintFace: + case ControllerMode.paintMesh: + return paintToolhead; + case ControllerMode.move: + return grabToolhead; + case ControllerMode.subdivideFace: + case ControllerMode.reshape: + case ControllerMode.extrude: + return modifyToolhead; + case ControllerMode.delete: + case ControllerMode.deletePart: + return eraseToolhead; + } + return null; } - if (Features.useContinuousSnapDetection && controller.WasTriggerJustReleasedFromHalfPress()) { - if (PaletteControllerActionHandler != null) { - PaletteControllerActionHandler(this, new ControllerEventArgs(ControllerType.PALETTE, - ButtonId.Trigger, - ButtonAction.LIGHT_UP, - TouchpadLocation.NONE, - currentOverlay)); - } + public void SetPublishDialogActive() + { + publishedTakeOffHeadsetPrompt.SetActive(true); + publishDialogStartTime = Time.time; } - if (controller.WasJustPressed(ButtonId.Trigger)) { - if (PaletteControllerActionHandler != null) { - ControllerEventArgs eventArgs = new ControllerEventArgs(ControllerType.PALETTE, - ButtonId.Trigger, ButtonAction.DOWN, TouchpadLocation.NONE, - TouchpadOverlay.NONE); - PaletteControllerActionHandler(this, eventArgs); - } + private bool IsUndoEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN + && args.TouchpadLocation == TouchpadLocation.LEFT; } - if (controller.WasJustReleased(ButtonId.Trigger)) { - if (PaletteControllerActionHandler != null) { - ControllerEventArgs eventArgs = new ControllerEventArgs(ControllerType.PALETTE, - ButtonId.Trigger, ButtonAction.UP, TouchpadLocation.NONE, - TouchpadOverlay.NONE); - PaletteControllerActionHandler(this, eventArgs); - } + private bool IsRedoEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN + && args.TouchpadLocation == TouchpadLocation.RIGHT; } - if (controller.IsPressed(ButtonId.Touchpad)) { - // If the touchpad is held down, repeat events if appropriate. - // Start repeating if it's been long enough since the initial press-down. - SetHoverTooltips(isTouched: true); - if (touchpadPressDownTime.HasValue && !touchpadRepeating && - (Time.time - touchpadPressDownTime.Value) > DELAY_UNTIL_EVENT_REPEATS) { - touchpadRepeating = true; - PaletteControllerActionHandler(this, eventToRepeat); - lastTouchpadRepeatTime = Time.time; - // Keep repeating if it's been long enough since the last - // repeated event was sent. - } else if (touchpadRepeating && - (Time.time - lastTouchpadRepeatTime) > - DELAY_BETWEEN_REPEATING_EVENTS) { - if (repeatScale < MAX_REPEAT_SCALE) { - repeatScale *= REPEAT_SCALE_MULTIPLIER; + private void ControllerEventHandler(object sender, ControllerEventArgs args) + { + if (!PeltzerMain.Instance.restrictionManager.undoRedoAllowed) + { + return; } - for (int i = 1; i <= repeatScale; i++) { - PaletteControllerActionHandler(this, eventToRepeat); + else if (IsUndoEvent(args)) + { + Undo(); + } + else if (IsRedoEvent(args)) + { + Redo(); } - lastTouchpadRepeatTime = Time.time; - } - } else if (controller.IsTouched(ButtonId.Touchpad)) { - SetHoverTooltips(isTouched: true); - } else { - SetHoverTooltips(isTouched: false); - } - } - } - - /// - /// Sets the hover tooltips on the touchpad. - /// - /// Whether the touchpad is currently touched (hovered over) - private void SetHoverTooltips(bool isTouched) { - // Show the menu tooltips if the user is touching, but not if we're zooming or all tooltips are disabled. - bool touchedAndEnabled = !PeltzerMain.Instance.HasDisabledTooltips - && !PeltzerMain.Instance.Zoomer.Zooming - && isTouched; - bool showPolyMenu = Features.showModelsMenuTooltips - && touchedAndEnabled && PeltzerMain.Instance.polyMenuMain.polyMenu.activeInHierarchy; - bool showToolMenu = touchedAndEnabled && PeltzerMain.Instance.polyMenuMain.toolMenu.activeInHierarchy; - - // Get the correct references to tooltips. - GameObject pageLeftRightTooltip = PeltzerMain.Instance.paletteController.handedness == Handedness.RIGHT ? - PeltzerMain.Instance.paletteController.controllerGeometry.menuRightTooltip : - PeltzerMain.Instance.paletteController.controllerGeometry.menuLeftTooltip; - GameObject undoRedoTooltip = PeltzerMain.Instance.paletteController.handedness == Handedness.RIGHT ? - PeltzerMain.Instance.paletteController.controllerGeometry.undoRedoRightTooltip : - PeltzerMain.Instance.paletteController.controllerGeometry.undoRedoLeftTooltip; - - PeltzerMain.Instance.paletteController.controllerGeometry.menuDownTooltip.SetActive(showPolyMenu); - PeltzerMain.Instance.paletteController.controllerGeometry.menuUpTooltip.SetActive(showPolyMenu); - pageLeftRightTooltip.SetActive(showPolyMenu); - // Reset both instances to false before setting the state of shown tooltip. - PeltzerMain.Instance.paletteController.controllerGeometry.undoRedoRightTooltip.SetActive(false); - PeltzerMain.Instance.paletteController.controllerGeometry.undoRedoLeftTooltip.SetActive(false); - undoRedoTooltip.SetActive(showToolMenu); - if (touchedAndEnabled) { - TouchpadHoverState touchpadHoverState; - switch (controller.GetTouchpadLocation()) { - case TouchpadLocation.TOP: - touchpadHoverState = TouchpadHoverState.UP; - break; - case TouchpadLocation.BOTTOM: - touchpadHoverState = TouchpadHoverState.DOWN; - break; - case TouchpadLocation.LEFT: - touchpadHoverState = TouchpadHoverState.LEFT; - break; - case TouchpadLocation.RIGHT: - touchpadHoverState = TouchpadHoverState.RIGHT; - break; - default: - touchpadHoverState = TouchpadHoverState.NONE; - break; } - SetTouchpadHoverTexture(touchpadHoverState); - } else { - SetTouchpadHoverTexture(TouchpadHoverState.NONE); - } - } - public GameObject GetToolheadForMode(ControllerMode mode) { - switch (mode) { - case ControllerMode.insertVolume: - case ControllerMode.subtract: - return shapeToolhead; - case ControllerMode.insertStroke: - return freeformToolhead; - case ControllerMode.paintFace: - case ControllerMode.paintMesh: - return paintToolhead; - case ControllerMode.move: - return grabToolhead; - case ControllerMode.subdivideFace: - case ControllerMode.reshape: - case ControllerMode.extrude: - return modifyToolhead; - case ControllerMode.delete: - case ControllerMode.deletePart: - return eraseToolhead; - } - return null; - } - - public void SetPublishDialogActive() { - publishedTakeOffHeadsetPrompt.SetActive(true); - publishDialogStartTime = Time.time; - } - - private bool IsUndoEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN - && args.TouchpadLocation == TouchpadLocation.LEFT; - } + /// + /// The 'undo' command. Special-cased for stroke and subdivide modes, and selected items. + /// + private void Undo() + { + bool undid = false; + bool canUndoInModel = false; + Selector selector = PeltzerMain.Instance.GetSelector(); + + if (Features.clickToSelectWithUndoRedoEnabled && selector.selectedMeshes.Count > 0) + { + // Divert to local undo for multi-selection + undid = selector.UndoMultiSelect(); + } + else if (Features.localUndoRedoEnabled && selector.isMultiSelecting) + { + // Divert to local undo for multi-selection + undid = selector.UndoMultiSelect(); + } + else if (PeltzerMain.Instance.GetFreeform().IsStroking()) + { + // In stroke mode, we try and remove a checkpoint. + undid = PeltzerMain.Instance.GetFreeform().Undo(); + } + else if (selector.AnythingSelected()) + { + // If anything was selected in any mode, we clear the selection. + undid = true; + selector.DeselectAll(); + } + else if (PeltzerMain.Instance.GetDeleter().isDeleting) + { + // If we are currently deleting, anything that has been deleted so far is restored. + undid = PeltzerMain.Instance.GetDeleter().CancelDeletionsSoFar(); + } + else + { + // Else, we actually try and perform 'undo' on the model. We defer the actual 'undo' call until after the + // handler fires, in case tools try and do something with a mesh which is about to be deleted. + canUndoInModel = !PeltzerMain.Instance.OperationInProgress() && PeltzerMain.Instance.GetModel().CanUndo(); + // As soon as the user undoes something that is not multi-selection, clear all the redo stacks (the undo stacks would + // already be empty at this point). + selector.ClearMultiSelectRedoState(); + } - private bool IsRedoEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN - && args.TouchpadLocation == TouchpadLocation.RIGHT; - } + audioLibrary.PlayClip(undid || canUndoInModel ? audioLibrary.undoSound : audioLibrary.errorSound); + TriggerHapticFeedback(); - private void ControllerEventHandler(object sender, ControllerEventArgs args) { - if (!PeltzerMain.Instance.restrictionManager.undoRedoAllowed) { - return; - } else if (IsUndoEvent(args)) { - Undo(); - } else if (IsRedoEvent(args)) { - Redo(); - } - } + if (undid || canUndoInModel) + { + if (UndoActionHandler != null) + { + UndoActionHandler(); + } + } - /// - /// The 'undo' command. Special-cased for stroke and subdivide modes, and selected items. - /// - private void Undo() { - bool undid = false; - bool canUndoInModel = false; - Selector selector = PeltzerMain.Instance.GetSelector(); - - if (Features.clickToSelectWithUndoRedoEnabled && selector.selectedMeshes.Count > 0) { - // Divert to local undo for multi-selection - undid = selector.UndoMultiSelect(); - } else if (Features.localUndoRedoEnabled && selector.isMultiSelecting) { - // Divert to local undo for multi-selection - undid = selector.UndoMultiSelect(); - } else if (PeltzerMain.Instance.GetFreeform().IsStroking()) { - // In stroke mode, we try and remove a checkpoint. - undid = PeltzerMain.Instance.GetFreeform().Undo(); - } else if (selector.AnythingSelected()) { - // If anything was selected in any mode, we clear the selection. - undid = true; - selector.DeselectAll(); - } else if (PeltzerMain.Instance.GetDeleter().isDeleting) { - // If we are currently deleting, anything that has been deleted so far is restored. - undid = PeltzerMain.Instance.GetDeleter().CancelDeletionsSoFar(); - } else { - // Else, we actually try and perform 'undo' on the model. We defer the actual 'undo' call until after the - // handler fires, in case tools try and do something with a mesh which is about to be deleted. - canUndoInModel = !PeltzerMain.Instance.OperationInProgress() && PeltzerMain.Instance.GetModel().CanUndo(); - // As soon as the user undoes something that is not multi-selection, clear all the redo stacks (the undo stacks would - // already be empty at this point). - selector.ClearMultiSelectRedoState(); - } - - audioLibrary.PlayClip(undid || canUndoInModel ? audioLibrary.undoSound : audioLibrary.errorSound); - TriggerHapticFeedback(); - - if (undid || canUndoInModel) { - if (UndoActionHandler != null) { - UndoActionHandler(); + if (canUndoInModel) + { + PeltzerMain.Instance.GetModel().Undo(); + } } - } - if (canUndoInModel) { - PeltzerMain.Instance.GetModel().Undo(); - } - } + /// + /// The 'redo' command. No special-cases. + /// + private void Redo() + { + bool redid; + Selector selector = PeltzerMain.Instance.GetSelector(); + + if (Features.clickToSelectWithUndoRedoEnabled + && selector.redoMeshMultiSelect.Count > 0) + { + // Divert to local redo for multi-selection + redid = selector.RedoMultiSelect(); + } + else if (Features.localUndoRedoEnabled && selector.isMultiSelecting) + { + // Divert to local redo for multi-selection + redid = selector.RedoMultiSelect(); + } + else if (PeltzerMain.Instance.OperationInProgress()) + { + redid = false; + } + else + { + // We always remove selections when re-doing. + if (selector.AnythingSelected()) + { + selector.DeselectAll(); + } + redid = PeltzerMain.Instance.GetModel().Redo(); + } - /// - /// The 'redo' command. No special-cases. - /// - private void Redo() { - bool redid; - Selector selector = PeltzerMain.Instance.GetSelector(); - - if (Features.clickToSelectWithUndoRedoEnabled - && selector.redoMeshMultiSelect.Count > 0) { - // Divert to local redo for multi-selection - redid = selector.RedoMultiSelect(); - } else if (Features.localUndoRedoEnabled && selector.isMultiSelecting) { - // Divert to local redo for multi-selection - redid = selector.RedoMultiSelect(); - } else if (PeltzerMain.Instance.OperationInProgress()) { - redid = false; - } else { - // We always remove selections when re-doing. - if (selector.AnythingSelected()) { - selector.DeselectAll(); + audioLibrary.PlayClip(redid ? audioLibrary.redoSound : audioLibrary.errorSound); + TriggerHapticFeedback(); } - redid = PeltzerMain.Instance.GetModel().Redo(); - } - audioLibrary.PlayClip(redid ? audioLibrary.redoSound : audioLibrary.errorSound); - TriggerHapticFeedback(); - } + /// + /// Called when the handedness changes of the controller to accomodate necessary changes. + /// + public void ControllerHandednessChanged() + { + if (handedness == Handedness.LEFT) + { + if (Config.Instance.sdkMode == SdkMode.Oculus) + { + menuPanel.transform.localPosition = menuPanelLeftPosOculus; + polyMenuPanel.transform.localPosition = menuPanelZandriaLeftPosOculus; + detailsMenuPanel.transform.localPosition = detailsPanelZandriaLeftPosOculus; + } + else + { + menuPanel.transform.localPosition = Vector3.zero; + polyMenuPanel.transform.localPosition = Vector3.zero; + detailsMenuPanel.transform.localPosition = detailsPanelZandriaLeftPos; + } + + controllerGeometry.zoomRightTooltip.SetActive(false); + controllerGeometry.moveRightTooltip.SetActive(false); + controllerGeometry.snapRightTooltip.SetActive(false); + controllerGeometry.straightenRightTooltip.SetActive(false); + controllerGeometry.menuRightTooltip.SetActive(false); + + controllerGeometry.snapGrabAssistRightTooltip.SetActive(false); + controllerGeometry.snapGrabHoldRightTooltip.SetActive(false); + controllerGeometry.snapStrokeRightTooltip.SetActive(false); + controllerGeometry.snapShapeInsertRightTooltip.SetActive(false); + controllerGeometry.snapModifyRightTooltip.SetActive(false); + controllerGeometry.snapPaintOrEraseRightTooltip.SetActive(false); + + controllerGeometry.applicationButtonTooltipRight.SetActive(false); + } + else if (handedness == Handedness.RIGHT) + { + if (Config.Instance.sdkMode == SdkMode.Oculus) + { + menuPanel.transform.localPosition = menuPanelRightPosOculus; + polyMenuPanel.transform.localPosition = menuPanelZandriaRightPosOculus; + detailsMenuPanel.transform.localPosition = detailsPanelZandriaRightPosOculus; + } + else + { + menuPanel.transform.localPosition = menuPanelRightPos; + polyMenuPanel.transform.localPosition = menuPanelZandriaRightPos; + detailsMenuPanel.transform.localPosition = detailsPanelZandriaRightPos; + } + + controllerGeometry.zoomLeftTooltip.SetActive(false); + controllerGeometry.moveLeftTooltip.SetActive(false); + controllerGeometry.snapLeftTooltip.SetActive(false); + controllerGeometry.straightenLeftTooltip.SetActive(false); + controllerGeometry.menuLeftTooltip.SetActive(false); + + controllerGeometry.snapGrabAssistLeftTooltip.SetActive(false); + controllerGeometry.snapGrabHoldLeftTooltip.SetActive(false); + controllerGeometry.snapStrokeLeftTooltip.SetActive(false); + controllerGeometry.snapShapeInsertLeftTooltip.SetActive(false); + controllerGeometry.snapModifyLeftTooltip.SetActive(false); + controllerGeometry.snapPaintOrEraseLeftTooltip.SetActive(false); + + controllerGeometry.applicationButtonTooltipLeft.SetActive(false); + } + } - /// - /// Called when the handedness changes of the controller to accomodate necessary changes. - /// - public void ControllerHandednessChanged() { - if (handedness == Handedness.LEFT) { - if (Config.Instance.sdkMode == SdkMode.Oculus) { - menuPanel.transform.localPosition = menuPanelLeftPosOculus; - polyMenuPanel.transform.localPosition = menuPanelZandriaLeftPosOculus; - detailsMenuPanel.transform.localPosition = detailsPanelZandriaLeftPosOculus; - } else { - menuPanel.transform.localPosition = Vector3.zero; - polyMenuPanel.transform.localPosition = Vector3.zero; - detailsMenuPanel.transform.localPosition = detailsPanelZandriaLeftPos; + /// + /// Triggers controller vibration. + /// + /// Better haptic feedback will come with https://bug + /// + public void TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType type = HapticFeedback.HapticFeedbackType.FEEDBACK_1, + float durationSeconds = 0.01f, float strength = 0.3f) + { + + if (hapticsLibrary != null) + { + hapticsLibrary.PlayHapticFeedback(type, durationSeconds, strength); + } } - controllerGeometry.zoomRightTooltip.SetActive(false); - controllerGeometry.moveRightTooltip.SetActive(false); - controllerGeometry.snapRightTooltip.SetActive(false); - controllerGeometry.straightenRightTooltip.SetActive(false); - controllerGeometry.menuRightTooltip.SetActive(false); - - controllerGeometry.snapGrabAssistRightTooltip.SetActive(false); - controllerGeometry.snapGrabHoldRightTooltip.SetActive(false); - controllerGeometry.snapStrokeRightTooltip.SetActive(false); - controllerGeometry.snapShapeInsertRightTooltip.SetActive(false); - controllerGeometry.snapModifyRightTooltip.SetActive(false); - controllerGeometry.snapPaintOrEraseRightTooltip.SetActive(false); - - controllerGeometry.applicationButtonTooltipRight.SetActive(false); - } else if (handedness == Handedness.RIGHT) { - if (Config.Instance.sdkMode == SdkMode.Oculus) { - menuPanel.transform.localPosition = menuPanelRightPosOculus; - polyMenuPanel.transform.localPosition = menuPanelZandriaRightPosOculus; - detailsMenuPanel.transform.localPosition = detailsPanelZandriaRightPosOculus; - } else { - menuPanel.transform.localPosition = menuPanelRightPos; - polyMenuPanel.transform.localPosition = menuPanelZandriaRightPos; - detailsMenuPanel.transform.localPosition = detailsPanelZandriaRightPos; + /// + /// Triggers controller vibrations to get the user to look at the controller. + /// + public void LookAtMe() + { + TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_1, + 0.2f, + 0.5f + ); } - controllerGeometry.zoomLeftTooltip.SetActive(false); - controllerGeometry.moveLeftTooltip.SetActive(false); - controllerGeometry.snapLeftTooltip.SetActive(false); - controllerGeometry.straightenLeftTooltip.SetActive(false); - controllerGeometry.menuLeftTooltip.SetActive(false); - - controllerGeometry.snapGrabAssistLeftTooltip.SetActive(false); - controllerGeometry.snapGrabHoldLeftTooltip.SetActive(false); - controllerGeometry.snapStrokeLeftTooltip.SetActive(false); - controllerGeometry.snapShapeInsertLeftTooltip.SetActive(false); - controllerGeometry.snapModifyLeftTooltip.SetActive(false); - controllerGeometry.snapPaintOrEraseLeftTooltip.SetActive(false); - - controllerGeometry.applicationButtonTooltipLeft.SetActive(false); - } - } + /// + /// Triggers controller vibrations to let the user know they've done a good job. + /// + public void YouDidIt() + { + TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_1, + 0.1f, + 0.2f + ); + } - /// - /// Triggers controller vibration. - /// - /// Better haptic feedback will come with https://bug - /// - public void TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType type = HapticFeedback.HapticFeedbackType.FEEDBACK_1, - float durationSeconds = 0.01f, float strength = 0.3f) { + /// + /// Update the colors on the palette to match the selected color. + /// + /// The new color. + public void UpdateColors(int currentMaterial) + { + if (!PeltzerMain.Instance.restrictionManager.toolheadColorChangeAllowed) + { + return; + } - if (hapticsLibrary != null) { - hapticsLibrary.PlayHapticFeedback(type, durationSeconds, strength); - } - } + // Change the tools on the palette. + foreach (ColorChanger colorChanger in colorChangers) + { + colorChanger.ChangeMaterial(currentMaterial); + } + } - /// - /// Triggers controller vibrations to get the user to look at the controller. - /// - public void LookAtMe() { - TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_1, - 0.2f, - 0.5f - ); - } + /// + /// Depending on the current controller mode, will show the relevant snapping tooltips. + /// + public void ShowSnapAssistanceTooltip() + { + if (!TooltipsAllowed()) return; + + switch (PeltzerMain.Instance.peltzerController.mode) + { + case ControllerMode.insertVolume: + GameObject shapeInsertSnapTooltip = handedness == Handedness.LEFT ? + controllerGeometry.snapShapeInsertLeftTooltip : controllerGeometry.snapShapeInsertRightTooltip; + shapeInsertSnapTooltip.SetActive(true); + break; + case ControllerMode.subdivideFace: + case ControllerMode.reshape: + case ControllerMode.extrude: + GameObject modifySnapTooltip = handedness == Handedness.LEFT ? + controllerGeometry.snapModifyLeftTooltip : controllerGeometry.snapModifyRightTooltip; + modifySnapTooltip.SetActive(true); + break; + case ControllerMode.paintFace: + case ControllerMode.paintMesh: + case ControllerMode.delete: + GameObject paintOrEraseSnapTooltip = handedness == Handedness.LEFT ? + controllerGeometry.snapPaintOrEraseLeftTooltip : controllerGeometry.snapPaintOrEraseRightTooltip; + paintOrEraseSnapTooltip.SetActive(true); + break; + case ControllerMode.insertStroke: + GameObject strokeSnapTooltip = handedness == Handedness.LEFT ? + controllerGeometry.snapStrokeLeftTooltip : controllerGeometry.snapStrokeRightTooltip; + strokeSnapTooltip.SetActive(true); + break; + case ControllerMode.move: + // Give priority to half trigger tool tip over the more general one, because it has an expiration time. + GameObject holdTriggerHalfwaySnapTooltip = handedness == Handedness.LEFT ? + controllerGeometry.snapGrabHoldLeftTooltip : controllerGeometry.snapGrabHoldRightTooltip; + if (holdTriggerHalfwaySnapTooltip.activeSelf) break; + + GameObject grabAssistSnapTooltip = handedness == Handedness.LEFT ? + controllerGeometry.snapGrabAssistLeftTooltip : controllerGeometry.snapGrabAssistRightTooltip; + grabAssistSnapTooltip.SetActive(true); + break; + default: + return; + } + } - /// - /// Triggers controller vibrations to let the user know they've done a good job. - /// - public void YouDidIt() { - TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_1, - 0.1f, - 0.2f - ); - } + /// + /// Show the tool tip that instructs the user on how to hold the half trigger to preview a snap. + /// In a separate function because it has different behavior from other snap tooltips. + /// + public void ShowHoldTriggerHalfwaySnapTooltip() + { + if (!TooltipsAllowed()) return; + + GameObject holdTriggerHalfwaySnapTooltip = handedness == Handedness.LEFT ? + controllerGeometry.snapGrabHoldLeftTooltip : controllerGeometry.snapGrabHoldRightTooltip; + holdTriggerHalfwaySnapTooltip.SetActive(true); + } - /// - /// Update the colors on the palette to match the selected color. - /// - /// The new color. - public void UpdateColors(int currentMaterial) { - if (!PeltzerMain.Instance.restrictionManager.toolheadColorChangeAllowed) { - return; - } - - // Change the tools on the palette. - foreach (ColorChanger colorChanger in colorChangers) { - colorChanger.ChangeMaterial(currentMaterial); - } - } + /// + /// Returns whether tooltips are currently allowed. + /// + private bool TooltipsAllowed() + { + return PeltzerMain.Instance.restrictionManager.tooltipsAllowed + && !PeltzerMain.Instance.tutorialManager.TutorialOccurring() + && !PeltzerMain.Instance.HasDisabledTooltips; + } - /// - /// Depending on the current controller mode, will show the relevant snapping tooltips. - /// - public void ShowSnapAssistanceTooltip() { - if (!TooltipsAllowed()) return; - - switch (PeltzerMain.Instance.peltzerController.mode) { - case ControllerMode.insertVolume: - GameObject shapeInsertSnapTooltip = handedness == Handedness.LEFT ? - controllerGeometry.snapShapeInsertLeftTooltip : controllerGeometry.snapShapeInsertRightTooltip; - shapeInsertSnapTooltip.SetActive(true); - break; - case ControllerMode.subdivideFace: - case ControllerMode.reshape: - case ControllerMode.extrude: - GameObject modifySnapTooltip = handedness == Handedness.LEFT ? - controllerGeometry.snapModifyLeftTooltip : controllerGeometry.snapModifyRightTooltip; - modifySnapTooltip.SetActive(true); - break; - case ControllerMode.paintFace: - case ControllerMode.paintMesh: - case ControllerMode.delete: - GameObject paintOrEraseSnapTooltip = handedness == Handedness.LEFT ? - controllerGeometry.snapPaintOrEraseLeftTooltip : controllerGeometry.snapPaintOrEraseRightTooltip; - paintOrEraseSnapTooltip.SetActive(true); - break; - case ControllerMode.insertStroke: - GameObject strokeSnapTooltip = handedness == Handedness.LEFT ? - controllerGeometry.snapStrokeLeftTooltip : controllerGeometry.snapStrokeRightTooltip; - strokeSnapTooltip.SetActive(true); - break; - case ControllerMode.move: - // Give priority to half trigger tool tip over the more general one, because it has an expiration time. - GameObject holdTriggerHalfwaySnapTooltip = handedness == Handedness.LEFT ? - controllerGeometry.snapGrabHoldLeftTooltip : controllerGeometry.snapGrabHoldRightTooltip; - if (holdTriggerHalfwaySnapTooltip.activeSelf) break; - - GameObject grabAssistSnapTooltip = handedness == Handedness.LEFT ? - controllerGeometry.snapGrabAssistLeftTooltip : controllerGeometry.snapGrabAssistRightTooltip; - grabAssistSnapTooltip.SetActive(true); - break; - default: - return; - } - } + public void DisableSnapTooltips() + { + GameObject snapTooltip = handedness == Handedness.LEFT ? + controllerGeometry.snapLeftTooltip : controllerGeometry.snapRightTooltip; + snapTooltip.SetActive(false); + GameObject straightenTooltip = handedness == Handedness.LEFT ? + controllerGeometry.straightenLeftTooltip : controllerGeometry.straightenRightTooltip; + straightenTooltip.SetActive(false); + } - /// - /// Show the tool tip that instructs the user on how to hold the half trigger to preview a snap. - /// In a separate function because it has different behavior from other snap tooltips. - /// - public void ShowHoldTriggerHalfwaySnapTooltip() { - if (!TooltipsAllowed()) return; + public void HideSnapAssistanceTooltips() + { + GameObject snapGrabAssistTooltip = handedness == Handedness.LEFT ? + controllerGeometry.snapGrabAssistLeftTooltip : controllerGeometry.snapGrabAssistRightTooltip; + snapGrabAssistTooltip.SetActive(false); + GameObject snapGrabHoldTooltip = handedness == Handedness.LEFT ? + controllerGeometry.snapGrabHoldLeftTooltip : controllerGeometry.snapGrabHoldRightTooltip; + snapGrabHoldTooltip.SetActive(false); + GameObject snapStrokeTooltip = handedness == Handedness.LEFT ? + controllerGeometry.snapStrokeLeftTooltip : controllerGeometry.snapStrokeRightTooltip; + snapStrokeTooltip.SetActive(false); + GameObject snapShapeInsertTooltip = handedness == Handedness.LEFT ? + controllerGeometry.snapShapeInsertLeftTooltip : controllerGeometry.snapShapeInsertRightTooltip; + snapShapeInsertTooltip.SetActive(false); + GameObject snapModifyTooltip = handedness == Handedness.LEFT ? + controllerGeometry.snapModifyLeftTooltip : controllerGeometry.snapModifyRightTooltip; + snapModifyTooltip.SetActive(false); + GameObject snapPaintOrEraseTooltip = handedness == Handedness.LEFT ? + controllerGeometry.snapPaintOrEraseLeftTooltip : controllerGeometry.snapPaintOrEraseRightTooltip; + snapPaintOrEraseTooltip.SetActive(false); + } - GameObject holdTriggerHalfwaySnapTooltip = handedness == Handedness.LEFT ? - controllerGeometry.snapGrabHoldLeftTooltip : controllerGeometry.snapGrabHoldRightTooltip; - holdTriggerHalfwaySnapTooltip.SetActive(true); - } + /// + /// Determines which tooltip and where to show it when called. These are the grip tooltips to + /// advise a user how to move/zoom the world. + /// We only show these tooltips until the user has successfully moved or zoomed the world. + /// We do not show these tooltips until at least one object is in the scene. + /// + public void SetGripTooltip() + { + if (!PeltzerController.AcquireIfNecessary(ref PeltzerMain.Instance.peltzerController)) return; + + if (PeltzerMain.Instance.Zoomer.userHasEverZoomed + || !PeltzerMain.Instance.restrictionManager.tooltipsAllowed + || PeltzerMain.Instance.HasDisabledTooltips + || PeltzerMain.Instance.tutorialManager.TutorialOccurring()) + { + DisableGripTooltips(); + return; + } - /// - /// Returns whether tooltips are currently allowed. - /// - private bool TooltipsAllowed() { - return PeltzerMain.Instance.restrictionManager.tooltipsAllowed - && !PeltzerMain.Instance.tutorialManager.TutorialOccurring() - && !PeltzerMain.Instance.HasDisabledTooltips; - } + GameObject zoomTooltip = handedness == Handedness.LEFT ? + controllerGeometry.zoomLeftTooltip : controllerGeometry.zoomRightTooltip; + GameObject moveTooltip = handedness == Handedness.LEFT ? + controllerGeometry.moveLeftTooltip : controllerGeometry.moveRightTooltip; + if (controller.IsPressed(ButtonId.Grip) + && PeltzerMain.Instance.peltzerController.controller.IsPressed(ButtonId.Grip)) + { + zoomTooltip.SetActive(false); + // Stop pulsating glow highlight on grips. + float emission = 0; + Color highlightColor = BASE_COLOR * Mathf.LinearToGammaSpace(emission); + if (controllerGeometry.gripLeft != null) + { + controllerGeometry.gripLeft.GetComponent().material.SetColor("_EmissionColor", highlightColor); + } + if (controllerGeometry.gripRight != null) + { + controllerGeometry.gripRight.GetComponent().material.SetColor("_EmissionColor", highlightColor); + } + // hide the hold to move tooltip + moveTooltip.SetActive(false); + } + else if (controller.IsPressed(ButtonId.Grip) + && !PeltzerMain.Instance.peltzerController.controller.IsPressed(ButtonId.Grip)) + { + zoomTooltip.SetActive(true); + moveTooltip.SetActive(false); + } + else if (!controller.IsPressed(ButtonId.Grip) + && PeltzerMain.Instance.peltzerController.controller.IsPressed(ButtonId.Grip)) + { + zoomTooltip.SetActive(true); + // Pulsating glow highlight on grips. + float emission = Mathf.PingPong(Time.time / 2F, 0.4f); + Color highlightColor = BASE_COLOR * Mathf.LinearToGammaSpace(emission); + if (controllerGeometry.gripLeft != null) + { + controllerGeometry.gripLeft.GetComponent().material.SetColor("_EmissionColor", highlightColor); + } + if (controllerGeometry.gripRight != null) + { + controllerGeometry.gripRight.GetComponent().material.SetColor("_EmissionColor", highlightColor); + } + moveTooltip.SetActive(false); + } + else + { + DisableGripTooltips(); + } + } - public void DisableSnapTooltips() { - GameObject snapTooltip = handedness == Handedness.LEFT ? - controllerGeometry.snapLeftTooltip : controllerGeometry.snapRightTooltip; - snapTooltip.SetActive(false); - GameObject straightenTooltip = handedness == Handedness.LEFT ? - controllerGeometry.straightenLeftTooltip : controllerGeometry.straightenRightTooltip; - straightenTooltip.SetActive(false); - } + /// + /// Disables the 'hold to move' and 'hold to zoom' tooltips, and grip-button pulsing. + /// + public void DisableGripTooltips() + { + GameObject zoomTooltip = handedness == Handedness.LEFT ? + controllerGeometry.zoomLeftTooltip : controllerGeometry.zoomRightTooltip; + GameObject moveTooltip = handedness == Handedness.LEFT ? + controllerGeometry.moveLeftTooltip : controllerGeometry.moveRightTooltip; + + zoomTooltip.SetActive(false); + moveTooltip.SetActive(false); + // Stop pulsating glow highlight on grips. + float emission = 0; + Color highlightColor = BASE_COLOR * Mathf.LinearToGammaSpace(emission); + + if (controllerGeometry.gripLeft != null) + { + controllerGeometry.gripLeft.GetComponent().material.SetColor("_EmissionColor", highlightColor); + } + if (controllerGeometry.gripRight != null) + { + controllerGeometry.gripRight.GetComponent().material.SetColor("_EmissionColor", highlightColor); + } + } - public void HideSnapAssistanceTooltips() { - GameObject snapGrabAssistTooltip = handedness == Handedness.LEFT ? - controllerGeometry.snapGrabAssistLeftTooltip : controllerGeometry.snapGrabAssistRightTooltip; - snapGrabAssistTooltip.SetActive(false); - GameObject snapGrabHoldTooltip = handedness == Handedness.LEFT ? - controllerGeometry.snapGrabHoldLeftTooltip : controllerGeometry.snapGrabHoldRightTooltip; - snapGrabHoldTooltip.SetActive(false); - GameObject snapStrokeTooltip = handedness == Handedness.LEFT ? - controllerGeometry.snapStrokeLeftTooltip : controllerGeometry.snapStrokeRightTooltip; - snapStrokeTooltip.SetActive(false); - GameObject snapShapeInsertTooltip = handedness == Handedness.LEFT ? - controllerGeometry.snapShapeInsertLeftTooltip : controllerGeometry.snapShapeInsertRightTooltip; - snapShapeInsertTooltip.SetActive(false); - GameObject snapModifyTooltip = handedness == Handedness.LEFT ? - controllerGeometry.snapModifyLeftTooltip : controllerGeometry.snapModifyRightTooltip; - snapModifyTooltip.SetActive(false); - GameObject snapPaintOrEraseTooltip = handedness == Handedness.LEFT ? - controllerGeometry.snapPaintOrEraseLeftTooltip : controllerGeometry.snapPaintOrEraseRightTooltip; - snapPaintOrEraseTooltip.SetActive(false); - } + /// + /// Make the app menu button red or grey depending on whether or not its active. + /// + /// + public void SetApplicationButtonOverlay(bool active) + { + if (controllerGeometry.appMenuButton == null) return; + + Material material = controllerGeometry.appMenuButton.GetComponent().material; + Color highlightEmmissionColor; + Color highlightColor; + if (active) + { + highlightEmmissionColor = ACTIVE_BUTTON_COLOR * Mathf.LinearToGammaSpace(0.6f); // Set emission to 60%. + highlightColor = ACTIVE_BUTTON_COLOR; + } + else + { + highlightEmmissionColor = INACTIVE_BUTTON_COLOR * Mathf.LinearToGammaSpace(0f); + highlightColor = INACTIVE_BUTTON_COLOR; + } - /// - /// Determines which tooltip and where to show it when called. These are the grip tooltips to - /// advise a user how to move/zoom the world. - /// We only show these tooltips until the user has successfully moved or zoomed the world. - /// We do not show these tooltips until at least one object is in the scene. - /// - public void SetGripTooltip() { - if (!PeltzerController.AcquireIfNecessary(ref PeltzerMain.Instance.peltzerController)) return; - - if (PeltzerMain.Instance.Zoomer.userHasEverZoomed - || !PeltzerMain.Instance.restrictionManager.tooltipsAllowed - || PeltzerMain.Instance.HasDisabledTooltips - || PeltzerMain.Instance.tutorialManager.TutorialOccurring()) { - DisableGripTooltips(); - return; - } - - GameObject zoomTooltip = handedness == Handedness.LEFT ? - controllerGeometry.zoomLeftTooltip : controllerGeometry.zoomRightTooltip; - GameObject moveTooltip = handedness == Handedness.LEFT ? - controllerGeometry.moveLeftTooltip : controllerGeometry.moveRightTooltip; - if (controller.IsPressed(ButtonId.Grip) - && PeltzerMain.Instance.peltzerController.controller.IsPressed(ButtonId.Grip)) { - zoomTooltip.SetActive(false); - // Stop pulsating glow highlight on grips. - float emission = 0; - Color highlightColor = BASE_COLOR * Mathf.LinearToGammaSpace(emission); - if (controllerGeometry.gripLeft != null) { - controllerGeometry.gripLeft.GetComponent().material.SetColor("_EmissionColor", highlightColor); - } - if (controllerGeometry.gripRight != null) { - controllerGeometry.gripRight.GetComponent().material.SetColor("_EmissionColor", highlightColor); - } - // hide the hold to move tooltip - moveTooltip.SetActive(false); - } else if (controller.IsPressed(ButtonId.Grip) - && !PeltzerMain.Instance.peltzerController.controller.IsPressed(ButtonId.Grip)) { - zoomTooltip.SetActive(true); - moveTooltip.SetActive(false); - } else if (!controller.IsPressed(ButtonId.Grip) - && PeltzerMain.Instance.peltzerController.controller.IsPressed(ButtonId.Grip)) { - zoomTooltip.SetActive(true); - // Pulsating glow highlight on grips. - float emission = Mathf.PingPong(Time.time / 2F, 0.4f); - Color highlightColor = BASE_COLOR * Mathf.LinearToGammaSpace(emission); - if (controllerGeometry.gripLeft != null) { - controllerGeometry.gripLeft.GetComponent().material.SetColor("_EmissionColor", highlightColor); - } - if (controllerGeometry.gripRight != null) { - controllerGeometry.gripRight.GetComponent().material.SetColor("_EmissionColor", highlightColor); + material.SetColor("_EmissionColor", highlightEmmissionColor); + material.color = highlightColor; } - moveTooltip.SetActive(false); - } else { - DisableGripTooltips(); - } - } - /// - /// Disables the 'hold to move' and 'hold to zoom' tooltips, and grip-button pulsing. - /// - public void DisableGripTooltips() { - GameObject zoomTooltip = handedness == Handedness.LEFT ? - controllerGeometry.zoomLeftTooltip : controllerGeometry.zoomRightTooltip; - GameObject moveTooltip = handedness == Handedness.LEFT ? - controllerGeometry.moveLeftTooltip : controllerGeometry.moveRightTooltip; - - zoomTooltip.SetActive(false); - moveTooltip.SetActive(false); - // Stop pulsating glow highlight on grips. - float emission = 0; - Color highlightColor = BASE_COLOR * Mathf.LinearToGammaSpace(emission); - - if (controllerGeometry.gripLeft != null) { - controllerGeometry.gripLeft.GetComponent().material.SetColor("_EmissionColor", highlightColor); - } - if (controllerGeometry.gripRight != null) { - controllerGeometry.gripRight.GetComponent().material.SetColor("_EmissionColor", highlightColor); - } - } + /// + /// Make the secondary button red or grey depending on whether or not its active. + /// + /// + public void SetSecondaryButtonOverlay(bool active) + { + if (controllerGeometry.secondaryButton == null) return; + + Material material = controllerGeometry.secondaryButton.GetComponent().material; + Color highlightEmmissionColor; + Color highlightColor; + if (active) + { + highlightEmmissionColor = ACTIVE_BUTTON_COLOR * Mathf.LinearToGammaSpace(0.6f); // Set emission to 60%. + highlightColor = ACTIVE_BUTTON_COLOR; + } + else + { + highlightEmmissionColor = INACTIVE_BUTTON_COLOR * Mathf.LinearToGammaSpace(0f); + highlightColor = INACTIVE_BUTTON_COLOR; + } - /// - /// Make the app menu button red or grey depending on whether or not its active. - /// - /// - public void SetApplicationButtonOverlay(bool active) { - if (controllerGeometry.appMenuButton == null) return; - - Material material = controllerGeometry.appMenuButton.GetComponent().material; - Color highlightEmmissionColor; - Color highlightColor; - if (active) { - highlightEmmissionColor = ACTIVE_BUTTON_COLOR * Mathf.LinearToGammaSpace(0.6f); // Set emission to 60%. - highlightColor = ACTIVE_BUTTON_COLOR; - } else { - highlightEmmissionColor = INACTIVE_BUTTON_COLOR * Mathf.LinearToGammaSpace(0f); - highlightColor = INACTIVE_BUTTON_COLOR; - } - - material.SetColor("_EmissionColor", highlightEmmissionColor); - material.color = highlightColor; - } + material.SetColor("_EmissionColor", highlightEmmissionColor); + material.color = highlightColor; + } - /// - /// Make the secondary button red or grey depending on whether or not its active. - /// - /// - public void SetSecondaryButtonOverlay(bool active) { - if (controllerGeometry.secondaryButton == null) return; - - Material material = controllerGeometry.secondaryButton.GetComponent().material; - Color highlightEmmissionColor; - Color highlightColor; - if (active) { - highlightEmmissionColor = ACTIVE_BUTTON_COLOR * Mathf.LinearToGammaSpace(0.6f); // Set emission to 60%. - highlightColor = ACTIVE_BUTTON_COLOR; - } else { - highlightEmmissionColor = INACTIVE_BUTTON_COLOR * Mathf.LinearToGammaSpace(0f); - highlightColor = INACTIVE_BUTTON_COLOR; - } - - material.SetColor("_EmissionColor", highlightEmmissionColor); - material.color = highlightColor; - } + /// + /// Takes a controller and registers any event handlers with the specified controller in order to facilitate + /// event handling between controllers. + /// + /// The controller containing the handlers to register against. + public void RegisterCrossControllerHandlers(PeltzerController controller) + { + // Register the ShapeToolheadAnimation component for shape and material changes. + ShapeToolheadAnimation sta = gameObject.GetComponent(); + controller.shapesMenu.ShapeMenuItemChangedHandler += sta.ShapeChangedHandler; + } - /// - /// Takes a controller and registers any event handlers with the specified controller in order to facilitate - /// event handling between controllers. - /// - /// The controller containing the handlers to register against. - public void RegisterCrossControllerHandlers(PeltzerController controller) { - // Register the ShapeToolheadAnimation component for shape and material changes. - ShapeToolheadAnimation sta = gameObject.GetComponent(); - controller.shapesMenu.ShapeMenuItemChangedHandler += sta.ShapeChangedHandler; - } + public void ResetTouchpadOverlay() + { + PolyMenuMain polyMenuMain = PeltzerMain.Instance.GetPolyMenuMain(); + Zoomer zoomer = PeltzerMain.Instance.Zoomer; + if (zoomer != null && zoomer.isMovingWithPaletteController) + { + ChangeTouchpadOverlay(TouchpadOverlay.RESET_ZOOM); + } + else if (polyMenuMain != null && (polyMenuMain.DetailsMenuIsActive() || polyMenuMain.PolyMenuIsActive())) + { + ChangeTouchpadOverlay(TouchpadOverlay.MENU); + } + else + { + ChangeTouchpadOverlay(TouchpadOverlay.UNDO_REDO); + } + } - public void ResetTouchpadOverlay() { - PolyMenuMain polyMenuMain = PeltzerMain.Instance.GetPolyMenuMain(); - Zoomer zoomer = PeltzerMain.Instance.Zoomer; - if (zoomer != null && zoomer.isMovingWithPaletteController) { - ChangeTouchpadOverlay(TouchpadOverlay.RESET_ZOOM); - } else if (polyMenuMain != null && (polyMenuMain.DetailsMenuIsActive() || polyMenuMain.PolyMenuIsActive())) { - ChangeTouchpadOverlay(TouchpadOverlay.MENU); - } else { - ChangeTouchpadOverlay(TouchpadOverlay.UNDO_REDO); - } - } + /// + /// Change the touchpad overlay to the given type. Will automatically highlight selected + /// modes where appropriate. + /// + /// + public void ChangeTouchpadOverlay(TouchpadOverlay newOverlay) + { + currentOverlay = newOverlay; + + // Set the correct parent overlay active based on the passed overlay. Some parent overlays have sub-overlays that + // will be set based on ControllerMode. + controllerGeometry.volumeInserterOverlay.SetActive(currentOverlay == TouchpadOverlay.VOLUME_INSERTER); + controllerGeometry.freeformOverlay.SetActive(currentOverlay == TouchpadOverlay.FREEFORM); + controllerGeometry.paintOverlay.SetActive(currentOverlay == TouchpadOverlay.PAINT); + controllerGeometry.modifyOverlay.SetActive(currentOverlay == TouchpadOverlay.MODIFY); + + controllerGeometry.moveOverlay.SetActive(currentOverlay == TouchpadOverlay.MOVE); + if (controllerGeometry.OnMoveOverlay != null) controllerGeometry.OnMoveOverlay.SetActive(currentOverlay == TouchpadOverlay.MOVE); + + controllerGeometry.deleteOverlay.SetActive(currentOverlay == TouchpadOverlay.DELETE); + + controllerGeometry.menuOverlay.SetActive(currentOverlay == TouchpadOverlay.MENU); + if (controllerGeometry.OnMenuOverlay != null) controllerGeometry.OnMenuOverlay.SetActive(currentOverlay == TouchpadOverlay.MENU); + + controllerGeometry.undoRedoOverlay.SetActive(currentOverlay == TouchpadOverlay.UNDO_REDO); + if (controllerGeometry.OnUndoRedoOverlay != null) controllerGeometry.OnUndoRedoOverlay.SetActive(currentOverlay == TouchpadOverlay.UNDO_REDO); + + controllerGeometry.resizeOverlay.SetActive(currentOverlay == TouchpadOverlay.RESIZE); + controllerGeometry.resetZoomOverlay.SetActive(currentOverlay == TouchpadOverlay.RESET_ZOOM); + + // Set the secondary button active if we need to show the reset zoom overlay. + SetSecondaryButtonOverlay(/*active*/ currentOverlay == TouchpadOverlay.RESET_ZOOM); + // The user can't open the menu while zooming. + SetApplicationButtonOverlay(/*active*/ currentOverlay != TouchpadOverlay.RESET_ZOOM); + + // The palette controller overlays have no sub-overlays so we don't need to worry about setting them. But if they + // did we would copy what we do in peltzer controller and do it here. + + // Get reference to current overlay. + GameObject currentOverlayGO; + switch (currentOverlay) + { + case TouchpadOverlay.VOLUME_INSERTER: + currentOverlayGO = controllerGeometry.volumeInserterOverlay; + break; + case TouchpadOverlay.FREEFORM: + currentOverlayGO = controllerGeometry.freeformOverlay; + break; + case TouchpadOverlay.PAINT: + currentOverlayGO = controllerGeometry.paintOverlay; + break; + case TouchpadOverlay.MODIFY: + currentOverlayGO = controllerGeometry.modifyOverlay; + break; + case TouchpadOverlay.MOVE: + currentOverlayGO = controllerGeometry.moveOverlay; + break; + case TouchpadOverlay.DELETE: + currentOverlayGO = controllerGeometry.deleteOverlay; + break; + case TouchpadOverlay.MENU: + currentOverlayGO = controllerGeometry.menuOverlay; + break; + case TouchpadOverlay.UNDO_REDO: + currentOverlayGO = controllerGeometry.undoRedoOverlay; + break; + case TouchpadOverlay.RESIZE: + currentOverlayGO = controllerGeometry.resizeOverlay; + break; + case TouchpadOverlay.RESET_ZOOM: + currentOverlayGO = controllerGeometry.resetZoomOverlay; + break; + default: + currentOverlayGO = null; + break; + } - /// - /// Change the touchpad overlay to the given type. Will automatically highlight selected - /// modes where appropriate. - /// - /// - public void ChangeTouchpadOverlay(TouchpadOverlay newOverlay) { - currentOverlay = newOverlay; - - // Set the correct parent overlay active based on the passed overlay. Some parent overlays have sub-overlays that - // will be set based on ControllerMode. - controllerGeometry.volumeInserterOverlay.SetActive(currentOverlay == TouchpadOverlay.VOLUME_INSERTER); - controllerGeometry.freeformOverlay.SetActive(currentOverlay == TouchpadOverlay.FREEFORM); - controllerGeometry.paintOverlay.SetActive(currentOverlay == TouchpadOverlay.PAINT); - controllerGeometry.modifyOverlay.SetActive(currentOverlay == TouchpadOverlay.MODIFY); - - controllerGeometry.moveOverlay.SetActive(currentOverlay == TouchpadOverlay.MOVE); - if (controllerGeometry.OnMoveOverlay != null) controllerGeometry.OnMoveOverlay.SetActive(currentOverlay == TouchpadOverlay.MOVE); - - controllerGeometry.deleteOverlay.SetActive(currentOverlay == TouchpadOverlay.DELETE); - - controllerGeometry.menuOverlay.SetActive(currentOverlay == TouchpadOverlay.MENU); - if (controllerGeometry.OnMenuOverlay != null) controllerGeometry.OnMenuOverlay.SetActive(currentOverlay == TouchpadOverlay.MENU); - - controllerGeometry.undoRedoOverlay.SetActive(currentOverlay == TouchpadOverlay.UNDO_REDO); - if (controllerGeometry.OnUndoRedoOverlay != null) controllerGeometry.OnUndoRedoOverlay.SetActive(currentOverlay == TouchpadOverlay.UNDO_REDO); - - controllerGeometry.resizeOverlay.SetActive(currentOverlay == TouchpadOverlay.RESIZE); - controllerGeometry.resetZoomOverlay.SetActive(currentOverlay == TouchpadOverlay.RESET_ZOOM); - - // Set the secondary button active if we need to show the reset zoom overlay. - SetSecondaryButtonOverlay(/*active*/ currentOverlay == TouchpadOverlay.RESET_ZOOM); - // The user can't open the menu while zooming. - SetApplicationButtonOverlay(/*active*/ currentOverlay != TouchpadOverlay.RESET_ZOOM); - - // The palette controller overlays have no sub-overlays so we don't need to worry about setting them. But if they - // did we would copy what we do in peltzer controller and do it here. - - // Get reference to current overlay. - GameObject currentOverlayGO; - switch (currentOverlay) { - case TouchpadOverlay.VOLUME_INSERTER: - currentOverlayGO = controllerGeometry.volumeInserterOverlay; - break; - case TouchpadOverlay.FREEFORM: - currentOverlayGO = controllerGeometry.freeformOverlay; - break; - case TouchpadOverlay.PAINT: - currentOverlayGO = controllerGeometry.paintOverlay; - break; - case TouchpadOverlay.MODIFY: - currentOverlayGO = controllerGeometry.modifyOverlay; - break; - case TouchpadOverlay.MOVE: - currentOverlayGO = controllerGeometry.moveOverlay; - break; - case TouchpadOverlay.DELETE: - currentOverlayGO = controllerGeometry.deleteOverlay; - break; - case TouchpadOverlay.MENU: - currentOverlayGO = controllerGeometry.menuOverlay; - break; - case TouchpadOverlay.UNDO_REDO: - currentOverlayGO = controllerGeometry.undoRedoOverlay; - break; - case TouchpadOverlay.RESIZE: - currentOverlayGO = controllerGeometry.resizeOverlay; - break; - case TouchpadOverlay.RESET_ZOOM: - currentOverlayGO = controllerGeometry.resetZoomOverlay; - break; - default: - currentOverlayGO = null; - break; - } - - // Update cache reference to current Overlay component. - if (currentOverlayGO != null) { - overlay = currentOverlayGO.GetComponent(); - } - } + // Update cache reference to current Overlay component. + if (currentOverlayGO != null) + { + overlay = currentOverlayGO.GetComponent(); + } + } - public TouchpadOverlay TouchpadOverlay { get { return currentOverlay; } } - - public void HideTooltips() { - controllerGeometry.snapLeftTooltip.SetActive(false); - controllerGeometry.snapRightTooltip.SetActive(false); - controllerGeometry.straightenLeftTooltip.SetActive(false); - controllerGeometry.straightenRightTooltip.SetActive(false); - controllerGeometry.zoomLeftTooltip.SetActive(false); - controllerGeometry.zoomRightTooltip.SetActive(false); - controllerGeometry.moveLeftTooltip.SetActive(false); - controllerGeometry.moveRightTooltip.SetActive(false); - controllerGeometry.applicationButtonTooltipLeft.SetActive(false); - controllerGeometry.applicationButtonTooltipRight.SetActive(false); - - controllerGeometry.snapGrabAssistLeftTooltip.SetActive(false); - controllerGeometry.snapGrabAssistRightTooltip.SetActive(false); - controllerGeometry.snapGrabHoldLeftTooltip.SetActive(false); - controllerGeometry.snapGrabHoldRightTooltip.SetActive(false); - controllerGeometry.snapStrokeLeftTooltip.SetActive(false); - controllerGeometry.snapStrokeRightTooltip.SetActive(false); - controllerGeometry.snapShapeInsertLeftTooltip.SetActive(false); - controllerGeometry.snapShapeInsertRightTooltip.SetActive(false); - controllerGeometry.snapModifyLeftTooltip.SetActive(false); - controllerGeometry.snapModifyRightTooltip.SetActive(false); - controllerGeometry.snapPaintOrEraseLeftTooltip.SetActive(false); - controllerGeometry.snapPaintOrEraseRightTooltip.SetActive(false); - SetTouchpadHoverTexture(TouchpadHoverState.NONE); - } + public TouchpadOverlay TouchpadOverlay { get { return currentOverlay; } } + + public void HideTooltips() + { + controllerGeometry.snapLeftTooltip.SetActive(false); + controllerGeometry.snapRightTooltip.SetActive(false); + controllerGeometry.straightenLeftTooltip.SetActive(false); + controllerGeometry.straightenRightTooltip.SetActive(false); + controllerGeometry.zoomLeftTooltip.SetActive(false); + controllerGeometry.zoomRightTooltip.SetActive(false); + controllerGeometry.moveLeftTooltip.SetActive(false); + controllerGeometry.moveRightTooltip.SetActive(false); + controllerGeometry.applicationButtonTooltipLeft.SetActive(false); + controllerGeometry.applicationButtonTooltipRight.SetActive(false); + + controllerGeometry.snapGrabAssistLeftTooltip.SetActive(false); + controllerGeometry.snapGrabAssistRightTooltip.SetActive(false); + controllerGeometry.snapGrabHoldLeftTooltip.SetActive(false); + controllerGeometry.snapGrabHoldRightTooltip.SetActive(false); + controllerGeometry.snapStrokeLeftTooltip.SetActive(false); + controllerGeometry.snapStrokeRightTooltip.SetActive(false); + controllerGeometry.snapShapeInsertLeftTooltip.SetActive(false); + controllerGeometry.snapShapeInsertRightTooltip.SetActive(false); + controllerGeometry.snapModifyLeftTooltip.SetActive(false); + controllerGeometry.snapModifyRightTooltip.SetActive(false); + controllerGeometry.snapPaintOrEraseLeftTooltip.SetActive(false); + controllerGeometry.snapPaintOrEraseRightTooltip.SetActive(false); + SetTouchpadHoverTexture(TouchpadHoverState.NONE); + } - /// - /// Set the hover state material on the controller. - /// - /// State of the hover state to match. - public void SetTouchpadHoverTexture(TouchpadHoverState state) { - // Only for VIVE currently. - if (Config.Instance.VrHardware == VrHardware.Vive) { - // Set state of hover icon for the current overlay. - // Reset scale to default. - if (overlay != null) { - Vector3 resetScale = new Vector3(1.0f, 1.0f, 1.0f); - overlay.upIcon.transform.localScale = resetScale; - overlay.downIcon.transform.localScale = resetScale; - overlay.leftIcon.transform.localScale = resetScale; - overlay.rightIcon.transform.localScale = resetScale; - // Reset positions to default. - overlay.upIcon.transform.localPosition = UP_OVERLAY_ICON_DEFAULT_POSITION_VIVE; - overlay.downIcon.transform.localPosition = DOWN_OVERLAY_ICON_DEFAULT_POSITION_VIVE; - overlay.leftIcon.transform.localPosition = LEFT_OVERLAY_ICON_DEFAULT_POSITION_VIVE; - overlay.rightIcon.transform.localPosition = RIGHT_OVERLAY_ICON_DEFAULT_POSITION_VIVE; - // } - SpriteRenderer icon = null; - switch (state) { - case TouchpadHoverState.UP: - icon = overlay.upIcon; - icon.transform.localScale *= 1.25f; - icon.transform.localPosition = UP_OVERLAY_ICON_HOVER_POSITION; - - break; - case TouchpadHoverState.DOWN: - icon = overlay.downIcon; - icon.transform.localScale *= 1.25f; - icon.transform.localPosition = DOWN_OVERLAY_ICON_HOVER_POSITION; - break; - case TouchpadHoverState.LEFT: - icon = overlay.leftIcon; - icon.transform.localScale *= 1.25f; - icon.transform.localPosition = LEFT_OVERLAY_ICON_HOVER_POSITION; - break; - case TouchpadHoverState.RIGHT: - icon = overlay.rightIcon; - icon.transform.localScale *= 1.25f; - icon.transform.localPosition = RIGHT_OVERLAY_ICON_HOVER_POSITION; - break; - case TouchpadHoverState.NONE: - break; - } - if (state != lastTouchpadHoverState && icon && icon.gameObject.activeInHierarchy) { - TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_1, - 0.003f, - 0.15f - ); - } - lastTouchpadHoverState = state; + /// + /// Set the hover state material on the controller. + /// + /// State of the hover state to match. + public void SetTouchpadHoverTexture(TouchpadHoverState state) + { + // Only for VIVE currently. + if (Config.Instance.VrHardware == VrHardware.Vive) + { + // Set state of hover icon for the current overlay. + // Reset scale to default. + if (overlay != null) + { + Vector3 resetScale = new Vector3(1.0f, 1.0f, 1.0f); + overlay.upIcon.transform.localScale = resetScale; + overlay.downIcon.transform.localScale = resetScale; + overlay.leftIcon.transform.localScale = resetScale; + overlay.rightIcon.transform.localScale = resetScale; + // Reset positions to default. + overlay.upIcon.transform.localPosition = UP_OVERLAY_ICON_DEFAULT_POSITION_VIVE; + overlay.downIcon.transform.localPosition = DOWN_OVERLAY_ICON_DEFAULT_POSITION_VIVE; + overlay.leftIcon.transform.localPosition = LEFT_OVERLAY_ICON_DEFAULT_POSITION_VIVE; + overlay.rightIcon.transform.localPosition = RIGHT_OVERLAY_ICON_DEFAULT_POSITION_VIVE; + // } + SpriteRenderer icon = null; + switch (state) + { + case TouchpadHoverState.UP: + icon = overlay.upIcon; + icon.transform.localScale *= 1.25f; + icon.transform.localPosition = UP_OVERLAY_ICON_HOVER_POSITION; + + break; + case TouchpadHoverState.DOWN: + icon = overlay.downIcon; + icon.transform.localScale *= 1.25f; + icon.transform.localPosition = DOWN_OVERLAY_ICON_HOVER_POSITION; + break; + case TouchpadHoverState.LEFT: + icon = overlay.leftIcon; + icon.transform.localScale *= 1.25f; + icon.transform.localPosition = LEFT_OVERLAY_ICON_HOVER_POSITION; + break; + case TouchpadHoverState.RIGHT: + icon = overlay.rightIcon; + icon.transform.localScale *= 1.25f; + icon.transform.localPosition = RIGHT_OVERLAY_ICON_HOVER_POSITION; + break; + case TouchpadHoverState.NONE: + break; + } + if (state != lastTouchpadHoverState && icon && icon.gameObject.activeInHierarchy) + { + TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_1, + 0.003f, + 0.15f + ); + } + lastTouchpadHoverState = state; + } + } } - } } - } } diff --git a/Assets/Scripts/model/controller/PeltzerController.cs b/Assets/Scripts/model/controller/PeltzerController.cs index fc40c772..ac2fe977 100644 --- a/Assets/Scripts/model/controller/PeltzerController.cs +++ b/Assets/Scripts/model/controller/PeltzerController.cs @@ -26,1702 +26,1937 @@ using com.google.apps.peltzer.client.zandria; using com.google.apps.peltzer.client.app; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Delegate method for controller clients to implement. - /// - public delegate void PeltzerControllerActionHandler(object sender, ControllerEventArgs args); - - /// - /// Delegate method called when the selected material has changed. - /// - /// - public delegate void MaterialChangedHandler(int newMaterialId); - - /// - /// Delegate called when the controller mode changes. - /// - public delegate void ModeChangedHandler(ControllerMode oldMode, ControllerMode newMode); - - /// - /// Delegate called when the block mode changes. - /// - /// Whether block mode is enabled. - public delegate void BlockModeChangedHandler(bool isBlockMode); - - /// - /// Type of overlay to show on the controller's touchpad. - /// - public enum TouchpadOverlay { - NONE, VOLUME_INSERTER, FREEFORM, PAINT, MODIFY, MOVE, DELETE, MENU, UNDO_REDO, RESET_ZOOM, RESIZE - }; - - public enum TouchpadHoverState { - NONE, UP, DOWN, LEFT, RIGHT, RESIZE_UP, RESIZE_DOWN - } - - /// - /// A 6DOF controller. Must be attached to the controllers at the outset of the app's runtime. - /// Manages controller state and communicates events to clients. - /// - public class PeltzerController : MonoBehaviour { - // The physical controller responsible for input & pose. - public ControllerDevice controller; - public ControllerGeometry controllerGeometry; - - public GameObject steamRiftHolder; - public GameObject oculusRiftHolder; - - // Some tools intelligently choose between a 'click and hold' operation and a 'click to begin, click to end' - // operation based on how long the trigger is held after its initial depression. This is the threshold - // beyond which the user is forced into a 'click and hold' behaviour. This is in seconds. - public static readonly float SINGLE_CLICK_THRESHOLD = 0.175f; +namespace com.google.apps.peltzer.client.model.controller +{ + /// + /// Delegate method for controller clients to implement. + /// + public delegate void PeltzerControllerActionHandler(object sender, ControllerEventArgs args); /// - /// Minimum interval, in seconds, between two successive haptic feedback events of the same type. + /// Delegate method called when the selected material has changed. /// - public const float MIN_HAPTIC_FEEDBACK_INTERVAL = 0.1f; + /// + public delegate void MaterialChangedHandler(int newMaterialId); /// - /// Time when haptic feedback was last triggered (for each feedback type), as given by Time.time. + /// Delegate called when the controller mode changes. /// - private Dictionary lastHapticFeedbackTime = - new Dictionary(); + public delegate void ModeChangedHandler(ControllerMode oldMode, ControllerMode newMode); - private readonly List controllerModes - = new List(Enum.GetValues(typeof(ControllerMode)).Cast()); + /// + /// Delegate called when the block mode changes. + /// + /// Whether block mode is enabled. + public delegate void BlockModeChangedHandler(bool isBlockMode); /// - /// Occasionally, the controller is not set when our app starts. This method - /// will find the controller if it's null, and will return false if the - /// controller is not found. + /// Type of overlay to show on the controller's touchpad. /// - /// A reference to the controller that will be set if it's null. - /// Whether or not the controller was acquired and set. - public static bool AcquireIfNecessary(ref PeltzerController peltzerController) { - if (peltzerController == null) { - peltzerController = FindObjectOfType(); - if (peltzerController == null) { - return false; - } - } - return true; + public enum TouchpadOverlay + { + NONE, VOLUME_INSERTER, FREEFORM, PAINT, MODIFY, MOVE, DELETE, MENU, UNDO_REDO, RESET_ZOOM, RESIZE + }; + + public enum TouchpadHoverState + { + NONE, UP, DOWN, LEFT, RIGHT, RESIZE_UP, RESIZE_DOWN } - public GameObject wandTip; - public TextMesh wandTipLabel; - public HeldMeshes heldMeshes; + /// + /// A 6DOF controller. Must be attached to the controllers at the outset of the app's runtime. + /// Manages controller state and communicates events to clients. + /// + public class PeltzerController : MonoBehaviour + { + // The physical controller responsible for input & pose. + public ControllerDevice controller; + public ControllerGeometry controllerGeometry; + + public GameObject steamRiftHolder; + public GameObject oculusRiftHolder; + + // Some tools intelligently choose between a 'click and hold' operation and a 'click to begin, click to end' + // operation based on how long the trigger is held after its initial depression. This is the threshold + // beyond which the user is forced into a 'click and hold' behaviour. This is in seconds. + public static readonly float SINGLE_CLICK_THRESHOLD = 0.175f; + + /// + /// Minimum interval, in seconds, between two successive haptic feedback events of the same type. + /// + public const float MIN_HAPTIC_FEEDBACK_INTERVAL = 0.1f; + + /// + /// Time when haptic feedback was last triggered (for each feedback type), as given by Time.time. + /// + private Dictionary lastHapticFeedbackTime = + new Dictionary(); + + private readonly List controllerModes + = new List(Enum.GetValues(typeof(ControllerMode)).Cast()); + + /// + /// Occasionally, the controller is not set when our app starts. This method + /// will find the controller if it's null, and will return false if the + /// controller is not found. + /// + /// A reference to the controller that will be set if it's null. + /// Whether or not the controller was acquired and set. + public static bool AcquireIfNecessary(ref PeltzerController peltzerController) + { + if (peltzerController == null) + { + peltzerController = FindObjectOfType(); + if (peltzerController == null) + { + return false; + } + } + return true; + } - // Touchpad Textures - public Material touchpadUpMaterial; - public Material touchpadDownMaterial; - public Material touchpadLeftMaterial; - public Material touchpadRightMaterial; - private Material touchpadDefaultMaterial; - private readonly Color BASE_COLOR = new Color(0.9927992f, 1f, 0.4779411f); // Yellowish highlight. + public GameObject wandTip; + public TextMesh wandTipLabel; + public HeldMeshes heldMeshes; + + // Touchpad Textures + public Material touchpadUpMaterial; + public Material touchpadDownMaterial; + public Material touchpadLeftMaterial; + public Material touchpadRightMaterial; + private Material touchpadDefaultMaterial; + private readonly Color BASE_COLOR = new Color(0.9927992f, 1f, 0.4779411f); // Yellowish highlight. + + // Create the WaitForSeconds once to avoid leaking: + // https://forum.unity3d.com/threads/c-coroutine-waitforseconds-garbage-collection-tip.224878/ + private static WaitForSeconds SHORT_WAIT = new WaitForSeconds(0.02f); + + private static readonly float DELAY_UNTIL_EVENT_REPEATS = 0.5f; + private static readonly float DELAY_BETWEEN_REPEATING_EVENTS = 0.2f; + private static readonly float DETACHED_TOOLHEAD_TIME_TO_LIVE = 0.125f; + + /// + /// Distance threshold in metric units for how close hands have to be to 'point' at the palette. + /// + private readonly float PALETTE_DISTANCE_THRESHOLD = 0.33f; + /// + /// The default material (color) for painting/insertion. + /// + public static readonly int DEFAULT_MATERIAL = MaterialRegistry.WHITE_ID; + + private ControllerMode previousMode; + public SelectableMenuItem currentSelectableMenuItem; + public TouchpadOverlay currentOverlay; + private Overlay overlay; + private AudioLibrary audioLibrary; + private WorldSpace worldSpace; + private MeshRepresentationCache meshRepresentationCache; + + private Dictionary tooltips; + + private int _currentMaterial = DEFAULT_MATERIAL; + + /// + /// Red color for the active state of the app menu button. + /// + private readonly Color ACTIVE_BUTTON_COLOR = new Color(244f / 255f, 67f / 255f, 54f / 255f, 1f); + /// + /// Grey color for the inactive state of the app menu button. + /// + private readonly Color INACTIVE_BUTTON_COLOR = new Color(114f / 255f, 115f / 255f, 118f / 255f, 1f); + /// + /// Almost white color for the waiting state of the app menu button. + /// + private readonly Color WAITING_BUTTON_COLOR = new Color(0.99f, 0.99f, 0.99f, 1f); + + private RaycastHit menuHit; + private Transform defaultTipPointer; + private Vector3 defaultTipPointerDefaultLocation; + public bool isPointingAtMenu = false; + private GameObject currentHoveredObject; + private float currentHoveredMenuItemStartTime; + private Vector3 currentHoveredMenuItemDefaultPos; + private Vector3 currentHoveredMenuItemDefaultScale; + private readonly float MENU_HOVER_ANIMATION_SPEED = .004f; + private readonly float BUTTON_HOVER_ANIMATION_SPEED = 0.150f; + + public static readonly Color MENU_BUTTON_LIGHT = new Color(97f / 255f, 97f / 255f, 97f / 255f); + public static readonly Color MENU_BUTTON_DARK = new Color(51f / 255f, 51f / 255f, 51f / 255f); + public static readonly Color MENU_BUTTON_GREEN = new Color(76f / 255f, 175f / 255f, 80f / 255f); + public static readonly Color MENU_BUTTON_RED = new Color(244f / 255f, 67f / 255f, 54f / 255f); + private bool menuIsInDefaultState; + + /// + /// Local position of the wand tip / selector when using RIFT. + /// + Vector3 WAND_TIP_POSITION_RIFT = new Vector3(0.0073f, -.0668f, .0022f); + Vector3 WAND_TIP_ROTATION_OFFSET_RIFT = new Vector3(-45, 0, 0); + + /// + /// Local position of the wand tip / selector when using RIFT on OCULUS. + /// + Vector3 WAND_TIP_POSITION_OCULUS = new Vector3(0, -.001f, -.079f); + Vector3 WAND_TIP_ROTATION_OFFSET_OCULUS = new Vector3(-82.238f, 7.095f, -1.992f); + + /// + /// Local position for overlay icon - default - VIVE. + /// + Vector3 LEFT_OVERLAY_ICON_DEFAULT_POSITION_VIVE = new Vector3(-2.5f, 0f, 0f); + Vector3 RIGHT_OVERLAY_ICON_DEFAULT_POSITION_VIVE = new Vector3(2.5f, 0f, 0f); + Vector3 UP_OVERLAY_ICON_DEFAULT_POSITION_VIVE = new Vector3(0f, 2.5f, 0f); + Vector3 DOWN_OVERLAY_ICON_DEFAULT_POSITION_VIVE = new Vector3(0f, -2.5f, 0f); + + /// + /// Local position for overlay icon - hover - VIVE. + /// + Vector3 LEFT_OVERLAY_ICON_HOVER_POSITION = new Vector3(-2.5f, 0f, -0.6f); + Vector3 RIGHT_OVERLAY_ICON_HOVER_POSITION = new Vector3(2.5f, 0f, -0.6f); + Vector3 UP_OVERLAY_ICON_HOVER_POSITION = new Vector3(0f, 2.5f, -0.6f); + Vector3 DOWN_OVERLAY_ICON_HOVER_POSITION = new Vector3(0f, -2.5f, -0.6f); + + public PeltzerController() + { + PeltzerControllerActionHandler += ControllerEventHandler; + } - // Create the WaitForSeconds once to avoid leaking: - // https://forum.unity3d.com/threads/c-coroutine-waitforseconds-garbage-collection-tip.224878/ - private static WaitForSeconds SHORT_WAIT = new WaitForSeconds(0.02f); + /// + /// What mode the controller is currently in. + /// + public ControllerMode mode { get; set; } + + /// + /// Whether block mode is enabled. + /// + public bool isBlockMode { get; private set; } + + /// + /// Last selected paint mode. So when we use teh dropper to pick a color, we can come back to this mode. + /// + public ControllerMode lastSelectedPaintMode { get; set; } + + /// + /// Get or set the current material by id. + /// + public int currentMaterial + { + get { return _currentMaterial; } + set { _currentMaterial = value; if (MaterialChangedHandler != null) { MaterialChangedHandler(currentMaterial); } } + } - private static readonly float DELAY_UNTIL_EVENT_REPEATS = 0.5f; - private static readonly float DELAY_BETWEEN_REPEATING_EVENTS = 0.2f; - private static readonly float DETACHED_TOOLHEAD_TIME_TO_LIVE = 0.125f; + /// + /// Clients must register themselves on this handler. + /// + public event PeltzerControllerActionHandler PeltzerControllerActionHandler; + + /// + /// Clients that care about material selection should register themselves + /// + public event MaterialChangedHandler MaterialChangedHandler; + + /// + /// Register for mode change events. + /// + public event ModeChangedHandler ModeChangedHandler; + + /// + /// Register for block mode change events. + /// + public event BlockModeChangedHandler BlockModeChangedHandler; + + /// + /// Library to generate haptic feedback. + /// + public HapticFeedback haptics { get; set; } + + /// + /// Represents the time when haptic feedback ceases. Continually spam + /// haptic feedback until this time. This is done because SteamVR_Controller + /// doesn't seem to respond well to haptic pulses over 1000ms. + /// + private float hapticFeedbackUntilTime = 0f; + + private Vector3 lastPositionModel; + private Quaternion lastRotationWorld = Quaternion.identity; + + private List buttons = new List { + ButtonId.Trigger, + ButtonId.Grip, + ButtonId.Touchpad, + ButtonId.ApplicationMenu, + ButtonId.SecondaryButton + }; - /// - /// Distance threshold in metric units for how close hands have to be to 'point' at the palette. - /// - private readonly float PALETTE_DISTANCE_THRESHOLD = 0.33f; - /// - /// The default material (color) for painting/insertion. - /// - public static readonly int DEFAULT_MATERIAL = MaterialRegistry.WHITE_ID; + private float? touchpadPressDownTime; + private bool touchpadRepeating; + private bool touchpadTouched = false; + private float lastTouchpadRepeatTime; + private ControllerEventArgs eventToRepeat; + + public GameObject attachedToolHead; + private GameObject blockModeButton; + private GameObject saveMenuBtn; + private GameObject saveSubMenu; + private GameObject tutorialSubMenu; + private GameObject grabToolOnPalette; + + private VolumeInserter volumeInserterInstance; + private Freeform freeformInstance; + + public Handedness handedness = Handedness.LEFT; + + /// + /// The shapes menu is the floating tray that shows the available primitives for insertion. + /// The menu is anchored to the controller (so it moves around with it). This behavior + /// (added at runtime) contains the logic that controls how it displays and updates. + /// + public ShapesMenu shapesMenu { get; private set; } + + private bool setupDone; + private bool triggerIsDown; + + /// + /// Performs one-time setup. This must be called before anything else. + /// + public void Setup(VolumeInserter volumeInserter, Freeform freeform) + { + if (Config.Instance.sdkMode == SdkMode.SteamVR) + { +#if STEAMVRBUILD + controller = new ControllerDeviceSteam(transform); +#endif + } + else + { + ControllerDeviceOculus oculusController = new ControllerDeviceOculus(transform); + oculusController.controllerType = OVRInput.Controller.RTouch; + controller = oculusController; + } + controllerGeometry.baseControllerAnimation.SetControllerDevice(controller); - private ControllerMode previousMode; - public SelectableMenuItem currentSelectableMenuItem; - public TouchpadOverlay currentOverlay; - private Overlay overlay; - private AudioLibrary audioLibrary; - private WorldSpace worldSpace; - private MeshRepresentationCache meshRepresentationCache; + audioLibrary = FindObjectOfType(); + haptics = GetComponent(); + lastSelectedPaintMode = ControllerMode.paintMesh; + worldSpace = PeltzerMain.Instance.worldSpace; + Transform wandTipXform = gameObject.transform.Find("UI-Tool/TipHead/Sphere"); + wandTip = wandTipXform != null ? wandTipXform.gameObject : gameObject; // Fall back to controller obj. + + if (Config.Instance.VrHardware == VrHardware.Rift) + { + // Adjust the placement of the selector position for Rift. + if (Config.Instance.sdkMode == SdkMode.SteamVR) + { + wandTip.transform.parent.transform.localPosition = WAND_TIP_POSITION_RIFT; + } + else // Oculus SDK + { + wandTip.transform.parent.transform.localPosition = WAND_TIP_POSITION_OCULUS; + } + } + wandTipLabel = gameObject.transform.Find("UI-Tool/TipHead/Label/txt").GetComponent(); + blockModeButton = transform.parent.Find("Controller (Palette)/ID_PanelTools/ToolSide/Actions/Blockmode").gameObject; + saveMenuBtn = transform.parent.Find("Controller (Palette)/ID_PanelTools/ToolSide/Actions/Save").gameObject; + saveSubMenu = transform.parent.Find("Controller (Palette)/ID_PanelTools/ToolSide/Menu-Save").gameObject; + tutorialSubMenu = transform.parent.Find("Controller (Palette)/ID_PanelTools/ToolSide/Menu-Tutorial").gameObject; + grabToolOnPalette = ObjectFinder.ObjectById("ID_ToolGrab"); + + // Grip tooltips. + controllerGeometry.zoomLeftTooltip.SetActive(false); + controllerGeometry.zoomRightTooltip.SetActive(false); + controllerGeometry.moveLeftTooltip.SetActive(false); + controllerGeometry.moveRightTooltip.SetActive(false); + controllerGeometry.groupLeftTooltip.SetActive(false); + controllerGeometry.groupRightTooltip.SetActive(false); + controllerGeometry.ungroupLeftTooltip.SetActive(false); + controllerGeometry.ungroupRightTooltip.SetActive(false); + + defaultTipPointer = transform.Find("UI-Tool/TipHead"); + defaultTipPointerDefaultLocation = new Vector3(defaultTipPointer.localPosition.x, + defaultTipPointer.localPosition.y, defaultTipPointer.localPosition.z); + + touchpadDefaultMaterial = controllerGeometry.touchpad.GetComponent().material; + + volumeInserterInstance = volumeInserter; + freeformInstance = freeform; + + ResetTouchpadOverlay(); + ShowTooltips(); + + shapesMenu = gameObject.AddComponent(); + shapesMenu.Setup(worldSpace, wandTip, _currentMaterial, meshRepresentationCache); + + // Put everything in the default handedness position. + ControllerHandednessChanged(); + + // This is a hack: we need the mode to be different to insertVolume to begin with such that the + // toolhead-changed logic is triggered and the toolhead is set up appropriately. + mode = ControllerMode.move; + ChangeMode(ControllerMode.insertVolume, ObjectFinder.ObjectById("ID_ToolShapes")); + + setupDone = true; + } - private Dictionary tooltips; + /// + /// Cache the GameObjects for tooltips to be shown over this controller. + /// + public void CacheTooltips() + { + tooltips = new Dictionary(); + tooltips.Add(ControllerMode.insertVolume, controllerGeometry.shapeTooltips); + tooltips.Add(ControllerMode.insertStroke, controllerGeometry.freeformTooltips); + tooltips.Add(ControllerMode.extrude, controllerGeometry.modifyTooltips); + tooltips.Add(ControllerMode.reshape, controllerGeometry.modifyTooltips); + tooltips.Add(ControllerMode.subdivideMesh, controllerGeometry.modifyTooltips); + tooltips.Add(ControllerMode.subdivideFace, controllerGeometry.modifyTooltips); + tooltips.Add(ControllerMode.paintFace, controllerGeometry.paintTooltips); + tooltips.Add(ControllerMode.paintMesh, controllerGeometry.paintTooltips); + tooltips.Add(ControllerMode.move, controllerGeometry.grabTooltips); + // Currently no tooltips for delete mode. + } - private int _currentMaterial = DEFAULT_MATERIAL; + public void SetDefaultMode() + { + // Invoke a change mode to our default tool (volume inserter, also known as "shape tool") so that our + // animation and menu interaction states, and analytics, are correct and in sync. This seemed to be + // the least invasive code to support that. + ChangeMode(ControllerMode.insertVolume, ObjectFinder.ObjectById("ID_ToolShapes")); + } - /// - /// Red color for the active state of the app menu button. - /// - private readonly Color ACTIVE_BUTTON_COLOR = new Color(244f / 255f, 67f / 255f, 54f / 255f, 1f); - /// - /// Grey color for the inactive state of the app menu button. - /// - private readonly Color INACTIVE_BUTTON_COLOR = new Color(114f / 255f, 115f / 255f, 118f / 255f, 1f); - /// - /// Almost white color for the waiting state of the app menu button. - /// - private readonly Color WAITING_BUTTON_COLOR = new Color(0.99f, 0.99f, 0.99f, 1f); - - private RaycastHit menuHit; - private Transform defaultTipPointer; - private Vector3 defaultTipPointerDefaultLocation; - public bool isPointingAtMenu = false; - private GameObject currentHoveredObject; - private float currentHoveredMenuItemStartTime; - private Vector3 currentHoveredMenuItemDefaultPos; - private Vector3 currentHoveredMenuItemDefaultScale; - private readonly float MENU_HOVER_ANIMATION_SPEED = .004f; - private readonly float BUTTON_HOVER_ANIMATION_SPEED = 0.150f; - - public static readonly Color MENU_BUTTON_LIGHT = new Color(97f / 255f, 97f / 255f, 97f / 255f); - public static readonly Color MENU_BUTTON_DARK = new Color(51f / 255f, 51f / 255f, 51f / 255f); - public static readonly Color MENU_BUTTON_GREEN = new Color(76f / 255f, 175f / 255f, 80f / 255f); - public static readonly Color MENU_BUTTON_RED = new Color(244f / 255f, 67f / 255f, 54f / 255f); - private bool menuIsInDefaultState; + void Update() + { + if (!setupDone) return; - /// - /// Local position of the wand tip / selector when using RIFT. - /// - Vector3 WAND_TIP_POSITION_RIFT = new Vector3(0.0073f, -.0668f, .0022f); - Vector3 WAND_TIP_ROTATION_OFFSET_RIFT = new Vector3(-45, 0, 0); + // First, update the ControllerDevice that can actually tell us the state of the physical controller. + controller.Update(); - /// - /// Local position of the wand tip / selector when using RIFT on OCULUS. - /// - Vector3 WAND_TIP_POSITION_OCULUS = new Vector3(0, -.001f, -.079f); - Vector3 WAND_TIP_ROTATION_OFFSET_OCULUS = new Vector3(-82.238f, 7.095f, -1.992f); + if (!PeltzerMain.Instance.restrictionManager.tooltipsAllowed) + { + HideTooltips(); + } - /// - /// Local position for overlay icon - default - VIVE. - /// - Vector3 LEFT_OVERLAY_ICON_DEFAULT_POSITION_VIVE = new Vector3(-2.5f, 0f, 0f); - Vector3 RIGHT_OVERLAY_ICON_DEFAULT_POSITION_VIVE = new Vector3(2.5f, 0f, 0f); - Vector3 UP_OVERLAY_ICON_DEFAULT_POSITION_VIVE = new Vector3(0f, 2.5f, 0f); - Vector3 DOWN_OVERLAY_ICON_DEFAULT_POSITION_VIVE = new Vector3(0f, -2.5f, 0f); + // Then find out if the user is pointing at the menu and update accordingly. + UpdateMenuItemPoint(); - /// - /// Local position for overlay icon - hover - VIVE. - /// - Vector3 LEFT_OVERLAY_ICON_HOVER_POSITION = new Vector3(-2.5f, 0f, -0.6f); - Vector3 RIGHT_OVERLAY_ICON_HOVER_POSITION = new Vector3(2.5f, 0f, -0.6f); - Vector3 UP_OVERLAY_ICON_HOVER_POSITION = new Vector3(0f, 2.5f, -0.6f); - Vector3 DOWN_OVERLAY_ICON_HOVER_POSITION = new Vector3(0f, -2.5f, -0.6f); + if (controller.IsTrackedObjectValid) + { + // Update some variables. + haptics.controller = controller; - public PeltzerController() { - PeltzerControllerActionHandler += ControllerEventHandler; - } + lastPositionModel = worldSpace.WorldToModel(wandTip.transform.position); + if (Config.Instance.VrHardware == VrHardware.Vive) + { + lastRotationWorld = transform.rotation; + } + else + { + if (Config.Instance.sdkMode == SdkMode.SteamVR) + { + wandTip.transform.localRotation = Quaternion.Euler(WAND_TIP_ROTATION_OFFSET_RIFT); + } + else + { + wandTip.transform.localRotation = Quaternion.Euler(WAND_TIP_ROTATION_OFFSET_OCULUS); + } + lastRotationWorld = wandTip.transform.rotation; + } - /// - /// What mode the controller is currently in. - /// - public ControllerMode mode { get; set; } + // If we have no listeners (which would be very strange) then abort early. + if (PeltzerControllerActionHandler == null) + { + return; + } - /// - /// Whether block mode is enabled. - /// - public bool isBlockMode { get; private set; } + // Update the tooltips and process button events. + if (PeltzerMain.Instance.introChoreographer.introIsComplete) + { + SetGripTooltip(); + ProcessButtonEvents(); + } + } + } - /// - /// Last selected paint mode. So when we use teh dropper to pick a color, we can come back to this mode. - /// - public ControllerMode lastSelectedPaintMode { get; set; } + private void UpdateMenuItemPoint() + { + bool isTriggerDown = controller.IsPressed(ButtonId.Trigger); + bool isOperationInProgress = freeformInstance.IsStroking() || + PeltzerMain.Instance.GetMover().IsMoving() || + PeltzerMain.Instance.GetReshaper().IsReshaping() || + PeltzerMain.Instance.GetExtruder().IsExtrudingFace(); + + if (isOperationInProgress) + { + // If an operation is in progress, the hover state needs to be reset, otherwise it might get stuck + // if the user starts an operation while a hover state is active (bug). + // It's cheap to call these methods every frame because they exit early if there is no work to be done. + ResetUnhoveredItem(); + ResetMenu(); + return; + } - /// - /// Get or set the current material by id. - /// - public int currentMaterial { - get { return _currentMaterial; } - set { _currentMaterial = value; if (MaterialChangedHandler != null) { MaterialChangedHandler(currentMaterial); } } - } + // Only update the hover state if the trigger is NOT down (if the trigger is down, don't change states because + // the user is currently in the process of trying to click something that's already hovered). + if (!isTriggerDown) + { + HandleMenuItemPoint(); + } + } - /// - /// Clients must register themselves on this handler. - /// - public event PeltzerControllerActionHandler PeltzerControllerActionHandler; + // Process user interaction. + private void ProcessButtonEvents() + { + TouchpadLocation location = controller.GetTouchpadLocation(); + + foreach (ButtonId buttonId in buttons) + { + if (buttonId == ButtonId.Trigger && currentSelectableMenuItem != null) + { + // If the user has pulled the trigger while pointing at a palette menu item, invoke the action handler. + if (controller.WasJustPressed(buttonId)) + { + currentSelectableMenuItem.ApplyMenuOptions(PeltzerMain.Instance); + TriggerHapticFeedback(); + // Swallow this event, so the other tools don't fire. + continue; + } + } + else if (buttonId == ButtonId.Trigger) + { + // Else, send out regular trigger events. + DetectTrigger(controller); + } - /// - /// Clients that care about material selection should register themselves - /// - public event MaterialChangedHandler MaterialChangedHandler; + // If the user has pressed any button other than the trigger, send out the event. + if (controller.WasJustPressed(buttonId) && buttonId != ButtonId.Trigger) + { + if (buttonId == ButtonId.Touchpad) + { + + } + ControllerEventArgs eventArgs = new ControllerEventArgs(ControllerType.PELTZER, buttonId, + ButtonAction.DOWN, buttonId == ButtonId.Touchpad ? + location : TouchpadLocation.NONE, currentOverlay); + + PeltzerControllerActionHandler(this, eventArgs); + + // If this was a touchpad press-down, then queue up this event to repeat in the case that the touchpad + // is not released. + if (buttonId == ButtonId.Touchpad && location != TouchpadLocation.CENTER) + { + touchpadPressDownTime = Time.time; + eventToRepeat = eventArgs; + } + } - /// - /// Register for mode change events. - /// - public event ModeChangedHandler ModeChangedHandler; + // If the user has released any button other than the trigger, send out the event. + if (controller.WasJustReleased(buttonId) && buttonId != ButtonId.Trigger) + { + PeltzerControllerActionHandler(this, new ControllerEventArgs(ControllerType.PELTZER, buttonId, + ButtonAction.UP, buttonId == ButtonId.Touchpad ? + location : TouchpadLocation.NONE, currentOverlay)); + + // If this was a touchpad release, then stop repeating the 'touchpad press-down' event. + if (buttonId == ButtonId.Touchpad) + { + touchpadPressDownTime = null; + touchpadRepeating = false; + } + } - /// - /// Register for block mode change events. - /// - public event BlockModeChangedHandler BlockModeChangedHandler; + if (buttonId == ButtonId.Touchpad) + { + if (controller.IsTouched(buttonId)) + { + touchpadTouched = true; + // If the touchpad is currently touched, and the touch isn't at 0,0 (which normally indicates + // spurious input), send the touch event. This can result in an event sent every frame when + // a user keeps their thumb on the touchpad. + PeltzerControllerActionHandler(this, new ControllerEventArgs(ControllerType.PELTZER, + ButtonId.Touchpad, ButtonAction.TOUCHPAD, location, currentOverlay)); + } + else if (touchpadTouched) + { + // If the user stops touching the touchpad, sent out a 'cancel' event. + PeltzerControllerActionHandler(this, new ControllerEventArgs(ControllerType.PELTZER, buttonId, + ButtonAction.NONE, TouchpadLocation.NONE, currentOverlay)); + touchpadTouched = false; + } + + // If the touchpad is held down, and we are past the delay for repeating this event, + // send out the event again. + if (touchpadPressDownTime.HasValue + && !touchpadRepeating && (Time.time - touchpadPressDownTime.Value) > DELAY_UNTIL_EVENT_REPEATS) + { + touchpadRepeating = true; + PeltzerControllerActionHandler(this, eventToRepeat); + lastTouchpadRepeatTime = Time.time; + } + else if (touchpadRepeating && (Time.time - lastTouchpadRepeatTime) > DELAY_BETWEEN_REPEATING_EVENTS) + { + PeltzerControllerActionHandler(this, eventToRepeat); + lastTouchpadRepeatTime = Time.time; + } + } + } + } - /// - /// Library to generate haptic feedback. - /// - public HapticFeedback haptics { get; set; } + /// + /// Make the app menu button red or grey depending on whether or not its active. + /// + /// + public void SetApplicationButtonOverlay(ButtonMode buttonMode) + { + if (controllerGeometry.appMenuButton == null) return; + + Material material = controllerGeometry.appMenuButton.GetComponent().material; + Color highlightEmmissionColor; + Color highlightColor; + if (buttonMode == ButtonMode.ACTIVE) + { + highlightEmmissionColor = ACTIVE_BUTTON_COLOR * Mathf.LinearToGammaSpace(0.6f); // Set emission to 60%. + highlightColor = ACTIVE_BUTTON_COLOR; + } + else if (buttonMode == ButtonMode.WAITING) + { + highlightEmmissionColor = WAITING_BUTTON_COLOR * Mathf.LinearToGammaSpace(0f); + highlightColor = WAITING_BUTTON_COLOR; + } + else + { + highlightEmmissionColor = INACTIVE_BUTTON_COLOR * Mathf.LinearToGammaSpace(0f); + highlightColor = INACTIVE_BUTTON_COLOR; + } - /// - /// Represents the time when haptic feedback ceases. Continually spam - /// haptic feedback until this time. This is done because SteamVR_Controller - /// doesn't seem to respond well to haptic pulses over 1000ms. - /// - private float hapticFeedbackUntilTime = 0f; + material.SetColor("_EmissionColor", highlightEmmissionColor); + material.color = highlightColor; + } - private Vector3 lastPositionModel; - private Quaternion lastRotationWorld = Quaternion.identity; + /// + /// Make the secondary button red or grey depending on whether or not its active. + /// + /// + public void SetSecondaryButtonOverlay(bool active) + { + if (controllerGeometry.secondaryButton == null) return; + + Material material = controllerGeometry.secondaryButton.GetComponent().material; + Color highlightEmmissionColor; + Color highlightColor; + if (active) + { + highlightEmmissionColor = ACTIVE_BUTTON_COLOR * Mathf.LinearToGammaSpace(0.6f); // Set emission to 60%. + highlightColor = ACTIVE_BUTTON_COLOR; + } + else + { + highlightEmmissionColor = INACTIVE_BUTTON_COLOR * Mathf.LinearToGammaSpace(0f); + highlightColor = INACTIVE_BUTTON_COLOR; + } - private List buttons = new List { - ButtonId.Trigger, - ButtonId.Grip, - ButtonId.Touchpad, - ButtonId.ApplicationMenu, - ButtonId.SecondaryButton - }; + material.SetColor("_EmissionColor", highlightEmmissionColor); + material.color = highlightColor; + } - private float? touchpadPressDownTime; - private bool touchpadRepeating; - private bool touchpadTouched = false; - private float lastTouchpadRepeatTime; - private ControllerEventArgs eventToRepeat; + /// + /// Casts ray to detect user pointing at the palette menu with this GameObject. (Not to be confused + /// with item selection.) If so, the cursor is bound to the surface of the hit and associated menu interaction + /// UX is coordinated such as haptic feedback, and view alterations such as + /// showing or hiding aspects of the controllers. + /// + private void HandleMenuItemPoint() + { + // Cast a ray from the controller and see if it hits the menu. + Vector3 controllerRayVector; + Vector3 controllerRayOrigin; + if (Config.Instance.VrHardware == VrHardware.Rift) + { + if (Config.Instance.sdkMode == SdkMode.SteamVR) + { + controllerRayVector = Quaternion.Euler(45, 0, 0) * Vector3.forward; + controllerRayOrigin = transform.position + new Vector3(0, -0.045f, 0); + } + else + { + controllerRayVector = Vector3.forward; + controllerRayOrigin = transform.position;// + new Vector3(0, -0.045f, 0); + } + } + else + { + controllerRayVector = Vector3.forward; + controllerRayOrigin = transform.position; + } + if (!Physics.Raycast(controllerRayOrigin, transform.TransformDirection(controllerRayVector), + out menuHit, PALETTE_DISTANCE_THRESHOLD)) + { + // We didn't hit the menu with the ray, reset any existing state and exit early. + if (mode == ControllerMode.reshape) + { + PeltzerMain.Instance.GetSelector().TurnOnSelectIndicator(); + } + ResetUnhoveredItem(); + ResetMenu(); + return; + } - public GameObject attachedToolHead; - private GameObject blockModeButton; - private GameObject saveMenuBtn; - private GameObject saveSubMenu; - private GameObject tutorialSubMenu; - private GameObject grabToolOnPalette; + // Since we are pointing at the menu, be sure to hide the selector so we don't have any overlap. + PeltzerMain.Instance.GetSelector().TurnOffSelectIndicator(); + // If a button is inactive or we're hovering over empty palette space, we want to show the + // pointer and take no further action. + SelectableMenuItem selectableMenuItem = menuHit.transform.GetComponent(); + bool hitIsActive = true; + if (selectableMenuItem != null) + { + hitIsActive = selectableMenuItem.isActive; + } - private VolumeInserter volumeInserterInstance; - private Freeform freeformInstance; + // Check for the various types of things we could be hovering. + ChangeMaterialMenuItem changeMaterialMenuItem = menuHit.transform.GetComponent(); + bool hoveringColor = changeMaterialMenuItem != null; + ChangeModeMenuItem changeModeMenuItem = menuHit.transform.GetComponent(); + bool hoveringToolhead = changeModeMenuItem != null; + MenuActionItem menuActionItem = menuHit.transform.GetComponent(); + bool hoveringButton = menuActionItem != null; + SelectZandriaCreationMenuItem zandriaCreationMenuItem = menuHit.transform.GetComponent(); + bool hoveringZandriaCreation = zandriaCreationMenuItem != null; + PolyMenuButton polyMenuButton = menuHit.transform.GetComponent(); + bool hoveringPolyMenuOption = polyMenuButton != null; + SelectableDetailsMenuItem selectableDetailsMenuItem = menuHit.transform.GetComponent(); + bool hoveringZandriaDetails = selectableDetailsMenuItem != null; + if (hoveringZandriaDetails) + { + hitIsActive &= selectableDetailsMenuItem.creation != null + && selectableDetailsMenuItem.creation.entry != null + && selectableDetailsMenuItem.creation.entry.loadStatus != ZandriaCreationsManager.LoadStatus.FAILED; + } + EmptyMenuItem emptyMenuItem = menuHit.transform.GetComponent(); + bool isHoveringEmptyPaletteSpace = emptyMenuItem != null; + EnvironmentMenuItem environmentMenuItem = menuHit.transform.GetComponent(); + bool isHoveringEnvironmentMenuItem = environmentMenuItem != null; + + // Reset hovered items if we are not hovering anything. + if (!(hoveringColor || hoveringToolhead || hoveringButton || hoveringZandriaCreation || hoveringPolyMenuOption + || hoveringZandriaDetails || isHoveringEmptyPaletteSpace || isHoveringEnvironmentMenuItem)) + { + ResetUnhoveredItem(); + ResetMenu(); + return; + } - public Handedness handedness = Handedness.LEFT; + // At this point, we know something on the menu has been hit, so we update variables accordingly and + // hide the shapes menu. + menuIsInDefaultState = false; + isPointingAtMenu = true; + shapesMenu.Hide(); + + // We set the position of the pointer dot to be just in front of the item being pointed at. + defaultTipPointer.transform.gameObject.SetActive(true); + if (hoveringToolhead) + { + defaultTipPointer.position = menuHit.point + menuHit.transform.up * .015f; + } + else + { + defaultTipPointer.position = menuHit.point; + } - /// - /// The shapes menu is the floating tray that shows the available primitives for insertion. - /// The menu is anchored to the controller (so it moves around with it). This behavior - /// (added at runtime) contains the logic that controls how it displays and updates. - /// - public ShapesMenu shapesMenu { get; private set; } + if (!hitIsActive || isHoveringEmptyPaletteSpace) + { + ResetUnhoveredItem(); + currentHoveredObject = null; + currentSelectableMenuItem = null; + wandTipLabel.text = ""; + ResetTutorialMenu(); + return; + } - private bool setupDone; - private bool triggerIsDown; + // We set a label on the pointer dot to show what is being pointed at. + if (selectableMenuItem != null && hitIsActive) + { + wandTipLabel.text = selectableMenuItem.hoverName; + wandTipLabel.transform.parent.transform.LookAt(PeltzerMain.Instance.hmd.transform); + } + else + { + wandTipLabel.text = ""; + } - /// - /// Performs one-time setup. This must be called before anything else. - /// - public void Setup(VolumeInserter volumeInserter, Freeform freeform) { - if (Config.Instance.sdkMode == SdkMode.SteamVR) { -#if STEAMVRBUILD - controller = new ControllerDeviceSteam(transform); -#endif - } else { - ControllerDeviceOculus oculusController = new ControllerDeviceOculus(transform); - oculusController.controllerType = OVRInput.Controller.RTouch; - controller = oculusController; - } - controllerGeometry.baseControllerAnimation.SetControllerDevice(controller); - - audioLibrary = FindObjectOfType(); - haptics = GetComponent(); - lastSelectedPaintMode = ControllerMode.paintMesh; - worldSpace = PeltzerMain.Instance.worldSpace; - Transform wandTipXform = gameObject.transform.Find("UI-Tool/TipHead/Sphere"); - wandTip = wandTipXform != null ? wandTipXform.gameObject : gameObject; // Fall back to controller obj. - - if (Config.Instance.VrHardware == VrHardware.Rift) { - // Adjust the placement of the selector position for Rift. - if (Config.Instance.sdkMode == SdkMode.SteamVR) { - wandTip.transform.parent.transform.localPosition = WAND_TIP_POSITION_RIFT; - } else // Oculus SDK - { - wandTip.transform.parent.transform.localPosition = WAND_TIP_POSITION_OCULUS; - } - } - wandTipLabel = gameObject.transform.Find("UI-Tool/TipHead/Label/txt").GetComponent(); - blockModeButton = transform.parent.Find("Controller (Palette)/ID_PanelTools/ToolSide/Actions/Blockmode").gameObject; - saveMenuBtn = transform.parent.Find("Controller (Palette)/ID_PanelTools/ToolSide/Actions/Save").gameObject; - saveSubMenu = transform.parent.Find("Controller (Palette)/ID_PanelTools/ToolSide/Menu-Save").gameObject; - tutorialSubMenu = transform.parent.Find("Controller (Palette)/ID_PanelTools/ToolSide/Menu-Tutorial").gameObject; - grabToolOnPalette = ObjectFinder.ObjectById("ID_ToolGrab"); - - // Grip tooltips. - controllerGeometry.zoomLeftTooltip.SetActive(false); - controllerGeometry.zoomRightTooltip.SetActive(false); - controllerGeometry.moveLeftTooltip.SetActive(false); - controllerGeometry.moveRightTooltip.SetActive(false); - controllerGeometry.groupLeftTooltip.SetActive(false); - controllerGeometry.groupRightTooltip.SetActive(false); - controllerGeometry.ungroupLeftTooltip.SetActive(false); - controllerGeometry.ungroupRightTooltip.SetActive(false); - - defaultTipPointer = transform.Find("UI-Tool/TipHead"); - defaultTipPointerDefaultLocation = new Vector3(defaultTipPointer.localPosition.x, - defaultTipPointer.localPosition.y, defaultTipPointer.localPosition.z); - - touchpadDefaultMaterial = controllerGeometry.touchpad.GetComponent().material; - - volumeInserterInstance = volumeInserter; - freeformInstance = freeform; - - ResetTouchpadOverlay(); - ShowTooltips(); - - shapesMenu = gameObject.AddComponent(); - shapesMenu.Setup(worldSpace, wandTip, _currentMaterial, meshRepresentationCache); - - // Put everything in the default handedness position. - ControllerHandednessChanged(); - - // This is a hack: we need the mode to be different to insertVolume to begin with such that the - // toolhead-changed logic is triggered and the toolhead is set up appropriately. - mode = ControllerMode.move; - ChangeMode(ControllerMode.insertVolume, ObjectFinder.ObjectById("ID_ToolShapes")); - - setupDone = true; - } + // Reset the hover animation and start time if this is a new item or not a save submenu. + if ((currentHoveredObject != menuHit.transform.gameObject) + && (!menuHit.transform.name.Equals("Save-Copy") + && !menuHit.transform.name.Equals("Save-Confirm") + && !menuHit.transform.name.Equals("Save-Selected") + && !menuHit.transform.name.Equals("Publish") + && !menuHit.transform.name.Equals("Intro") + && !menuHit.transform.name.Equals("SelectingMoving") + && !menuHit.transform.name.Equals("ModifyingModels") + && !menuHit.transform.name.Equals("SnappingAlignment"))) + { + currentHoveredMenuItemStartTime = Time.time; + currentHoveredMenuItemDefaultPos = menuHit.transform.localPosition; + currentHoveredMenuItemDefaultScale = menuHit.transform.localScale; + ResetSaveMenu(); + } - /// - /// Cache the GameObjects for tooltips to be shown over this controller. - /// - public void CacheTooltips() { - tooltips = new Dictionary(); - tooltips.Add(ControllerMode.insertVolume, controllerGeometry.shapeTooltips); - tooltips.Add(ControllerMode.insertStroke, controllerGeometry.freeformTooltips); - tooltips.Add(ControllerMode.extrude, controllerGeometry.modifyTooltips); - tooltips.Add(ControllerMode.reshape, controllerGeometry.modifyTooltips); - tooltips.Add(ControllerMode.subdivideMesh, controllerGeometry.modifyTooltips); - tooltips.Add(ControllerMode.subdivideFace, controllerGeometry.modifyTooltips); - tooltips.Add(ControllerMode.paintFace, controllerGeometry.paintTooltips); - tooltips.Add(ControllerMode.paintMesh, controllerGeometry.paintTooltips); - tooltips.Add(ControllerMode.move, controllerGeometry.grabTooltips); - // Currently no tooltips for delete mode. - } + // Begin, or continue any animations. + if (hoveringButton) + { + float delta = (Time.time - currentHoveredMenuItemStartTime); + float pct = (delta / BUTTON_HOVER_ANIMATION_SPEED); + float targetYPos; + float targetYScale; + if (pct > 1f) + { + pct = 1f; + // Show sub-menu if available at the end of the animation. + if (menuHit.transform.name.Equals("Save")) + { + if (PeltzerMain.Instance.model.GetNumberOfMeshes() > 0) + { + saveSubMenu.SetActive(true); + } + } + else if (menuHit.transform.name.Equals("Tutorial")) + { + tutorialSubMenu.SetActive(true); + } + } + if (!menuHit.transform.name.Equals("Save-Copy") + && !menuHit.transform.name.Equals("Save-Confirm") + && !menuHit.transform.name.Equals("Save-Selected") + && !menuHit.transform.name.Equals("Publish")) + { + targetYPos = pct * 0.00250f; + targetYScale = pct * (0.01f - currentHoveredMenuItemDefaultScale.y); + + menuHit.transform.localPosition = new Vector3(menuHit.transform.localPosition.x, + targetYPos, + menuHit.transform.localPosition.z); + + menuHit.transform.localScale = new Vector3(menuHit.transform.localScale.x, + currentHoveredMenuItemDefaultScale.y + targetYScale, + menuHit.transform.localScale.z); + } + } + else if (hoveringToolhead) + { + // Record the starting collider position so we can move it back to its original position after the toolhead + // is moved. + Vector3 globalColliderStartPosition = + menuHit.transform.TransformPoint(menuHit.transform.GetComponent().center); + + // Hovering over a tool head. + menuHit.transform.localPosition = new Vector3(menuHit.transform.localPosition.x, + (menuHit.transform.localPosition.y + MENU_HOVER_ANIMATION_SPEED) < .02f ? + (menuHit.transform.localPosition.y + .002f) : .02f, + menuHit.transform.localPosition.z); + + // Restore the position of the collider. + menuHit.transform.GetComponent().center = + menuHit.transform.InverseTransformPoint(globalColliderStartPosition); + } + else if (hoveringZandriaDetails) + { + // Hovering over a saved or featured Poly models menu. + GameObject creationPreview = menuHit.transform.Find("CreationPreview").gameObject; + + creationPreview.transform.localPosition = new Vector3(creationPreview.transform.localPosition.x, + creationPreview.transform.localPosition.y, + (creationPreview.transform.localPosition.z) > -.2f ? + (creationPreview.transform.localPosition.z - .02f) : -.2f); + } + else if (isHoveringEnvironmentMenuItem) + { + menuHit.transform.localPosition = new Vector3(menuHit.transform.localPosition.x, + (menuHit.transform.localPosition.y + MENU_HOVER_ANIMATION_SPEED) < 0f ? + (menuHit.transform.localPosition.y + .003f) : 0f, + menuHit.transform.localPosition.z); + } + else if (!hoveringColor && !hoveringPolyMenuOption) + { + // Catch-all for other possible objects to hover. + menuHit.transform.localPosition = new Vector3(menuHit.transform.localPosition.x, + (menuHit.transform.localPosition.y + MENU_HOVER_ANIMATION_SPEED) < .02f ? + (menuHit.transform.localPosition.y + .002f) : .02f, + menuHit.transform.localPosition.z); + } - public void SetDefaultMode() { - // Invoke a change mode to our default tool (volume inserter, also known as "shape tool") so that our - // animation and menu interaction states, and analytics, are correct and in sync. This seemed to be - // the least invasive code to support that. - ChangeMode(ControllerMode.insertVolume, ObjectFinder.ObjectById("ID_ToolShapes")); - } + // If this hover is the same as last frame, nothing new to do. + if (currentHoveredObject == menuHit.transform.gameObject) + { + return; + } - void Update() { - if (!setupDone) return; - - // First, update the ControllerDevice that can actually tell us the state of the physical controller. - controller.Update(); - - if (!PeltzerMain.Instance.restrictionManager.tooltipsAllowed) { - HideTooltips(); - } - - // Then find out if the user is pointing at the menu and update accordingly. - UpdateMenuItemPoint(); - - if (controller.IsTrackedObjectValid) { - // Update some variables. - haptics.controller = controller; - - lastPositionModel = worldSpace.WorldToModel(wandTip.transform.position); - if (Config.Instance.VrHardware == VrHardware.Vive) { - lastRotationWorld = transform.rotation; - } else { - if (Config.Instance.sdkMode == SdkMode.SteamVR) { - wandTip.transform.localRotation = Quaternion.Euler(WAND_TIP_ROTATION_OFFSET_RIFT); - } else { - wandTip.transform.localRotation = Quaternion.Euler(WAND_TIP_ROTATION_OFFSET_OCULUS); - } - lastRotationWorld = wandTip.transform.rotation; - } + // Reset last hovered item if needed, special-casing the 'save' sub-menu. + if (currentHoveredObject != null && !currentHoveredObject.name.Equals("Save")) + { + ResetUnhoveredItem(); + } + else if (currentHoveredObject != null && currentHoveredObject.name.Equals("Save") + && !(menuHit.transform.name.Equals("Save-Copy") + || menuHit.transform.name.Equals("Save-Selected") + || menuHit.transform.name.Equals("Save-Confirm") + || menuHit.transform.name.Equals("Publish"))) + { + ResetUnhoveredItem(); + } + + // Trigger haptic feedback. + TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_1, /* durationSeconds */ 0.01f, /* strength */ 0.15f); - // If we have no listeners (which would be very strange) then abort early. - if (PeltzerControllerActionHandler == null) { - return; + // Animate Color swatches + if (hoveringColor) + { + changeMaterialMenuItem.SetHovered(true); + } + + // Animate Poly Menu buttons + if (hoveringPolyMenuOption) + { + polyMenuButton.SetHovered(true); + } + + // Set the current menu item so that we can use this value on Trigger pull, i.e. mode selection. + currentSelectableMenuItem = menuHit.transform.GetComponent(); + currentHoveredObject = menuHit.transform.gameObject; + + // Set the material / color for a hovered button. + menuActionItem = currentHoveredObject.GetComponent(); + if (menuActionItem != null) + { + if (IsHoveredButtonThatShouldChangeColour(menuActionItem)) + { + menuHit.transform.gameObject.GetComponent().material.color = MENU_BUTTON_LIGHT; + } + } } - // Update the tooltips and process button events. - if (PeltzerMain.Instance.introChoreographer.introIsComplete) { - SetGripTooltip(); - ProcessButtonEvents(); + /// + /// Hide and reset any floating save UI and associated state. + /// + private void ResetSaveMenu() + { + saveSubMenu.SetActive(false); + saveMenuBtn.GetComponent().material.color = MENU_BUTTON_DARK; + saveMenuBtn.transform.localScale = new Vector3(saveMenuBtn.transform.localScale.x, + 0.005f, saveMenuBtn.transform.localScale.z); + saveMenuBtn.transform.localPosition = new Vector3(saveMenuBtn.transform.localPosition.x, + 0f, saveMenuBtn.transform.localPosition.z); } - } - } - private void UpdateMenuItemPoint() { - bool isTriggerDown = controller.IsPressed(ButtonId.Trigger); - bool isOperationInProgress = freeformInstance.IsStroking() || - PeltzerMain.Instance.GetMover().IsMoving() || - PeltzerMain.Instance.GetReshaper().IsReshaping() || - PeltzerMain.Instance.GetExtruder().IsExtrudingFace(); - - if (isOperationInProgress) { - // If an operation is in progress, the hover state needs to be reset, otherwise it might get stuck - // if the user starts an operation while a hover state is active (bug). - // It's cheap to call these methods every frame because they exit early if there is no work to be done. - ResetUnhoveredItem(); - ResetMenu(); - return; - } - - // Only update the hover state if the trigger is NOT down (if the trigger is down, don't change states because - // the user is currently in the process of trying to click something that's already hovered). - if (!isTriggerDown) { - HandleMenuItemPoint(); - } - } - - // Process user interaction. - private void ProcessButtonEvents() { - TouchpadLocation location = controller.GetTouchpadLocation(); - - foreach (ButtonId buttonId in buttons) { - if (buttonId == ButtonId.Trigger && currentSelectableMenuItem != null) { - // If the user has pulled the trigger while pointing at a palette menu item, invoke the action handler. - if (controller.WasJustPressed(buttonId)) { - currentSelectableMenuItem.ApplyMenuOptions(PeltzerMain.Instance); - TriggerHapticFeedback(); - // Swallow this event, so the other tools don't fire. - continue; - } - } else if (buttonId == ButtonId.Trigger) { - // Else, send out regular trigger events. - DetectTrigger(controller); + /// + /// Hide and reset any floating save UI and associated state. + /// + private void ResetTutorialMenu() + { + tutorialSubMenu.SetActive(false); } - // If the user has pressed any button other than the trigger, send out the event. - if (controller.WasJustPressed(buttonId) && buttonId != ButtonId.Trigger) { - if (buttonId == ButtonId.Touchpad) { + /// + /// If nothing on the menu is being pointed at, reset the whole menu state. + /// + public void ResetMenu() + { + if (menuIsInDefaultState) + { + return; + } - } - ControllerEventArgs eventArgs = new ControllerEventArgs(ControllerType.PELTZER, buttonId, - ButtonAction.DOWN, buttonId == ButtonId.Touchpad ? - location : TouchpadLocation.NONE, currentOverlay); + // Hide and reset any floating save UI and associated state. + ResetSaveMenu(); + ResetTutorialMenu(); - PeltzerControllerActionHandler(this, eventArgs); + // Set the "cursor/pointer" back to the default location. + defaultTipPointer.transform.gameObject.SetActive(false); + defaultTipPointer.localPosition = defaultTipPointerDefaultLocation; + lastPositionModel = worldSpace.WorldToModel(wandTip.transform.position); + isPointingAtMenu = false; - // If this was a touchpad press-down, then queue up this event to repeat in the case that the touchpad - // is not released. - if (buttonId == ButtonId.Touchpad && location != TouchpadLocation.CENTER) { - touchpadPressDownTime = Time.time; - eventToRepeat = eventArgs; - } - } + // Reset currentMenuItem and hover + currentSelectableMenuItem = null; + currentHoveredObject = null; - // If the user has released any button other than the trigger, send out the event. - if (controller.WasJustReleased(buttonId) && buttonId != ButtonId.Trigger) { - PeltzerControllerActionHandler(this, new ControllerEventArgs(ControllerType.PELTZER, buttonId, - ButtonAction.UP, buttonId == ButtonId.Touchpad ? - location : TouchpadLocation.NONE, currentOverlay)); - - // If this was a touchpad release, then stop repeating the 'touchpad press-down' event. - if (buttonId == ButtonId.Touchpad) { - touchpadPressDownTime = null; - touchpadRepeating = false; - } + if (attachedToolHead != null) + { + attachedToolHead.SetActive(true); + } + menuIsInDefaultState = true; } - if (buttonId == ButtonId.Touchpad) { - if (controller.IsTouched(buttonId)) { - touchpadTouched = true; - // If the touchpad is currently touched, and the touch isn't at 0,0 (which normally indicates - // spurious input), send the touch event. This can result in an event sent every frame when - // a user keeps their thumb on the touchpad. - PeltzerControllerActionHandler(this, new ControllerEventArgs(ControllerType.PELTZER, - ButtonId.Touchpad, ButtonAction.TOUCHPAD, location, currentOverlay)); - } else if (touchpadTouched) { - // If the user stops touching the touchpad, sent out a 'cancel' event. - PeltzerControllerActionHandler(this, new ControllerEventArgs(ControllerType.PELTZER, buttonId, - ButtonAction.NONE, TouchpadLocation.NONE, currentOverlay)); - touchpadTouched = false; - } - - // If the touchpad is held down, and we are past the delay for repeating this event, - // send out the event again. - if (touchpadPressDownTime.HasValue - && !touchpadRepeating && (Time.time - touchpadPressDownTime.Value) > DELAY_UNTIL_EVENT_REPEATS) { - touchpadRepeating = true; - PeltzerControllerActionHandler(this, eventToRepeat); - lastTouchpadRepeatTime = Time.time; - } else if (touchpadRepeating && (Time.time - lastTouchpadRepeatTime) > DELAY_BETWEEN_REPEATING_EVENTS) { - PeltzerControllerActionHandler(this, eventToRepeat); - lastTouchpadRepeatTime = Time.time; - } + /// + /// True if the given item is a button that should change color when hovered. + /// + private bool IsHoveredButtonThatShouldChangeColour(MenuActionItem menuActionItem) + { + return menuActionItem.action == MenuAction.CLEAR + || menuActionItem.action == MenuAction.SAVE + || menuActionItem.action == MenuAction.CANCEL_SAVE + || menuActionItem.action == MenuAction.NOTHING + || menuActionItem.action == MenuAction.SHOW_SAVE_CONFIRM + || menuActionItem.action == MenuAction.SAVE_COPY + || menuActionItem.action == MenuAction.SAVE_SELECTED + || menuActionItem.action == MenuAction.PUBLISH + || menuActionItem.action == MenuAction.NEW_WITH_SAVE + || (menuActionItem.action == MenuAction.TUTORIAL_PROMPT && !( + PeltzerMain.Instance.paletteController.tutorialBeginPrompt.activeInHierarchy || + PeltzerMain.Instance.paletteController.tutorialSavePrompt.activeInHierarchy || + PeltzerMain.Instance.paletteController.tutorialExitPrompt.activeInHierarchy)) + || (menuActionItem.action == MenuAction.BLOCKMODE && !isBlockMode); } - } - } - /// - /// Make the app menu button red or grey depending on whether or not its active. - /// - /// - public void SetApplicationButtonOverlay(ButtonMode buttonMode) { - if (controllerGeometry.appMenuButton == null) return; - - Material material = controllerGeometry.appMenuButton.GetComponent().material; - Color highlightEmmissionColor; - Color highlightColor; - if (buttonMode == ButtonMode.ACTIVE) { - highlightEmmissionColor = ACTIVE_BUTTON_COLOR * Mathf.LinearToGammaSpace(0.6f); // Set emission to 60%. - highlightColor = ACTIVE_BUTTON_COLOR; - } else if (buttonMode == ButtonMode.WAITING) { - highlightEmmissionColor = WAITING_BUTTON_COLOR * Mathf.LinearToGammaSpace(0f); - highlightColor = WAITING_BUTTON_COLOR; - } else { - highlightEmmissionColor = INACTIVE_BUTTON_COLOR * Mathf.LinearToGammaSpace(0f); - highlightColor = INACTIVE_BUTTON_COLOR; - } - - material.SetColor("_EmissionColor", highlightEmmissionColor); - material.color = highlightColor; - } + /// + /// Resets an unhovered item. + /// + public void ResetUnhoveredItem() + { + if (currentHoveredObject == null) + { + return; + } - /// - /// Make the secondary button red or grey depending on whether or not its active. - /// - /// - public void SetSecondaryButtonOverlay(bool active) { - if (controllerGeometry.secondaryButton == null) return; - - Material material = controllerGeometry.secondaryButton.GetComponent().material; - Color highlightEmmissionColor; - Color highlightColor; - if (active) { - highlightEmmissionColor = ACTIVE_BUTTON_COLOR * Mathf.LinearToGammaSpace(0.6f); // Set emission to 60%. - highlightColor = ACTIVE_BUTTON_COLOR; - } else { - highlightEmmissionColor = INACTIVE_BUTTON_COLOR * Mathf.LinearToGammaSpace(0f); - highlightColor = INACTIVE_BUTTON_COLOR; - } - - material.SetColor("_EmissionColor", highlightEmmissionColor); - material.color = highlightColor; - } + // Some menu buttons turn light gray when hovered, here we reset them. + MenuActionItem menuActionItem = currentHoveredObject.GetComponent(); + if (menuActionItem != null) + { + if (IsHoveredButtonThatShouldChangeColour(menuActionItem)) + { + currentHoveredObject.gameObject.GetComponent().material.color = MENU_BUTTON_DARK; + } + } - /// - /// Casts ray to detect user pointing at the palette menu with this GameObject. (Not to be confused - /// with item selection.) If so, the cursor is bound to the surface of the hit and associated menu interaction - /// UX is coordinated such as haptic feedback, and view alterations such as - /// showing or hiding aspects of the controllers. - /// - private void HandleMenuItemPoint() { - // Cast a ray from the controller and see if it hits the menu. - Vector3 controllerRayVector; - Vector3 controllerRayOrigin; - if (Config.Instance.VrHardware == VrHardware.Rift) { - if (Config.Instance.sdkMode == SdkMode.SteamVR) { - controllerRayVector = Quaternion.Euler(45, 0, 0) * Vector3.forward; - controllerRayOrigin = transform.position + new Vector3(0, -0.045f, 0); - } else { - controllerRayVector = Vector3.forward; - controllerRayOrigin = transform.position;// + new Vector3(0, -0.045f, 0); - } - } else { - controllerRayVector = Vector3.forward; - controllerRayOrigin = transform.position; - } - if (!Physics.Raycast(controllerRayOrigin, transform.TransformDirection(controllerRayVector), - out menuHit, PALETTE_DISTANCE_THRESHOLD)) { - // We didn't hit the menu with the ray, reset any existing state and exit early. - if (mode == ControllerMode.reshape) { - PeltzerMain.Instance.GetSelector().TurnOnSelectIndicator(); - } - ResetUnhoveredItem(); - ResetMenu(); - return; - } - - // Since we are pointing at the menu, be sure to hide the selector so we don't have any overlap. - PeltzerMain.Instance.GetSelector().TurnOffSelectIndicator(); - // If a button is inactive or we're hovering over empty palette space, we want to show the - // pointer and take no further action. - SelectableMenuItem selectableMenuItem = menuHit.transform.GetComponent(); - bool hitIsActive = true; - if (selectableMenuItem != null) { - hitIsActive = selectableMenuItem.isActive; - } - - // Check for the various types of things we could be hovering. - ChangeMaterialMenuItem changeMaterialMenuItem = menuHit.transform.GetComponent(); - bool hoveringColor = changeMaterialMenuItem != null; - ChangeModeMenuItem changeModeMenuItem = menuHit.transform.GetComponent(); - bool hoveringToolhead = changeModeMenuItem != null; - MenuActionItem menuActionItem = menuHit.transform.GetComponent(); - bool hoveringButton = menuActionItem != null; - SelectZandriaCreationMenuItem zandriaCreationMenuItem = menuHit.transform.GetComponent(); - bool hoveringZandriaCreation = zandriaCreationMenuItem != null; - PolyMenuButton polyMenuButton = menuHit.transform.GetComponent(); - bool hoveringPolyMenuOption = polyMenuButton != null; - SelectableDetailsMenuItem selectableDetailsMenuItem = menuHit.transform.GetComponent(); - bool hoveringZandriaDetails = selectableDetailsMenuItem != null; - if (hoveringZandriaDetails) { - hitIsActive &= selectableDetailsMenuItem.creation != null - && selectableDetailsMenuItem.creation.entry != null - && selectableDetailsMenuItem.creation.entry.loadStatus != ZandriaCreationsManager.LoadStatus.FAILED; - } - EmptyMenuItem emptyMenuItem = menuHit.transform.GetComponent(); - bool isHoveringEmptyPaletteSpace = emptyMenuItem != null; - EnvironmentMenuItem environmentMenuItem = menuHit.transform.GetComponent(); - bool isHoveringEnvironmentMenuItem = environmentMenuItem != null; - - // Reset hovered items if we are not hovering anything. - if (!(hoveringColor || hoveringToolhead || hoveringButton || hoveringZandriaCreation || hoveringPolyMenuOption - || hoveringZandriaDetails || isHoveringEmptyPaletteSpace || isHoveringEnvironmentMenuItem)) { - ResetUnhoveredItem(); - ResetMenu(); - return; - } - - // At this point, we know something on the menu has been hit, so we update variables accordingly and - // hide the shapes menu. - menuIsInDefaultState = false; - isPointingAtMenu = true; - shapesMenu.Hide(); - - // We set the position of the pointer dot to be just in front of the item being pointed at. - defaultTipPointer.transform.gameObject.SetActive(true); - if (hoveringToolhead) { - defaultTipPointer.position = menuHit.point + menuHit.transform.up * .015f; - } else { - defaultTipPointer.position = menuHit.point; - } - - if (!hitIsActive || isHoveringEmptyPaletteSpace) { - ResetUnhoveredItem(); - currentHoveredObject = null; - currentSelectableMenuItem = null; - wandTipLabel.text = ""; - ResetTutorialMenu(); - return; - } - - // We set a label on the pointer dot to show what is being pointed at. - if (selectableMenuItem != null && hitIsActive) { - wandTipLabel.text = selectableMenuItem.hoverName; - wandTipLabel.transform.parent.transform.LookAt(PeltzerMain.Instance.hmd.transform); - } else { - wandTipLabel.text = ""; - } - - // Reset the hover animation and start time if this is a new item or not a save submenu. - if ((currentHoveredObject != menuHit.transform.gameObject) - && (!menuHit.transform.name.Equals("Save-Copy") - && !menuHit.transform.name.Equals("Save-Confirm") - && !menuHit.transform.name.Equals("Save-Selected") - && !menuHit.transform.name.Equals("Publish") - && !menuHit.transform.name.Equals("Intro") - && !menuHit.transform.name.Equals("SelectingMoving") - && !menuHit.transform.name.Equals("ModifyingModels") - && !menuHit.transform.name.Equals("SnappingAlignment"))) { - currentHoveredMenuItemStartTime = Time.time; - currentHoveredMenuItemDefaultPos = menuHit.transform.localPosition; - currentHoveredMenuItemDefaultScale = menuHit.transform.localScale; - ResetSaveMenu(); - } - - // Begin, or continue any animations. - if (hoveringButton) { - float delta = (Time.time - currentHoveredMenuItemStartTime); - float pct = (delta / BUTTON_HOVER_ANIMATION_SPEED); - float targetYPos; - float targetYScale; - if (pct > 1f) { - pct = 1f; - // Show sub-menu if available at the end of the animation. - if (menuHit.transform.name.Equals("Save")) { - if (PeltzerMain.Instance.model.GetNumberOfMeshes() > 0) { - saveSubMenu.SetActive(true); - } - } else if (menuHit.transform.name.Equals("Tutorial")) { - tutorialSubMenu.SetActive(true); - } - } - if (!menuHit.transform.name.Equals("Save-Copy") - && !menuHit.transform.name.Equals("Save-Confirm") - && !menuHit.transform.name.Equals("Save-Selected") - && !menuHit.transform.name.Equals("Publish")) { - targetYPos = pct * 0.00250f; - targetYScale = pct * (0.01f - currentHoveredMenuItemDefaultScale.y); - - menuHit.transform.localPosition = new Vector3(menuHit.transform.localPosition.x, - targetYPos, - menuHit.transform.localPosition.z); - - menuHit.transform.localScale = new Vector3(menuHit.transform.localScale.x, - currentHoveredMenuItemDefaultScale.y + targetYScale, - menuHit.transform.localScale.z); - } - } else if (hoveringToolhead) { - // Record the starting collider position so we can move it back to its original position after the toolhead - // is moved. - Vector3 globalColliderStartPosition = - menuHit.transform.TransformPoint(menuHit.transform.GetComponent().center); - - // Hovering over a tool head. - menuHit.transform.localPosition = new Vector3(menuHit.transform.localPosition.x, - (menuHit.transform.localPosition.y + MENU_HOVER_ANIMATION_SPEED) < .02f ? - (menuHit.transform.localPosition.y + .002f) : .02f, - menuHit.transform.localPosition.z); - - // Restore the position of the collider. - menuHit.transform.GetComponent().center = - menuHit.transform.InverseTransformPoint(globalColliderStartPosition); - } else if (hoveringZandriaDetails) { - // Hovering over a saved or featured Poly models menu. - GameObject creationPreview = menuHit.transform.Find("CreationPreview").gameObject; - - creationPreview.transform.localPosition = new Vector3(creationPreview.transform.localPosition.x, - creationPreview.transform.localPosition.y, - (creationPreview.transform.localPosition.z) > -.2f ? - (creationPreview.transform.localPosition.z - .02f) : -.2f); - } else if (isHoveringEnvironmentMenuItem) { - menuHit.transform.localPosition = new Vector3(menuHit.transform.localPosition.x, - (menuHit.transform.localPosition.y + MENU_HOVER_ANIMATION_SPEED) < 0f ? - (menuHit.transform.localPosition.y + .003f) : 0f, - menuHit.transform.localPosition.z); - } else if (!hoveringColor && !hoveringPolyMenuOption) { - // Catch-all for other possible objects to hover. - menuHit.transform.localPosition = new Vector3(menuHit.transform.localPosition.x, - (menuHit.transform.localPosition.y + MENU_HOVER_ANIMATION_SPEED) < .02f ? - (menuHit.transform.localPosition.y + .002f) : .02f, - menuHit.transform.localPosition.z); - } - - // If this hover is the same as last frame, nothing new to do. - if (currentHoveredObject == menuHit.transform.gameObject) { - return; - } - - // Reset last hovered item if needed, special-casing the 'save' sub-menu. - if (currentHoveredObject != null && !currentHoveredObject.name.Equals("Save")) { - ResetUnhoveredItem(); - } else if (currentHoveredObject != null && currentHoveredObject.name.Equals("Save") - && !(menuHit.transform.name.Equals("Save-Copy") - || menuHit.transform.name.Equals("Save-Selected") - || menuHit.transform.name.Equals("Save-Confirm") - || menuHit.transform.name.Equals("Publish"))) { - ResetUnhoveredItem(); - } - - // Trigger haptic feedback. - TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_1, /* durationSeconds */ 0.01f, /* strength */ 0.15f); - - // Animate Color swatches - if (hoveringColor) { - changeMaterialMenuItem.SetHovered(true); - } - - // Animate Poly Menu buttons - if (hoveringPolyMenuOption) { - polyMenuButton.SetHovered(true); - } - - // Set the current menu item so that we can use this value on Trigger pull, i.e. mode selection. - currentSelectableMenuItem = menuHit.transform.GetComponent(); - currentHoveredObject = menuHit.transform.gameObject; - - // Set the material / color for a hovered button. - menuActionItem = currentHoveredObject.GetComponent(); - if (menuActionItem != null) { - if (IsHoveredButtonThatShouldChangeColour(menuActionItem)) { - menuHit.transform.gameObject.GetComponent().material.color = MENU_BUTTON_LIGHT; + ChangeMaterialMenuItem changeMaterialMenuItem = currentHoveredObject.GetComponent(); + PolyMenuButton polyMenuButton = currentHoveredObject.GetComponent(); + if (changeMaterialMenuItem != null) + { + // If we were hovering a colour swatch, return it to its unhovered state. + changeMaterialMenuItem.SetHovered(false); + } + else if (polyMenuButton != null) + { + // If we were hovering a button on the PolyMenu, return it to its unhovered state. + polyMenuButton.SetHovered(false); + } + else if (menuActionItem != null) + { + // If we were hovering a button in the save sub-menu, reset its hovered state. + if (menuActionItem.action != MenuAction.SAVE_COPY + && menuActionItem.action != MenuAction.SAVE_SELECTED + && menuActionItem.action != MenuAction.SHOW_SAVE_CONFIRM + && menuActionItem.action != MenuAction.PUBLISH) + { + currentHoveredObject.transform.localScale = new Vector3(currentHoveredObject.transform.localScale.x, + 0.005f, currentHoveredObject.transform.localScale.z); + currentHoveredObject.transform.localPosition = new Vector3(currentHoveredObject.transform.localPosition.x, + currentHoveredMenuItemDefaultPos.y, currentHoveredObject.transform.localPosition.z); + } + } + else if (currentHoveredObject.GetComponent() != null) + { + // If we were hovering a toolhead, reset its hovered state. + // We need to change the position of the tool heads without moving the colliders position in global space. + Vector3 globalColliderStartPosition = currentHoveredObject.transform.TransformPoint( + currentHoveredObject.transform.GetComponent().center); + + // Move the toolhead back to it's original position. + currentHoveredObject.transform.localPosition = new Vector3(currentHoveredObject.transform.localPosition.x, + 0f, currentHoveredObject.transform.localPosition.z); + + // Restore the position of the collider before we moved the toolhead. + currentHoveredObject.transform.GetComponent().center = + currentHoveredObject.transform.InverseTransformPoint(globalColliderStartPosition); + } + else if (currentHoveredObject.GetComponent() != null) + { + currentHoveredObject.transform.localPosition = new Vector3(currentHoveredObject.transform.localPosition.x, + -0.02f, currentHoveredObject.transform.localPosition.z); + } + else + { + // If none of the above special-cases were hit, just reset the default position of whatever else was hovered. + currentHoveredObject.transform.localPosition = new Vector3(currentHoveredObject.transform.localPosition.x, + 0f, currentHoveredObject.transform.localPosition.z); + + // If a poly menu item was being hovered, reset it. + Transform creationPreview = currentHoveredObject.transform.Find("CreationPreview"); + if (creationPreview != null) + { + creationPreview.gameObject.transform.localPosition = new Vector3(0f, 0f, 0f); + } + } } - } - } - - /// - /// Hide and reset any floating save UI and associated state. - /// - private void ResetSaveMenu() { - saveSubMenu.SetActive(false); - saveMenuBtn.GetComponent().material.color = MENU_BUTTON_DARK; - saveMenuBtn.transform.localScale = new Vector3(saveMenuBtn.transform.localScale.x, - 0.005f, saveMenuBtn.transform.localScale.z); - saveMenuBtn.transform.localPosition = new Vector3(saveMenuBtn.transform.localPosition.x, - 0f, saveMenuBtn.transform.localPosition.z); - } - /// - /// Hide and reset any floating save UI and associated state. - /// - private void ResetTutorialMenu() { - tutorialSubMenu.SetActive(false); - } - - /// - /// If nothing on the menu is being pointed at, reset the whole menu state. - /// - public void ResetMenu() { - if (menuIsInDefaultState) { - return; - } - - // Hide and reset any floating save UI and associated state. - ResetSaveMenu(); - ResetTutorialMenu(); - - // Set the "cursor/pointer" back to the default location. - defaultTipPointer.transform.gameObject.SetActive(false); - defaultTipPointer.localPosition = defaultTipPointerDefaultLocation; - lastPositionModel = worldSpace.WorldToModel(wandTip.transform.position); - isPointingAtMenu = false; - - // Reset currentMenuItem and hover - currentSelectableMenuItem = null; - currentHoveredObject = null; - - if (attachedToolHead != null) { - attachedToolHead.SetActive(true); - } - menuIsInDefaultState = true; - } + /// + /// Detect and distribute events for an abstracted Trigger input. + /// + /// Instanec of SteamVR_Controller.Device + private void DetectTrigger(ControllerDevice controller) + { + // Note: for better precision, we detect trigger up/down based on a threshold rather than use the built-in + // SteamVR down/up detection for the trigger button. + + if (controller.WasJustPressed(ButtonId.Trigger)) + { + PeltzerControllerActionHandler(this, new ControllerEventArgs(ControllerType.PELTZER, + ButtonId.Trigger, + ButtonAction.DOWN, + controller.GetTouchpadLocation(), + currentOverlay)); + } + else if (controller.WasJustReleased(ButtonId.Trigger)) + { + PeltzerControllerActionHandler(this, new ControllerEventArgs(ControllerType.PELTZER, + ButtonId.Trigger, + ButtonAction.UP, + controller.GetTouchpadLocation(), + currentOverlay)); + } + } - /// - /// True if the given item is a button that should change color when hovered. - /// - private bool IsHoveredButtonThatShouldChangeColour(MenuActionItem menuActionItem) { - return menuActionItem.action == MenuAction.CLEAR - || menuActionItem.action == MenuAction.SAVE - || menuActionItem.action == MenuAction.CANCEL_SAVE - || menuActionItem.action == MenuAction.NOTHING - || menuActionItem.action == MenuAction.SHOW_SAVE_CONFIRM - || menuActionItem.action == MenuAction.SAVE_COPY - || menuActionItem.action == MenuAction.SAVE_SELECTED - || menuActionItem.action == MenuAction.PUBLISH - || menuActionItem.action == MenuAction.NEW_WITH_SAVE - || (menuActionItem.action == MenuAction.TUTORIAL_PROMPT && !( - PeltzerMain.Instance.paletteController.tutorialBeginPrompt.activeInHierarchy || - PeltzerMain.Instance.paletteController.tutorialSavePrompt.activeInHierarchy || - PeltzerMain.Instance.paletteController.tutorialExitPrompt.activeInHierarchy)) - || (menuActionItem.action == MenuAction.BLOCKMODE && !isBlockMode); - } + /// + /// Triggers controller vibration. + /// + /// Better haptic feedback will come with https://bug + /// + public void TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType type = HapticFeedback.HapticFeedbackType.FEEDBACK_1, + float durationSeconds = 0.01f, float strength = 0.3f) + { + + float lastTime; + if (lastHapticFeedbackTime.TryGetValue(type, out lastTime) && + Time.time - lastTime < MIN_HAPTIC_FEEDBACK_INTERVAL) + { + // We triggered haptic feedback of this type too recently. Throttle. + return; + } + lastHapticFeedbackTime[type] = Time.time; - /// - /// Resets an unhovered item. - /// - public void ResetUnhoveredItem() { - if (currentHoveredObject == null) { - return; - } - - // Some menu buttons turn light gray when hovered, here we reset them. - MenuActionItem menuActionItem = currentHoveredObject.GetComponent(); - if (menuActionItem != null) { - if (IsHoveredButtonThatShouldChangeColour(menuActionItem)) { - currentHoveredObject.gameObject.GetComponent().material.color = MENU_BUTTON_DARK; - } - } - - ChangeMaterialMenuItem changeMaterialMenuItem = currentHoveredObject.GetComponent(); - PolyMenuButton polyMenuButton = currentHoveredObject.GetComponent(); - if (changeMaterialMenuItem != null) { - // If we were hovering a colour swatch, return it to its unhovered state. - changeMaterialMenuItem.SetHovered(false); - } else if (polyMenuButton != null) { - // If we were hovering a button on the PolyMenu, return it to its unhovered state. - polyMenuButton.SetHovered(false); - } else if (menuActionItem != null) { - // If we were hovering a button in the save sub-menu, reset its hovered state. - if (menuActionItem.action != MenuAction.SAVE_COPY - && menuActionItem.action != MenuAction.SAVE_SELECTED - && menuActionItem.action != MenuAction.SHOW_SAVE_CONFIRM - && menuActionItem.action != MenuAction.PUBLISH) { - currentHoveredObject.transform.localScale = new Vector3(currentHoveredObject.transform.localScale.x, - 0.005f, currentHoveredObject.transform.localScale.z); - currentHoveredObject.transform.localPosition = new Vector3(currentHoveredObject.transform.localPosition.x, - currentHoveredMenuItemDefaultPos.y, currentHoveredObject.transform.localPosition.z); + if (haptics != null) + { + haptics.PlayHapticFeedback(type, durationSeconds, strength); + } } - } else if (currentHoveredObject.GetComponent() != null) { - // If we were hovering a toolhead, reset its hovered state. - // We need to change the position of the tool heads without moving the colliders position in global space. - Vector3 globalColliderStartPosition = currentHoveredObject.transform.TransformPoint( - currentHoveredObject.transform.GetComponent().center); - - // Move the toolhead back to it's original position. - currentHoveredObject.transform.localPosition = new Vector3(currentHoveredObject.transform.localPosition.x, - 0f, currentHoveredObject.transform.localPosition.z); - - // Restore the position of the collider before we moved the toolhead. - currentHoveredObject.transform.GetComponent().center = - currentHoveredObject.transform.InverseTransformPoint(globalColliderStartPosition); - } else if (currentHoveredObject.GetComponent() != null) { - currentHoveredObject.transform.localPosition = new Vector3(currentHoveredObject.transform.localPosition.x, - -0.02f, currentHoveredObject.transform.localPosition.z); - } else { - // If none of the above special-cases were hit, just reset the default position of whatever else was hovered. - currentHoveredObject.transform.localPosition = new Vector3(currentHoveredObject.transform.localPosition.x, - 0f, currentHoveredObject.transform.localPosition.z); - - // If a poly menu item was being hovered, reset it. - Transform creationPreview = currentHoveredObject.transform.Find("CreationPreview"); - if (creationPreview != null) { - creationPreview.gameObject.transform.localPosition = new Vector3(0f, 0f, 0f); + + /// + /// Triggers controller vibrations to get the user to look at the controller. + /// + public void LookAtMe() + { + TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_1, + 0.2f, + 0.2f + ); } - } - } - /// - /// Detect and distribute events for an abstracted Trigger input. - /// - /// Instanec of SteamVR_Controller.Device - private void DetectTrigger(ControllerDevice controller) { - // Note: for better precision, we detect trigger up/down based on a threshold rather than use the built-in - // SteamVR down/up detection for the trigger button. - - if (controller.WasJustPressed(ButtonId.Trigger)) { - PeltzerControllerActionHandler(this, new ControllerEventArgs(ControllerType.PELTZER, - ButtonId.Trigger, - ButtonAction.DOWN, - controller.GetTouchpadLocation(), - currentOverlay)); - } else if (controller.WasJustReleased(ButtonId.Trigger)) { - PeltzerControllerActionHandler(this, new ControllerEventArgs(ControllerType.PELTZER, - ButtonId.Trigger, - ButtonAction.UP, - controller.GetTouchpadLocation(), - currentOverlay)); - } - } + /// + /// Triggers controller vibrations to let the user know they've done a good job. + /// + public void YouDidIt() + { + TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_1, + 0.1f, + 0.2f + ); + } - /// - /// Triggers controller vibration. - /// - /// Better haptic feedback will come with https://bug - /// - public void TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType type = HapticFeedback.HapticFeedbackType.FEEDBACK_1, - float durationSeconds = 0.01f, float strength = 0.3f) { - - float lastTime; - if (lastHapticFeedbackTime.TryGetValue(type, out lastTime) && - Time.time - lastTime < MIN_HAPTIC_FEEDBACK_INTERVAL) { - // We triggered haptic feedback of this type too recently. Throttle. - return; - } - lastHapticFeedbackTime[type] = Time.time; - - if (haptics != null) { - haptics.PlayHapticFeedback(type, durationSeconds, strength); - } - } + // Handle mode selection when appropriate. + private void ControllerEventHandler(object sender, ControllerEventArgs args) + { + if (args.ControllerType == ControllerType.PELTZER && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN) + { + TouchpadLocation location = args.TouchpadLocation; + + // Don't allow mode switching while zooming. + if (PeltzerMain.Instance.Zoomer.Zooming) + { + return; + } - /// - /// Triggers controller vibrations to get the user to look at the controller. - /// - public void LookAtMe() { - TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_1, - 0.2f, - 0.2f - ); - } + if (IsModifyMode(mode)) + { + // Don't allow switching modes whilst in the middle of an operation. + if ((mode == ControllerMode.extrude && PeltzerMain.Instance.GetExtruder().IsExtrudingFace()) || + mode == ControllerMode.reshape && PeltzerMain.Instance.GetReshaper().IsReshaping() || + mode == ControllerMode.reshape && PeltzerMain.Instance.GetSubdivider().IsSubdividing()) + { + return; + } + + if (location == TouchpadLocation.TOP && mode != ControllerMode.reshape) + { + ChangeMode(ControllerMode.reshape); + } + else if (location == TouchpadLocation.LEFT && mode != ControllerMode.subdivideFace) + { + ChangeMode(ControllerMode.subdivideFace); + } + else if (location == TouchpadLocation.RIGHT && mode != ControllerMode.extrude) + { + ChangeMode(ControllerMode.extrude); + } + } + else if (IsPaintMode(mode)) + { + if (location == TouchpadLocation.LEFT && mode != ControllerMode.paintMesh) + { + ChangeMode(ControllerMode.paintMesh); + lastSelectedPaintMode = ControllerMode.paintMesh; + } + else if (location == TouchpadLocation.RIGHT && mode != ControllerMode.paintFace) + { + ChangeMode(ControllerMode.paintFace); + lastSelectedPaintMode = ControllerMode.paintFace; + } + } + else if (IsDeleteMode(mode)) + { + if (!Features.enablePartDeletion) + { + ChangeMode(ControllerMode.delete); + } + else + { + if (location == TouchpadLocation.RIGHT && mode != ControllerMode.deletePart) + { + ChangeMode(ControllerMode.deletePart); + } + else if (location == TouchpadLocation.LEFT && mode != ControllerMode.delete) + { + ChangeMode(ControllerMode.delete); + } + } + } + } + } - /// - /// Triggers controller vibrations to let the user know they've done a good job. - /// - public void YouDidIt() { - TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_1, - 0.1f, - 0.2f - ); - } + // Stop showing the overlay on the trackpad during volume 'fill' insertion. + public void SetVolumeOverlayActive(bool active) + { + controllerGeometry.volumeInserterOverlay.gameObject.SetActive(active); + controllerGeometry.shapeTooltips.SetActive(active); + } - // Handle mode selection when appropriate. - private void ControllerEventHandler(object sender, ControllerEventArgs args) { - if (args.ControllerType == ControllerType.PELTZER && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN) { - TouchpadLocation location = args.TouchpadLocation; + private bool IsModifyMode(ControllerMode mode) + { + return mode == ControllerMode.reshape + || mode == ControllerMode.extrude + || mode == ControllerMode.subdivideFace + || mode == ControllerMode.subdivideMesh; + } - // Don't allow mode switching while zooming. - if (PeltzerMain.Instance.Zoomer.Zooming) { - return; + private bool IsDeleteMode(ControllerMode mode) + { + return mode == ControllerMode.delete + || mode == ControllerMode.deletePart; } - if (IsModifyMode(mode)) { - // Don't allow switching modes whilst in the middle of an operation. - if ((mode == ControllerMode.extrude && PeltzerMain.Instance.GetExtruder().IsExtrudingFace()) || - mode == ControllerMode.reshape && PeltzerMain.Instance.GetReshaper().IsReshaping() || - mode == ControllerMode.reshape && PeltzerMain.Instance.GetSubdivider().IsSubdividing()) { - return; - } - - if (location == TouchpadLocation.TOP && mode != ControllerMode.reshape) { - ChangeMode(ControllerMode.reshape); - } else if (location == TouchpadLocation.LEFT && mode != ControllerMode.subdivideFace) { - ChangeMode(ControllerMode.subdivideFace); - } else if (location == TouchpadLocation.RIGHT && mode != ControllerMode.extrude) { - ChangeMode(ControllerMode.extrude); - } - } else if (IsPaintMode(mode)) { - if (location == TouchpadLocation.LEFT && mode != ControllerMode.paintMesh) { - ChangeMode(ControllerMode.paintMesh); - lastSelectedPaintMode = ControllerMode.paintMesh; - } else if (location == TouchpadLocation.RIGHT && mode != ControllerMode.paintFace) { - ChangeMode(ControllerMode.paintFace); - lastSelectedPaintMode = ControllerMode.paintFace; - } - } else if (IsDeleteMode(mode)) { - if (!Features.enablePartDeletion) { - ChangeMode(ControllerMode.delete); - } - else { - if (location == TouchpadLocation.RIGHT && mode != ControllerMode.deletePart) { - ChangeMode(ControllerMode.deletePart); - } - else if (location == TouchpadLocation.LEFT && mode != ControllerMode.delete) { - ChangeMode(ControllerMode.delete); - } - } + private bool IsPaintMode(ControllerMode mode) + { + return mode == ControllerMode.paintFace + || mode == ControllerMode.paintMesh; } - } - } - // Stop showing the overlay on the trackpad during volume 'fill' insertion. - public void SetVolumeOverlayActive(bool active) { - controllerGeometry.volumeInserterOverlay.gameObject.SetActive(active); - controllerGeometry.shapeTooltips.SetActive(active); - } + /// + /// Toggles block mode by enabling or disabling the grid and sending out a BlockModeChanged event to all + /// listening tools. + /// + public void ToggleBlockMode(bool initiatedByUser) + { + isBlockMode = !isBlockMode; + + if (isBlockMode) + { + // Set status button to green for active. + blockModeButton.GetComponent().material.color = MENU_BUTTON_GREEN; + } + else + { + // Set status button to dark default for inactive. + blockModeButton.GetComponent().material.color = MENU_BUTTON_DARK; + } - private bool IsModifyMode(ControllerMode mode) { - return mode == ControllerMode.reshape - || mode == ControllerMode.extrude - || mode == ControllerMode.subdivideFace - || mode == ControllerMode.subdivideMesh; - } + if (BlockModeChangedHandler != null) + BlockModeChangedHandler(isBlockMode); - private bool IsDeleteMode(ControllerMode mode) { - return mode == ControllerMode.delete - || mode == ControllerMode.deletePart; - } + } - private bool IsPaintMode(ControllerMode mode) { - return mode == ControllerMode.paintFace - || mode == ControllerMode.paintMesh; - } + public Vector3 LastPositionModel { get { return lastPositionModel; } } + public Quaternion LastRotationWorld { get { return lastRotationWorld; } } + public Quaternion LastRotationModel { get { return worldSpace.WorldOrientationToModel(lastRotationWorld); } } + + /// + /// Handles mode change requests. + /// + /// The mode we are enabling + /// An optional reference to a gameobject representing a tool head for animation. + public void ChangeMode(ControllerMode newMode, GameObject toolHead = null) + { + if (!PeltzerMain.Instance.restrictionManager.IsControllerModeAllowed(newMode)) + { + return; + } - /// - /// Toggles block mode by enabling or disabling the grid and sending out a BlockModeChanged event to all - /// listening tools. - /// - public void ToggleBlockMode(bool initiatedByUser) { - isBlockMode = !isBlockMode; + ControllerMode previousMode = this.mode; + this.mode = newMode; - if (isBlockMode) { - // Set status button to green for active. - blockModeButton.GetComponent().material.color = MENU_BUTTON_GREEN; - } else { - // Set status button to dark default for inactive. - blockModeButton.GetComponent().material.color = MENU_BUTTON_DARK; - } + if (toolHead != null && toolHead != attachedToolHead) + { + // We changed to a new tool, hide the necessary tooltips. + PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); + } - if (BlockModeChangedHandler != null) - BlockModeChangedHandler(isBlockMode); + ShowTooltips(); + ResetTouchpadOverlay(); - } - public Vector3 LastPositionModel { get { return lastPositionModel; } } - public Quaternion LastRotationWorld { get { return lastRotationWorld; } } - public Quaternion LastRotationModel { get { return worldSpace.WorldOrientationToModel(lastRotationWorld); } } + // Set the application button and secondary button to be inactive by default. If a tool wants it active it will + // set it itself. + SetApplicationButtonOverlay(ButtonMode.INACTIVE); + SetSecondaryButtonOverlay(/*active*/ false); - /// - /// Handles mode change requests. - /// - /// The mode we are enabling - /// An optional reference to a gameobject representing a tool head for animation. - public void ChangeMode(ControllerMode newMode, GameObject toolHead = null) { - if (!PeltzerMain.Instance.restrictionManager.IsControllerModeAllowed(newMode)) { - return; - } - - ControllerMode previousMode = this.mode; - this.mode = newMode; - - if (toolHead != null && toolHead != attachedToolHead) { - // We changed to a new tool, hide the necessary tooltips. - PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); - } - - ShowTooltips(); - ResetTouchpadOverlay(); - - - // Set the application button and secondary button to be inactive by default. If a tool wants it active it will - // set it itself. - SetApplicationButtonOverlay(ButtonMode.INACTIVE); - SetSecondaryButtonOverlay(/*active*/ false); - - if (ModeChangedHandler != null) { - ModeChangedHandler(previousMode, newMode); - } - - // Animate the tool mode change - if (previousMode != newMode) { - if (toolHead != null) { - if (attachedToolHead != null) { - // Deactivate any attached animations to instance of attachedToolHead. - switch (previousMode) { - case ControllerMode.extrude: - case ControllerMode.reshape: - case ControllerMode.subdivideFace: - case ControllerMode.subdivideMesh: - attachedToolHead.GetComponent().Deactivate(); - switch (newMode) { - case ControllerMode.reshape: - PeltzerMain.Instance.GetSelector().UpdateInactive(Selector.FACES_EDGES_AND_VERTICES); + if (ModeChangedHandler != null) + { + ModeChangedHandler(previousMode, newMode); + } + + // Animate the tool mode change + if (previousMode != newMode) + { + if (toolHead != null) + { + if (attachedToolHead != null) + { + // Deactivate any attached animations to instance of attachedToolHead. + switch (previousMode) + { + case ControllerMode.extrude: + case ControllerMode.reshape: + case ControllerMode.subdivideFace: + case ControllerMode.subdivideMesh: + attachedToolHead.GetComponent().Deactivate(); + switch (newMode) + { + case ControllerMode.reshape: + PeltzerMain.Instance.GetSelector().UpdateInactive(Selector.FACES_EDGES_AND_VERTICES); + break; + case ControllerMode.subdivideFace: + case ControllerMode.subdivideMesh: + PeltzerMain.Instance.GetSelector().UpdateInactive(Selector.EDGES_ONLY); + break; + default: + PeltzerMain.Instance.GetSelector().ResetInactive(); + break; + } + break; + case ControllerMode.move: + attachedToolHead.GetComponent().Deactivate(); + break; + case ControllerMode.paintFace: + case ControllerMode.paintMesh: + attachedToolHead.GetComponent().Deactivate(); + break; + case ControllerMode.deletePart: + PeltzerMain.Instance.GetSelector().ResetInactive(); + break; + } + + // Detach the current toolhead. + attachedToolHead.AddComponent(); + BoxCollider bc = attachedToolHead.AddComponent(); + bc.size = new Vector3(0.05f, 0.035f, 0.1f); + bc.center = new Vector3(0f, 0f, 0.02f); + attachedToolHead.transform.parent = null; + Destroy(attachedToolHead, DETACHED_TOOLHEAD_TIME_TO_LIVE); + } + + // Create the new tool head. + attachedToolHead = (GameObject)Instantiate(toolHead, gameObject.transform.localPosition, Quaternion.identity); + attachedToolHead.SetActive(true); + DestroyImmediate(attachedToolHead.GetComponent()); + + attachedToolHead.transform.parent = gameObject.transform; + attachedToolHead.transform.localScale = toolHead.transform.lossyScale; + attachedToolHead.transform.position = toolHead.transform.position; + attachedToolHead.transform.localEulerAngles = new Vector3(-6f, toolHead.transform.localEulerAngles.y, + toolHead.transform.localEulerAngles.z); + + // Hide the 'mock shape' attached to the toolhead, if any. + Transform attachedMockShape = attachedToolHead.transform.Find("mockShape"); + if (attachedMockShape != null) + { + attachedMockShape.gameObject.SetActive(false); + } + + // Reshow any removed tool and hide the toolhead on the palette. + GameObject removed = PeltzerMain.Instance.paletteController.GetToolheadForMode(previousMode); + // Show the 'mock shape' attached to the toolhead, if any. + Transform paletteMockShape = removed.transform.Find("mockShape"); + if (paletteMockShape != null) + { + paletteMockShape.gameObject.SetActive(true); + } + removed.SetActive(true); + + if (previousMode == ControllerMode.move && handedness == Handedness.LEFT) + { + removed.GetComponent().ScaleFromNothing(new Vector3(-1f, 1f, 1f)); + } + else + { + removed.GetComponent().ScaleFromNothing(Vector3.one); + } + + GameObject added = PeltzerMain.Instance.paletteController.GetToolheadForMode(newMode); + GetComponentInChildren().UpdateTouchpadOverlay(toolHead); + added.SetActive(false); + StartToolHeadAnimation(); + } + } + + // modify the registration point based on the tool. + switch (newMode) + { + case ControllerMode.reshape: + defaultTipPointerDefaultLocation = new Vector3(0f, 0f, -0.055f); break; - case ControllerMode.subdivideFace: - case ControllerMode.subdivideMesh: - PeltzerMain.Instance.GetSelector().UpdateInactive(Selector.EDGES_ONLY); + case ControllerMode.insertStroke: + case ControllerMode.insertVolume: + if (Config.Instance.VrHardware == VrHardware.Vive) + { + defaultTipPointerDefaultLocation = new Vector3(0f, 0f, -0.015f); + } + else + { + defaultTipPointerDefaultLocation = new Vector3(0f, 0, -0.045f); + } break; - default: - PeltzerMain.Instance.GetSelector().ResetInactive(); + case ControllerMode.delete: + case ControllerMode.deletePart: + defaultTipPointerDefaultLocation = new Vector3(0f, 0f, -0.035f); break; + default: + defaultTipPointerDefaultLocation = new Vector3(0f, 0f, -0.045f); + break; + } + + if (Config.Instance.VrHardware == VrHardware.Rift) + { + if (Config.Instance.sdkMode == SdkMode.SteamVR) + { + defaultTipPointerDefaultLocation = defaultTipPointerDefaultLocation + WAND_TIP_POSITION_RIFT - new Vector3(0f, 0f, -0.045f); } - break; - case ControllerMode.move: - attachedToolHead.GetComponent().Deactivate(); - break; - case ControllerMode.paintFace: - case ControllerMode.paintMesh: - attachedToolHead.GetComponent().Deactivate(); - break; - case ControllerMode.deletePart: - PeltzerMain.Instance.GetSelector().ResetInactive(); - break; - } - - // Detach the current toolhead. - attachedToolHead.AddComponent(); - BoxCollider bc = attachedToolHead.AddComponent(); - bc.size = new Vector3(0.05f, 0.035f, 0.1f); - bc.center = new Vector3(0f, 0f, 0.02f); - attachedToolHead.transform.parent = null; - Destroy(attachedToolHead, DETACHED_TOOLHEAD_TIME_TO_LIVE); - } - - // Create the new tool head. - attachedToolHead = (GameObject)Instantiate(toolHead, gameObject.transform.localPosition, Quaternion.identity); - attachedToolHead.SetActive(true); - DestroyImmediate(attachedToolHead.GetComponent()); - - attachedToolHead.transform.parent = gameObject.transform; - attachedToolHead.transform.localScale = toolHead.transform.lossyScale; - attachedToolHead.transform.position = toolHead.transform.position; - attachedToolHead.transform.localEulerAngles = new Vector3(-6f, toolHead.transform.localEulerAngles.y, - toolHead.transform.localEulerAngles.z); - - // Hide the 'mock shape' attached to the toolhead, if any. - Transform attachedMockShape = attachedToolHead.transform.Find("mockShape"); - if (attachedMockShape != null) { - attachedMockShape.gameObject.SetActive(false); - } - - // Reshow any removed tool and hide the toolhead on the palette. - GameObject removed = PeltzerMain.Instance.paletteController.GetToolheadForMode(previousMode); - // Show the 'mock shape' attached to the toolhead, if any. - Transform paletteMockShape = removed.transform.Find("mockShape"); - if (paletteMockShape != null) { - paletteMockShape.gameObject.SetActive(true); - } - removed.SetActive(true); - - if(previousMode == ControllerMode.move && handedness == Handedness.LEFT) { - removed.GetComponent().ScaleFromNothing(new Vector3(-1f, 1f, 1f)); - } else { - removed.GetComponent().ScaleFromNothing(Vector3.one); - } - - GameObject added = PeltzerMain.Instance.paletteController.GetToolheadForMode(newMode); - GetComponentInChildren().UpdateTouchpadOverlay(toolHead); - added.SetActive(false); - StartToolHeadAnimation(); - } - } - - // modify the registration point based on the tool. - switch (newMode) { - case ControllerMode.reshape: - defaultTipPointerDefaultLocation = new Vector3(0f, 0f, -0.055f); - break; - case ControllerMode.insertStroke: - case ControllerMode.insertVolume: - if (Config.Instance.VrHardware == VrHardware.Vive) { - defaultTipPointerDefaultLocation = new Vector3(0f, 0f, -0.015f); - } else { - defaultTipPointerDefaultLocation = new Vector3(0f, 0, -0.045f); - } - break; - case ControllerMode.delete: - case ControllerMode.deletePart: - defaultTipPointerDefaultLocation = new Vector3(0f, 0f, -0.035f); - break; - default: - defaultTipPointerDefaultLocation = new Vector3(0f, 0f, -0.045f); - break; - } - - if (Config.Instance.VrHardware == VrHardware.Rift) { - if (Config.Instance.sdkMode == SdkMode.SteamVR) { - defaultTipPointerDefaultLocation = defaultTipPointerDefaultLocation + WAND_TIP_POSITION_RIFT - new Vector3(0f, 0f, -0.045f); - } else { - defaultTipPointerDefaultLocation = defaultTipPointerDefaultLocation + WAND_TIP_POSITION_OCULUS - new Vector3(0f, 0f, -0.045f); + else + { + defaultTipPointerDefaultLocation = defaultTipPointerDefaultLocation + WAND_TIP_POSITION_OCULUS - new Vector3(0f, 0f, -0.045f); + } + + } + } - } + /// + /// Animates a toolhead from the palette menu onto the controller. + /// + /// + private void StartToolHeadAnimation() + { + BringAttachedToolheadToController(); + ChangeToolColor(); + TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_4, /* durationSeconds */ 0.2f, /* strength */ 0.4f); + } - } + public void BringAttachedToolheadToController() + { + attachedToolHead.GetComponent().BringToController(); + } - /// - /// Animates a toolhead from the palette menu onto the controller. - /// - /// - private void StartToolHeadAnimation() { - BringAttachedToolheadToController(); - ChangeToolColor(); - TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_4, /* durationSeconds */ 0.2f, /* strength */ 0.4f); - } + /// + /// Hide images on the trackpad that show the various modify modes. + /// + public void HideModifyOverlays() + { + controllerGeometry.modifyOverlay.SetActive(false); + } - public void BringAttachedToolheadToController() { - attachedToolHead.GetComponent().BringToController(); - } + /// + /// Show images on the trackpad that show the various modify modes. + /// + public void ShowModifyOverlays() + { + ChangeTouchpadOverlay(currentOverlay); + } - /// - /// Hide images on the trackpad that show the various modify modes. - /// - public void HideModifyOverlays() { - controllerGeometry.modifyOverlay.SetActive(false); - } + /// + /// Hide textual overlay tooltips. + /// + public void HideTooltips() + { + controllerGeometry.shapeTooltips.SetActive(false); + controllerGeometry.freeformTooltips.SetActive(false); + controllerGeometry.modifyTooltips.SetActive(false); + controllerGeometry.paintTooltips.SetActive(false); + controllerGeometry.moverTooltips.SetActive(false); + controllerGeometry.grabTooltips.SetActive(false); + controllerGeometry.groupTooltipRoot.SetActive(false); + } - /// - /// Show images on the trackpad that show the various modify modes. - /// - public void ShowModifyOverlays() { - ChangeTouchpadOverlay(currentOverlay); - } + /// + /// Show textual overlay tooltips appropriate to the current tool. + /// + public void ShowTooltips() + { + if (!PeltzerMain.Instance.restrictionManager.tooltipsAllowed || PeltzerMain.Instance.HasDisabledTooltips) return; + + controllerGeometry.shapeTooltips.SetActive(true); + controllerGeometry.freeformTooltips.SetActive(true); + controllerGeometry.modifyTooltips.SetActive(true); + controllerGeometry.paintTooltips.SetActive(true); + controllerGeometry.moverTooltips.SetActive(true); + controllerGeometry.grabTooltips.SetActive(true); + controllerGeometry.groupTooltipRoot.SetActive(true); + } - /// - /// Hide textual overlay tooltips. - /// - public void HideTooltips() { - controllerGeometry.shapeTooltips.SetActive(false); - controllerGeometry.freeformTooltips.SetActive(false); - controllerGeometry.modifyTooltips.SetActive(false); - controllerGeometry.paintTooltips.SetActive(false); - controllerGeometry.moverTooltips.SetActive(false); - controllerGeometry.grabTooltips.SetActive(false); - controllerGeometry.groupTooltipRoot.SetActive(false); - } + public void ChangeToolColor() + { + if (PaletteController.AcquireIfNecessary(ref PeltzerMain.Instance.paletteController)) + { + PeltzerMain.Instance.paletteController.UpdateColors(currentMaterial); + } - /// - /// Show textual overlay tooltips appropriate to the current tool. - /// - public void ShowTooltips() { - if (!PeltzerMain.Instance.restrictionManager.tooltipsAllowed || PeltzerMain.Instance.HasDisabledTooltips) return; - - controllerGeometry.shapeTooltips.SetActive(true); - controllerGeometry.freeformTooltips.SetActive(true); - controllerGeometry.modifyTooltips.SetActive(true); - controllerGeometry.paintTooltips.SetActive(true); - controllerGeometry.moverTooltips.SetActive(true); - controllerGeometry.grabTooltips.SetActive(true); - controllerGeometry.groupTooltipRoot.SetActive(true); - } + // Change the preview shapes for Volume Inserter. We have to generate new GameObjects because the old + // ones are being drawn with Graphics.DrawMesh which won't respond to changes in material. + shapesMenu.ChangeShapesMenuMaterial(currentMaterial); - public void ChangeToolColor() { - if (PaletteController.AcquireIfNecessary(ref PeltzerMain.Instance.paletteController)) { - PeltzerMain.Instance.paletteController.UpdateColors(currentMaterial); - } + if (attachedToolHead == null) + { + return; + } - // Change the preview shapes for Volume Inserter. We have to generate new GameObjects because the old - // ones are being drawn with Graphics.DrawMesh which won't respond to changes in material. - shapesMenu.ChangeShapesMenuMaterial(currentMaterial); + // Change the attached tool. + ColorChanger currentTool = attachedToolHead.GetComponentInChildren(); - if (attachedToolHead == null) { - return; - } + if (currentTool != null) + { + currentTool.ChangeMaterial(currentMaterial); + } + } - // Change the attached tool. - ColorChanger currentTool = attachedToolHead.GetComponentInChildren(); + /// + /// Change the touchpad overlay to the given type. Will automatically highlight selected + /// modes where appropriate. + /// + /// + public void ChangeTouchpadOverlay(TouchpadOverlay newOverlay) + { + currentOverlay = newOverlay; + + // Set the correct parent overlay active based on the passed overlay. Some parent overlays have sub-overlays that + // will be set based on ControllerMode. + controllerGeometry.volumeInserterOverlay.SetActive(currentOverlay == TouchpadOverlay.VOLUME_INSERTER); + controllerGeometry.freeformOverlay.SetActive(currentOverlay == TouchpadOverlay.FREEFORM); + controllerGeometry.paintOverlay.SetActive(currentOverlay == TouchpadOverlay.PAINT); + controllerGeometry.modifyOverlay.SetActive(currentOverlay == TouchpadOverlay.MODIFY); + controllerGeometry.moveOverlay.SetActive(currentOverlay == TouchpadOverlay.MOVE); + if (controllerGeometry.OnMoveOverlay != null) controllerGeometry.OnMoveOverlay.SetActive(currentOverlay == TouchpadOverlay.MOVE); + + controllerGeometry.deleteOverlay.SetActive(currentOverlay == TouchpadOverlay.DELETE); + + controllerGeometry.menuOverlay.SetActive(currentOverlay == TouchpadOverlay.MENU); + if (controllerGeometry.OnMenuOverlay != null) controllerGeometry.OnMenuOverlay.SetActive(currentOverlay == TouchpadOverlay.MENU); + + controllerGeometry.undoRedoOverlay.SetActive(currentOverlay == TouchpadOverlay.UNDO_REDO); + if (controllerGeometry.OnUndoRedoOverlay != null) controllerGeometry.OnUndoRedoOverlay.SetActive(currentOverlay == TouchpadOverlay.UNDO_REDO); + + controllerGeometry.resizeOverlay.SetActive(currentOverlay == TouchpadOverlay.RESIZE); + controllerGeometry.resetZoomOverlay.SetActive(currentOverlay == TouchpadOverlay.RESET_ZOOM); + + // Set the secondary button active if the reset zoom or checkpoint button should be shown. + SetSecondaryButtonOverlay(/*active*/ currentOverlay == TouchpadOverlay.RESET_ZOOM + || (mode == ControllerMode.insertStroke && freeformInstance.IsStroking() && freeformInstance.IsManualStroking())); + + // If the restrictions manager is allowing touchpadHighlighting don't let the peltzerController change any + // icons. + if (PeltzerMain.Instance.restrictionManager.touchpadHighlightingAllowed) + { + return; + } - if (currentTool != null) { - currentTool.ChangeMaterial(currentMaterial); - } - } + Color fullWhite = new Color(1f, 1f, 1f, 1f); + Color halfWhite = new Color(1f, 1f, 1f, 0.196f); + + // Color the sub-overlays depending on mode. + switch (mode) + { + case ControllerMode.paintMesh: + // The mesh. + controllerGeometry.paintOverlay.GetComponent().leftIcon.color = fullWhite; + // The face. + controllerGeometry.paintOverlay.GetComponent().rightIcon.color = halfWhite; + break; + case ControllerMode.paintFace: + // The mesh. + controllerGeometry.paintOverlay.GetComponent().leftIcon.color = halfWhite; + // The face. + controllerGeometry.paintOverlay.GetComponent().rightIcon.color = fullWhite; + break; + case ControllerMode.subdivideFace: + // Subdivide. + controllerGeometry.modifyOverlay.GetComponent().leftIcon.color = fullWhite; + // Reshape. + controllerGeometry.modifyOverlay.GetComponent().upIcon.color = halfWhite; + // Extrude. + controllerGeometry.modifyOverlay.GetComponent().rightIcon.color = halfWhite; + break; + case ControllerMode.reshape: + // Subdivide. + controllerGeometry.modifyOverlay.GetComponent().leftIcon.color = halfWhite; + // Reshape. + controllerGeometry.modifyOverlay.GetComponent().upIcon.color = fullWhite; + // Extrude. + controllerGeometry.modifyOverlay.GetComponent().rightIcon.color = halfWhite; + break; + case ControllerMode.extrude: + // Subdivide. + controllerGeometry.modifyOverlay.GetComponent().leftIcon.color = halfWhite; + // Reshape. + controllerGeometry.modifyOverlay.GetComponent().upIcon.color = halfWhite; + // Extrude. + controllerGeometry.modifyOverlay.GetComponent().rightIcon.color = fullWhite; + break; + case ControllerMode.delete: + controllerGeometry.deleteOverlay.GetComponent().leftIcon.gameObject + .SetActive(Features.enablePartDeletion); + controllerGeometry.deleteOverlay.GetComponent().rightIcon.gameObject + .SetActive(Features.enablePartDeletion); + // Delete part. + controllerGeometry.deleteOverlay.GetComponent().rightIcon.color = halfWhite; + // Delete mesh. + controllerGeometry.deleteOverlay.GetComponent().leftIcon.color = fullWhite; + break; + case ControllerMode.deletePart: + controllerGeometry.deleteOverlay.GetComponent().leftIcon.gameObject + .SetActive(Features.enablePartDeletion); + controllerGeometry.deleteOverlay.GetComponent().rightIcon.gameObject + .SetActive(Features.enablePartDeletion); + // Delete part. + controllerGeometry.deleteOverlay.GetComponent().rightIcon.color = fullWhite; + // Delete mesh. + controllerGeometry.deleteOverlay.GetComponent().leftIcon.color = halfWhite; + break; + case ControllerMode.insertStroke: + if (freeformInstance.IsStroking()) + { + // Set the change vertices of the face overlay to be inactive. You can't change verts in the middle of + // a stroke. + controllerGeometry.freeformChangeFaceOverlay.SetActive(false); + if (freeformInstance.IsManualStroking()) + { + // Activate the checkpoint button. + controllerGeometry.freeformOverlay.GetComponent().center.SetActive(true); + } + } + else + { + // Make sure the changeFaceOverlay is on. + controllerGeometry.freeformChangeFaceOverlay.SetActive(true); + controllerGeometry.freeformOverlay.GetComponent().center.SetActive(false); + } + break; + } - /// - /// Change the touchpad overlay to the given type. Will automatically highlight selected - /// modes where appropriate. - /// - /// - public void ChangeTouchpadOverlay(TouchpadOverlay newOverlay) { - currentOverlay = newOverlay; - - // Set the correct parent overlay active based on the passed overlay. Some parent overlays have sub-overlays that - // will be set based on ControllerMode. - controllerGeometry.volumeInserterOverlay.SetActive(currentOverlay == TouchpadOverlay.VOLUME_INSERTER); - controllerGeometry.freeformOverlay.SetActive(currentOverlay == TouchpadOverlay.FREEFORM); - controllerGeometry.paintOverlay.SetActive(currentOverlay == TouchpadOverlay.PAINT); - controllerGeometry.modifyOverlay.SetActive(currentOverlay == TouchpadOverlay.MODIFY); - controllerGeometry.moveOverlay.SetActive(currentOverlay == TouchpadOverlay.MOVE); - if (controllerGeometry.OnMoveOverlay != null) controllerGeometry.OnMoveOverlay.SetActive(currentOverlay == TouchpadOverlay.MOVE); - - controllerGeometry.deleteOverlay.SetActive(currentOverlay == TouchpadOverlay.DELETE); - - controllerGeometry.menuOverlay.SetActive(currentOverlay == TouchpadOverlay.MENU); - if (controllerGeometry.OnMenuOverlay != null) controllerGeometry.OnMenuOverlay.SetActive(currentOverlay == TouchpadOverlay.MENU); - - controllerGeometry.undoRedoOverlay.SetActive(currentOverlay == TouchpadOverlay.UNDO_REDO); - if (controllerGeometry.OnUndoRedoOverlay != null) controllerGeometry.OnUndoRedoOverlay.SetActive(currentOverlay == TouchpadOverlay.UNDO_REDO); - - controllerGeometry.resizeOverlay.SetActive(currentOverlay == TouchpadOverlay.RESIZE); - controllerGeometry.resetZoomOverlay.SetActive(currentOverlay == TouchpadOverlay.RESET_ZOOM); - - // Set the secondary button active if the reset zoom or checkpoint button should be shown. - SetSecondaryButtonOverlay(/*active*/ currentOverlay == TouchpadOverlay.RESET_ZOOM - || (mode == ControllerMode.insertStroke && freeformInstance.IsStroking() && freeformInstance.IsManualStroking())); - - // If the restrictions manager is allowing touchpadHighlighting don't let the peltzerController change any - // icons. - if (PeltzerMain.Instance.restrictionManager.touchpadHighlightingAllowed) { - return; - } - - Color fullWhite = new Color(1f, 1f, 1f, 1f); - Color halfWhite = new Color(1f, 1f, 1f, 0.196f); - - // Color the sub-overlays depending on mode. - switch (mode) { - case ControllerMode.paintMesh: - // The mesh. - controllerGeometry.paintOverlay.GetComponent().leftIcon.color = fullWhite; - // The face. - controllerGeometry.paintOverlay.GetComponent().rightIcon.color = halfWhite; - break; - case ControllerMode.paintFace: - // The mesh. - controllerGeometry.paintOverlay.GetComponent().leftIcon.color = halfWhite; - // The face. - controllerGeometry.paintOverlay.GetComponent().rightIcon.color = fullWhite; - break; - case ControllerMode.subdivideFace: - // Subdivide. - controllerGeometry.modifyOverlay.GetComponent().leftIcon.color = fullWhite; - // Reshape. - controllerGeometry.modifyOverlay.GetComponent().upIcon.color = halfWhite; - // Extrude. - controllerGeometry.modifyOverlay.GetComponent().rightIcon.color = halfWhite; - break; - case ControllerMode.reshape: - // Subdivide. - controllerGeometry.modifyOverlay.GetComponent().leftIcon.color = halfWhite; - // Reshape. - controllerGeometry.modifyOverlay.GetComponent().upIcon.color = fullWhite; - // Extrude. - controllerGeometry.modifyOverlay.GetComponent().rightIcon.color = halfWhite; - break; - case ControllerMode.extrude: - // Subdivide. - controllerGeometry.modifyOverlay.GetComponent().leftIcon.color = halfWhite; - // Reshape. - controllerGeometry.modifyOverlay.GetComponent().upIcon.color = halfWhite; - // Extrude. - controllerGeometry.modifyOverlay.GetComponent().rightIcon.color = fullWhite; - break; - case ControllerMode.delete: - controllerGeometry.deleteOverlay.GetComponent().leftIcon.gameObject - .SetActive(Features.enablePartDeletion); - controllerGeometry.deleteOverlay.GetComponent().rightIcon.gameObject - .SetActive(Features.enablePartDeletion); - // Delete part. - controllerGeometry.deleteOverlay.GetComponent().rightIcon.color = halfWhite; - // Delete mesh. - controllerGeometry.deleteOverlay.GetComponent().leftIcon.color = fullWhite; - break; - case ControllerMode.deletePart: - controllerGeometry.deleteOverlay.GetComponent().leftIcon.gameObject - .SetActive(Features.enablePartDeletion); - controllerGeometry.deleteOverlay.GetComponent().rightIcon.gameObject - .SetActive(Features.enablePartDeletion); - // Delete part. - controllerGeometry.deleteOverlay.GetComponent().rightIcon.color = fullWhite; - // Delete mesh. - controllerGeometry.deleteOverlay.GetComponent().leftIcon.color = halfWhite; - break; - case ControllerMode.insertStroke: - if (freeformInstance.IsStroking()) { - // Set the change vertices of the face overlay to be inactive. You can't change verts in the middle of - // a stroke. - controllerGeometry.freeformChangeFaceOverlay.SetActive(false); - if (freeformInstance.IsManualStroking()) { - // Activate the checkpoint button. - controllerGeometry.freeformOverlay.GetComponent().center.SetActive(true); - } - } else { - // Make sure the changeFaceOverlay is on. - controllerGeometry.freeformChangeFaceOverlay.SetActive(true); - controllerGeometry.freeformOverlay.GetComponent().center.SetActive(false); - } - break; - } - - GameObject currentOverlayGO; - // Get reference to current overlay. - switch (mode) { - case ControllerMode.insertVolume: - case ControllerMode.subtract: - currentOverlayGO = controllerGeometry.volumeInserterOverlay; - break; - case ControllerMode.insertStroke: - currentOverlayGO = controllerGeometry.freeformOverlay; - break; - case ControllerMode.paintFace: - case ControllerMode.paintMesh: - currentOverlayGO = controllerGeometry.paintOverlay; - break; - case ControllerMode.move: - currentOverlayGO = controllerGeometry.moveOverlay; - break; - case ControllerMode.reshape: - case ControllerMode.extrude: - case ControllerMode.subdivideFace: - case ControllerMode.subdivideMesh: - currentOverlayGO = controllerGeometry.modifyOverlay; - break; - case ControllerMode.delete: - case ControllerMode.deletePart: - currentOverlayGO = controllerGeometry.deleteOverlay; - break; - default: - currentOverlayGO = null; - break; - } - - // Set state of hover icon for the current overlay. - overlay = currentOverlayGO.GetComponent(); - } + GameObject currentOverlayGO; + // Get reference to current overlay. + switch (mode) + { + case ControllerMode.insertVolume: + case ControllerMode.subtract: + currentOverlayGO = controllerGeometry.volumeInserterOverlay; + break; + case ControllerMode.insertStroke: + currentOverlayGO = controllerGeometry.freeformOverlay; + break; + case ControllerMode.paintFace: + case ControllerMode.paintMesh: + currentOverlayGO = controllerGeometry.paintOverlay; + break; + case ControllerMode.move: + currentOverlayGO = controllerGeometry.moveOverlay; + break; + case ControllerMode.reshape: + case ControllerMode.extrude: + case ControllerMode.subdivideFace: + case ControllerMode.subdivideMesh: + currentOverlayGO = controllerGeometry.modifyOverlay; + break; + case ControllerMode.delete: + case ControllerMode.deletePart: + currentOverlayGO = controllerGeometry.deleteOverlay; + break; + default: + currentOverlayGO = null; + break; + } - /// - /// Reset the touchpad to its default state. This should probably be used by Actions that want - /// to temporarily set the overlay (i.e. to resize) and are done with their operation. - /// - public void ResetTouchpadOverlay() { - Zoomer zoomer = PeltzerMain.Instance.Zoomer; - if (zoomer != null && zoomer.isMovingWithPeltzerController) { - ChangeTouchpadOverlay(TouchpadOverlay.RESET_ZOOM); - return; - } - - switch (mode) { - case ControllerMode.insertVolume: - case ControllerMode.subtract: - ChangeTouchpadOverlay(TouchpadOverlay.VOLUME_INSERTER); - break; - case ControllerMode.insertStroke: - ChangeTouchpadOverlay(TouchpadOverlay.FREEFORM); - break; - case ControllerMode.reshape: - ChangeTouchpadOverlay(TouchpadOverlay.MODIFY); - break; - case ControllerMode.extrude: - ChangeTouchpadOverlay(TouchpadOverlay.MODIFY); - break; - case ControllerMode.subdivideFace: - ChangeTouchpadOverlay(TouchpadOverlay.MODIFY); - break; - case ControllerMode.subdivideMesh: - ChangeTouchpadOverlay(TouchpadOverlay.MODIFY); - break; - case ControllerMode.deletePart: - ChangeTouchpadOverlay(TouchpadOverlay.DELETE); - break; - case ControllerMode.delete: - ChangeTouchpadOverlay(TouchpadOverlay.DELETE); - break; - case ControllerMode.move: - ChangeTouchpadOverlay(TouchpadOverlay.MOVE); - break; - case ControllerMode.paintMesh: - ChangeTouchpadOverlay(TouchpadOverlay.PAINT); - break; - case ControllerMode.paintFace: - ChangeTouchpadOverlay(TouchpadOverlay.PAINT); - break; - default: - ChangeTouchpadOverlay(TouchpadOverlay.NONE); - break; - } - } + // Set state of hover icon for the current overlay. + overlay = currentOverlayGO.GetComponent(); + } - /// - /// Gets the current velocity of the controller. - /// - /// The current velocity of the controller. - public Vector3 GetVelocity() { - return controller.GetVelocity(); - } + /// + /// Reset the touchpad to its default state. This should probably be used by Actions that want + /// to temporarily set the overlay (i.e. to resize) and are done with their operation. + /// + public void ResetTouchpadOverlay() + { + Zoomer zoomer = PeltzerMain.Instance.Zoomer; + if (zoomer != null && zoomer.isMovingWithPeltzerController) + { + ChangeTouchpadOverlay(TouchpadOverlay.RESET_ZOOM); + return; + } - /// - /// Set the hover state material on the controller. - /// - /// State of the hover state to match. - public void SetTouchpadHoverTexture(TouchpadHoverState state) { - // Only for VIVE currently. - if (Config.Instance.VrHardware == VrHardware.Vive) { - switch (state) { - case TouchpadHoverState.UP: - overlay.upIcon.transform.localScale *= 1.25f; - overlay.upIcon.transform.localPosition = UP_OVERLAY_ICON_HOVER_POSITION; - break; - case TouchpadHoverState.DOWN: - overlay.downIcon.transform.localScale *= 1.25f; - overlay.downIcon.transform.localPosition = DOWN_OVERLAY_ICON_HOVER_POSITION; - break; - case TouchpadHoverState.LEFT: - overlay.leftIcon.transform.localScale *= 1.25f; - overlay.leftIcon.transform.localPosition = LEFT_OVERLAY_ICON_HOVER_POSITION; - break; - case TouchpadHoverState.RIGHT: - overlay.rightIcon.transform.localScale *= 1.25f; - overlay.rightIcon.transform.localPosition = RIGHT_OVERLAY_ICON_HOVER_POSITION; - break; - case TouchpadHoverState.NONE: - // Reset scale to default. - Vector3 resetScale = new Vector3(1.0f, 1.0f, 1.0f); - overlay.upIcon.transform.localScale = resetScale; - overlay.downIcon.transform.localScale = resetScale; - overlay.leftIcon.transform.localScale = resetScale; - overlay.rightIcon.transform.localScale = resetScale; - // Reset positions to default. - overlay.upIcon.transform.localPosition = UP_OVERLAY_ICON_DEFAULT_POSITION_VIVE; - overlay.downIcon.transform.localPosition = DOWN_OVERLAY_ICON_DEFAULT_POSITION_VIVE; - overlay.leftIcon.transform.localPosition = LEFT_OVERLAY_ICON_DEFAULT_POSITION_VIVE; - overlay.rightIcon.transform.localPosition = RIGHT_OVERLAY_ICON_DEFAULT_POSITION_VIVE; - break; + switch (mode) + { + case ControllerMode.insertVolume: + case ControllerMode.subtract: + ChangeTouchpadOverlay(TouchpadOverlay.VOLUME_INSERTER); + break; + case ControllerMode.insertStroke: + ChangeTouchpadOverlay(TouchpadOverlay.FREEFORM); + break; + case ControllerMode.reshape: + ChangeTouchpadOverlay(TouchpadOverlay.MODIFY); + break; + case ControllerMode.extrude: + ChangeTouchpadOverlay(TouchpadOverlay.MODIFY); + break; + case ControllerMode.subdivideFace: + ChangeTouchpadOverlay(TouchpadOverlay.MODIFY); + break; + case ControllerMode.subdivideMesh: + ChangeTouchpadOverlay(TouchpadOverlay.MODIFY); + break; + case ControllerMode.deletePart: + ChangeTouchpadOverlay(TouchpadOverlay.DELETE); + break; + case ControllerMode.delete: + ChangeTouchpadOverlay(TouchpadOverlay.DELETE); + break; + case ControllerMode.move: + ChangeTouchpadOverlay(TouchpadOverlay.MOVE); + break; + case ControllerMode.paintMesh: + ChangeTouchpadOverlay(TouchpadOverlay.PAINT); + break; + case ControllerMode.paintFace: + ChangeTouchpadOverlay(TouchpadOverlay.PAINT); + break; + default: + ChangeTouchpadOverlay(TouchpadOverlay.NONE); + break; + } } - } - } - /// - /// Called when the handedness changes of the controller to accomodate necessary changes. - /// - public void ControllerHandednessChanged() { - if (handedness == Handedness.LEFT) { - controllerGeometry.groupLeftTooltip.SetActive(false); - controllerGeometry.ungroupLeftTooltip.SetActive(false); - controllerGeometry.zoomLeftTooltip.SetActive(false); - controllerGeometry.moveLeftTooltip.SetActive(false); - // Set Move tool hand to LEFT. - grabToolOnPalette.transform.localScale = new Vector3(-1f, 1f, 1f); - if (mode == ControllerMode.move) { - attachedToolHead.transform.localScale = grabToolOnPalette.transform.localScale; + /// + /// Gets the current velocity of the controller. + /// + /// The current velocity of the controller. + public Vector3 GetVelocity() + { + return controller.GetVelocity(); } - } else if (handedness == Handedness.RIGHT) { - controllerGeometry.groupRightTooltip.SetActive(false); - controllerGeometry.ungroupRightTooltip.SetActive(false); - controllerGeometry.zoomRightTooltip.SetActive(false); - controllerGeometry.moveRightTooltip.SetActive(false); - // Set Move tool hand to RIGHT. - grabToolOnPalette.transform.localScale = new Vector3(1f, 1f, 1f); - if (mode == ControllerMode.move) { - attachedToolHead.transform.localScale = grabToolOnPalette.transform.localScale; + + /// + /// Set the hover state material on the controller. + /// + /// State of the hover state to match. + public void SetTouchpadHoverTexture(TouchpadHoverState state) + { + // Only for VIVE currently. + if (Config.Instance.VrHardware == VrHardware.Vive) + { + switch (state) + { + case TouchpadHoverState.UP: + overlay.upIcon.transform.localScale *= 1.25f; + overlay.upIcon.transform.localPosition = UP_OVERLAY_ICON_HOVER_POSITION; + break; + case TouchpadHoverState.DOWN: + overlay.downIcon.transform.localScale *= 1.25f; + overlay.downIcon.transform.localPosition = DOWN_OVERLAY_ICON_HOVER_POSITION; + break; + case TouchpadHoverState.LEFT: + overlay.leftIcon.transform.localScale *= 1.25f; + overlay.leftIcon.transform.localPosition = LEFT_OVERLAY_ICON_HOVER_POSITION; + break; + case TouchpadHoverState.RIGHT: + overlay.rightIcon.transform.localScale *= 1.25f; + overlay.rightIcon.transform.localPosition = RIGHT_OVERLAY_ICON_HOVER_POSITION; + break; + case TouchpadHoverState.NONE: + // Reset scale to default. + Vector3 resetScale = new Vector3(1.0f, 1.0f, 1.0f); + overlay.upIcon.transform.localScale = resetScale; + overlay.downIcon.transform.localScale = resetScale; + overlay.leftIcon.transform.localScale = resetScale; + overlay.rightIcon.transform.localScale = resetScale; + // Reset positions to default. + overlay.upIcon.transform.localPosition = UP_OVERLAY_ICON_DEFAULT_POSITION_VIVE; + overlay.downIcon.transform.localPosition = DOWN_OVERLAY_ICON_DEFAULT_POSITION_VIVE; + overlay.leftIcon.transform.localPosition = LEFT_OVERLAY_ICON_DEFAULT_POSITION_VIVE; + overlay.rightIcon.transform.localPosition = RIGHT_OVERLAY_ICON_DEFAULT_POSITION_VIVE; + break; + } + } } - } - } + /// + /// Called when the handedness changes of the controller to accomodate necessary changes. + /// + public void ControllerHandednessChanged() + { + if (handedness == Handedness.LEFT) + { + controllerGeometry.groupLeftTooltip.SetActive(false); + controllerGeometry.ungroupLeftTooltip.SetActive(false); + controllerGeometry.zoomLeftTooltip.SetActive(false); + controllerGeometry.moveLeftTooltip.SetActive(false); + // Set Move tool hand to LEFT. + grabToolOnPalette.transform.localScale = new Vector3(-1f, 1f, 1f); + if (mode == ControllerMode.move) + { + attachedToolHead.transform.localScale = grabToolOnPalette.transform.localScale; + } + } + else if (handedness == Handedness.RIGHT) + { + controllerGeometry.groupRightTooltip.SetActive(false); + controllerGeometry.ungroupRightTooltip.SetActive(false); + controllerGeometry.zoomRightTooltip.SetActive(false); + controllerGeometry.moveRightTooltip.SetActive(false); + // Set Move tool hand to RIGHT. + grabToolOnPalette.transform.localScale = new Vector3(1f, 1f, 1f); + if (mode == ControllerMode.move) + { + attachedToolHead.transform.localScale = grabToolOnPalette.transform.localScale; + } - /// - /// Determines which tooltip and where to show it when called. These are the grip tooltips to - /// advise a user how to move/zoom the world. - /// We only show these tooltips until the user has successfully moved or zoomed the world. - /// We do not show these tooltips until at least one object is in the scene. - /// We do not show these tooltips during tutorials. - /// - public void SetGripTooltip() { - if (!PaletteController.AcquireIfNecessary(ref PeltzerMain.Instance.paletteController)) return; - - if (PeltzerMain.Instance.Zoomer.userHasEverZoomed - || !PeltzerMain.Instance.restrictionManager.tooltipsAllowed - || PeltzerMain.Instance.HasDisabledTooltips - || PeltzerMain.Instance.tutorialManager.TutorialOccurring()) { - DisableGripTooltips(); - return; - } - - GameObject zoomTooltip = handedness == Handedness.RIGHT ? - controllerGeometry.zoomLeftTooltip : controllerGeometry.zoomRightTooltip; - GameObject moveTooltip = handedness == Handedness.RIGHT ? - controllerGeometry.moveLeftTooltip : controllerGeometry.moveRightTooltip; - - if (controller.IsPressed(ButtonId.Grip) - && PeltzerMain.Instance.paletteController.controller.IsPressed(ButtonId.Grip)) { - zoomTooltip.SetActive(false); - // Stop pulsating glow highlight on grips. - float emission = 0; - Color highlightColor = BASE_COLOR * Mathf.LinearToGammaSpace(emission); - if (controllerGeometry.gripLeft != null) { - controllerGeometry.gripLeft.GetComponent().material.SetColor("_EmissionColor", highlightColor); - } - if (controllerGeometry.gripRight != null) { - controllerGeometry.gripRight.GetComponent().material.SetColor("_EmissionColor", highlightColor); - } - // hide the hold to move tooltip - moveTooltip.SetActive(false); - } else if (controller.IsPressed(ButtonId.Grip) - && !PeltzerMain.Instance.paletteController.controller.IsPressed(ButtonId.Grip)) { - zoomTooltip.SetActive(true); - moveTooltip.SetActive(false); - } else if (!controller.IsPressed(ButtonId.Grip) - && PeltzerMain.Instance.paletteController.controller.IsPressed(ButtonId.Grip)) { - zoomTooltip.SetActive(true); - // Pulsating glow highlight on grips. - float emission = Mathf.PingPong(Time.time / 2F, 0.4f); - Color highlightColor = BASE_COLOR * Mathf.LinearToGammaSpace(emission); - if (controllerGeometry.gripLeft != null) { - controllerGeometry.gripLeft.GetComponent().material.SetColor("_EmissionColor", highlightColor); - } - if (controllerGeometry.gripRight != null) { - controllerGeometry.gripRight.GetComponent().material.SetColor("_EmissionColor", highlightColor); + } } - moveTooltip.SetActive(false); - } else { - zoomTooltip.SetActive(false); - bool showMoveTooltip = !PeltzerMain.Instance.Zoomer.userHasEverMoved - && PeltzerMain.Instance.model.GetNumberOfMeshes() > 0; - moveTooltip.SetActive(showMoveTooltip); - // Stop pulsating glow highlight on grips. - float emission = 0; - Color highlightColor = BASE_COLOR * Mathf.LinearToGammaSpace(emission); - if (controllerGeometry.gripLeft != null) { - controllerGeometry.gripLeft.GetComponent().material.SetColor("_EmissionColor", highlightColor); + + /// + /// Determines which tooltip and where to show it when called. These are the grip tooltips to + /// advise a user how to move/zoom the world. + /// We only show these tooltips until the user has successfully moved or zoomed the world. + /// We do not show these tooltips until at least one object is in the scene. + /// We do not show these tooltips during tutorials. + /// + public void SetGripTooltip() + { + if (!PaletteController.AcquireIfNecessary(ref PeltzerMain.Instance.paletteController)) return; + + if (PeltzerMain.Instance.Zoomer.userHasEverZoomed + || !PeltzerMain.Instance.restrictionManager.tooltipsAllowed + || PeltzerMain.Instance.HasDisabledTooltips + || PeltzerMain.Instance.tutorialManager.TutorialOccurring()) + { + DisableGripTooltips(); + return; + } + + GameObject zoomTooltip = handedness == Handedness.RIGHT ? + controllerGeometry.zoomLeftTooltip : controllerGeometry.zoomRightTooltip; + GameObject moveTooltip = handedness == Handedness.RIGHT ? + controllerGeometry.moveLeftTooltip : controllerGeometry.moveRightTooltip; + + if (controller.IsPressed(ButtonId.Grip) + && PeltzerMain.Instance.paletteController.controller.IsPressed(ButtonId.Grip)) + { + zoomTooltip.SetActive(false); + // Stop pulsating glow highlight on grips. + float emission = 0; + Color highlightColor = BASE_COLOR * Mathf.LinearToGammaSpace(emission); + if (controllerGeometry.gripLeft != null) + { + controllerGeometry.gripLeft.GetComponent().material.SetColor("_EmissionColor", highlightColor); + } + if (controllerGeometry.gripRight != null) + { + controllerGeometry.gripRight.GetComponent().material.SetColor("_EmissionColor", highlightColor); + } + // hide the hold to move tooltip + moveTooltip.SetActive(false); + } + else if (controller.IsPressed(ButtonId.Grip) + && !PeltzerMain.Instance.paletteController.controller.IsPressed(ButtonId.Grip)) + { + zoomTooltip.SetActive(true); + moveTooltip.SetActive(false); + } + else if (!controller.IsPressed(ButtonId.Grip) + && PeltzerMain.Instance.paletteController.controller.IsPressed(ButtonId.Grip)) + { + zoomTooltip.SetActive(true); + // Pulsating glow highlight on grips. + float emission = Mathf.PingPong(Time.time / 2F, 0.4f); + Color highlightColor = BASE_COLOR * Mathf.LinearToGammaSpace(emission); + if (controllerGeometry.gripLeft != null) + { + controllerGeometry.gripLeft.GetComponent().material.SetColor("_EmissionColor", highlightColor); + } + if (controllerGeometry.gripRight != null) + { + controllerGeometry.gripRight.GetComponent().material.SetColor("_EmissionColor", highlightColor); + } + moveTooltip.SetActive(false); + } + else + { + zoomTooltip.SetActive(false); + bool showMoveTooltip = !PeltzerMain.Instance.Zoomer.userHasEverMoved + && PeltzerMain.Instance.model.GetNumberOfMeshes() > 0; + moveTooltip.SetActive(showMoveTooltip); + // Stop pulsating glow highlight on grips. + float emission = 0; + Color highlightColor = BASE_COLOR * Mathf.LinearToGammaSpace(emission); + if (controllerGeometry.gripLeft != null) + { + controllerGeometry.gripLeft.GetComponent().material.SetColor("_EmissionColor", highlightColor); + } + if (controllerGeometry.gripRight != null) + { + controllerGeometry.gripRight.GetComponent().material.SetColor("_EmissionColor", highlightColor); + } + } } - if (controllerGeometry.gripRight != null) { - controllerGeometry.gripRight.GetComponent().material.SetColor("_EmissionColor", highlightColor); + + /// + /// Disables the 'hold to move' and 'hold to zoom' tooltips, and grip-button pulsing. + /// + public void DisableGripTooltips() + { + GameObject zoomTooltip = handedness == Handedness.RIGHT ? + controllerGeometry.zoomLeftTooltip : controllerGeometry.zoomRightTooltip; + GameObject moveTooltip = handedness == Handedness.RIGHT ? + controllerGeometry.moveLeftTooltip : controllerGeometry.moveRightTooltip; + + zoomTooltip.SetActive(false); + moveTooltip.SetActive(false); + // Stop pulsating glow highlight on grips. + float emission = 0; + Color highlightColor = BASE_COLOR * Mathf.LinearToGammaSpace(emission); + if (controllerGeometry.gripLeft != null) + { + controllerGeometry.gripLeft.GetComponent().material.SetColor("_EmissionColor", highlightColor); + } + if (controllerGeometry.gripRight != null) + { + controllerGeometry.gripRight.GetComponent().material.SetColor("_EmissionColor", highlightColor); + } } - } - } - /// - /// Disables the 'hold to move' and 'hold to zoom' tooltips, and grip-button pulsing. - /// - public void DisableGripTooltips() { - GameObject zoomTooltip = handedness == Handedness.RIGHT ? - controllerGeometry.zoomLeftTooltip : controllerGeometry.zoomRightTooltip; - GameObject moveTooltip = handedness == Handedness.RIGHT ? - controllerGeometry.moveLeftTooltip : controllerGeometry.moveRightTooltip; - - zoomTooltip.SetActive(false); - moveTooltip.SetActive(false); - // Stop pulsating glow highlight on grips. - float emission = 0; - Color highlightColor = BASE_COLOR * Mathf.LinearToGammaSpace(emission); - if (controllerGeometry.gripLeft != null) { - controllerGeometry.gripLeft.GetComponent().material.SetColor("_EmissionColor", highlightColor); - } - if (controllerGeometry.gripRight != null) { - controllerGeometry.gripRight.GetComponent().material.SetColor("_EmissionColor", highlightColor); - } + public TouchpadOverlay TouchpadOverlay { get { return currentOverlay; } } } - - public TouchpadOverlay TouchpadOverlay { get { return currentOverlay; } } - } } diff --git a/Assets/Scripts/model/controller/PolyMenuButton.cs b/Assets/Scripts/model/controller/PolyMenuButton.cs index ce855dcd..21293350 100644 --- a/Assets/Scripts/model/controller/PolyMenuButton.cs +++ b/Assets/Scripts/model/controller/PolyMenuButton.cs @@ -14,116 +14,132 @@ using UnityEngine; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// A button on the Poly menu. Specific actions are set in sub-classes, this class handles hover/click UI state. - /// - public class PolyMenuButton : SelectableMenuItem { - private readonly float BUMPED_Y_SCALE_FACTOR = 1.25f; - private readonly float HOVERED_Y_SCALE_FACTOR = 2f; - - private readonly float BUMP_DURATION = 0.1f; - private readonly float HOVER_DURATION = 0.25f; - - private float defaultYPosition; - private float defaultYScale; - - private float hoveredYPosition; - private float hoveredYScale; - - private float bumpedYPosition; - private float bumpedYScale; - - private bool isBumping = false; - public bool isHovered = false; - private Vector3? targetPosition = null; - private Vector3? targetScale = null; - private float timeStartedLerping; - private float currentLerpDuration; - - public void Start() { - defaultYPosition = transform.localPosition.y; - defaultYScale = transform.localScale.y; - - hoveredYScale = defaultYScale * HOVERED_Y_SCALE_FACTOR; - hoveredYPosition = (hoveredYScale - defaultYScale) / 2.0f; - - bumpedYScale = defaultYScale * BUMPED_Y_SCALE_FACTOR; - bumpedYPosition = (bumpedYScale - defaultYScale) / 2.0f; - } - +namespace com.google.apps.peltzer.client.model.controller +{ /// - /// Lerps towards a target position and scale over BUMP_DURATION. If we are bumping, then when the 'bumped' - /// positions and scales are reached, reverts to the 'hovered' positions and scales. + /// A button on the Poly menu. Specific actions are set in sub-classes, this class handles hover/click UI state. /// - void Update() { - if (targetPosition == null || targetScale == null) { - return; - } - - float pctDone = (Time.time - timeStartedLerping) / currentLerpDuration; - - if (pctDone >= 1) { - // If we're done, immediately set the position and scale. - gameObject.transform.localPosition = targetPosition.Value; - gameObject.transform.localScale = targetScale.Value; - if (isBumping) { - // If we're done and we were bumping down, revert to the expected position. - isBumping = false; - SetToDefaultPositionAndScale(); - } else { - // If we're done and were not bumping down, then there's no more lerping to do. - targetPosition = null; - targetScale = null; + public class PolyMenuButton : SelectableMenuItem + { + private readonly float BUMPED_Y_SCALE_FACTOR = 1.25f; + private readonly float HOVERED_Y_SCALE_FACTOR = 2f; + + private readonly float BUMP_DURATION = 0.1f; + private readonly float HOVER_DURATION = 0.25f; + + private float defaultYPosition; + private float defaultYScale; + + private float hoveredYPosition; + private float hoveredYScale; + + private float bumpedYPosition; + private float bumpedYScale; + + private bool isBumping = false; + public bool isHovered = false; + private Vector3? targetPosition = null; + private Vector3? targetScale = null; + private float timeStartedLerping; + private float currentLerpDuration; + + public void Start() + { + defaultYPosition = transform.localPosition.y; + defaultYScale = transform.localScale.y; + + hoveredYScale = defaultYScale * HOVERED_Y_SCALE_FACTOR; + hoveredYPosition = (hoveredYScale - defaultYScale) / 2.0f; + + bumpedYScale = defaultYScale * BUMPED_Y_SCALE_FACTOR; + bumpedYPosition = (bumpedYScale - defaultYScale) / 2.0f; } - } else { - // If we're not done, lerp towards the target position and scale. - gameObject.transform.localPosition = - Vector3.Lerp(gameObject.transform.localPosition, targetPosition.Value, pctDone); - gameObject.transform.localScale = - Vector3.Lerp(gameObject.transform.localScale, targetScale.Value, pctDone); - } - } - /// - /// Briefly 'bump' the menu item to a middle state, before returning it to its default state. - /// Used to visually indicate to the user that a click was received. - /// - internal void StartBump() { - isBumping = true; - StartLerp(bumpedYPosition, bumpedYScale, BUMP_DURATION); - } + /// + /// Lerps towards a target position and scale over BUMP_DURATION. If we are bumping, then when the 'bumped' + /// positions and scales are reached, reverts to the 'hovered' positions and scales. + /// + void Update() + { + if (targetPosition == null || targetScale == null) + { + return; + } + + float pctDone = (Time.time - timeStartedLerping) / currentLerpDuration; + + if (pctDone >= 1) + { + // If we're done, immediately set the position and scale. + gameObject.transform.localPosition = targetPosition.Value; + gameObject.transform.localScale = targetScale.Value; + if (isBumping) + { + // If we're done and we were bumping down, revert to the expected position. + isBumping = false; + SetToDefaultPositionAndScale(); + } + else + { + // If we're done and were not bumping down, then there's no more lerping to do. + targetPosition = null; + targetScale = null; + } + } + else + { + // If we're not done, lerp towards the target position and scale. + gameObject.transform.localPosition = + Vector3.Lerp(gameObject.transform.localPosition, targetPosition.Value, pctDone); + gameObject.transform.localScale = + Vector3.Lerp(gameObject.transform.localScale, targetScale.Value, pctDone); + } + } - /// - /// Allows an external source to set whether this swatch is being hovered. - /// - public void SetHovered(bool isHovered) { - if (this.isHovered == isHovered) { - return; - } - this.isHovered = isHovered; - SetToDefaultPositionAndScale(); - } + /// + /// Briefly 'bump' the menu item to a middle state, before returning it to its default state. + /// Used to visually indicate to the user that a click was received. + /// + internal void StartBump() + { + isBumping = true; + StartLerp(bumpedYPosition, bumpedYScale, BUMP_DURATION); + } - /// - /// Sets the position and scale to their default (non-bump-influenced) states. - /// - private void SetToDefaultPositionAndScale() { - float yPosition = isHovered ? hoveredYPosition : defaultYPosition; - float yScale = isHovered ? hoveredYScale : defaultYScale; - StartLerp(yPosition, yScale, HOVER_DURATION); - } + /// + /// Allows an external source to set whether this swatch is being hovered. + /// + public void SetHovered(bool isHovered) + { + if (this.isHovered == isHovered) + { + return; + } + this.isHovered = isHovered; + SetToDefaultPositionAndScale(); + } - /// - /// Lerps the position and scale to given states over a given duration. - /// - private void StartLerp(float yPosition, float yScale, float duration) { - targetPosition = new Vector3( - gameObject.transform.localPosition.x, yPosition, gameObject.transform.localPosition.z); - targetScale = new Vector3( - gameObject.transform.localScale.x, yScale, gameObject.transform.localScale.z); - currentLerpDuration = duration; - timeStartedLerping = Time.time; + /// + /// Sets the position and scale to their default (non-bump-influenced) states. + /// + private void SetToDefaultPositionAndScale() + { + float yPosition = isHovered ? hoveredYPosition : defaultYPosition; + float yScale = isHovered ? hoveredYScale : defaultYScale; + StartLerp(yPosition, yScale, HOVER_DURATION); + } + + /// + /// Lerps the position and scale to given states over a given duration. + /// + private void StartLerp(float yPosition, float yScale, float duration) + { + targetPosition = new Vector3( + gameObject.transform.localPosition.x, yPosition, gameObject.transform.localPosition.z); + targetScale = new Vector3( + gameObject.transform.localScale.x, yScale, gameObject.transform.localScale.z); + currentLerpDuration = duration; + timeStartedLerping = Time.time; + } } - } } diff --git a/Assets/Scripts/model/controller/SelectZandriaCreationMenuItem.cs b/Assets/Scripts/model/controller/SelectZandriaCreationMenuItem.cs index 0a2db39c..34416178 100644 --- a/Assets/Scripts/model/controller/SelectZandriaCreationMenuItem.cs +++ b/Assets/Scripts/model/controller/SelectZandriaCreationMenuItem.cs @@ -17,17 +17,20 @@ using com.google.apps.peltzer.client.zandria; using System.Collections.Generic; -namespace com.google.apps.peltzer.client.model.controller { +namespace com.google.apps.peltzer.client.model.controller +{ - /// - /// SelectableMenuItem that can be attached to a palette to change the current mode. - /// - public class SelectZandriaCreationMenuItem : SelectableMenuItem { - public List meshes; + /// + /// SelectableMenuItem that can be attached to a palette to change the current mode. + /// + public class SelectZandriaCreationMenuItem : SelectableMenuItem + { + public List meshes; - public override void ApplyMenuOptions(PeltzerMain main) { - // Uncomment to re-enable 'quick grab' if desired. - PeltzerMain.Instance.GetPolyMenuMain().InvokeDetailsMenuAction(menu.PolyMenuMain.DetailsMenuAction.IMPORT); + public override void ApplyMenuOptions(PeltzerMain main) + { + // Uncomment to re-enable 'quick grab' if desired. + PeltzerMain.Instance.GetPolyMenuMain().InvokeDetailsMenuAction(menu.PolyMenuMain.DetailsMenuAction.IMPORT); + } } - } } diff --git a/Assets/Scripts/model/controller/SelectableDetailsMenuItem.cs b/Assets/Scripts/model/controller/SelectableDetailsMenuItem.cs index 25cd06a7..a3d228c6 100644 --- a/Assets/Scripts/model/controller/SelectableDetailsMenuItem.cs +++ b/Assets/Scripts/model/controller/SelectableDetailsMenuItem.cs @@ -15,18 +15,21 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.zandria; -namespace com.google.apps.peltzer.client.model.controller { +namespace com.google.apps.peltzer.client.model.controller +{ - /// - /// SelectableDetailsMenuItem that can be attached to a creation on the PolyMenu and used to open and populate the - /// details section. - /// - public class SelectableDetailsMenuItem : SelectableMenuItem { - public Creation creation; + /// + /// SelectableDetailsMenuItem that can be attached to a creation on the PolyMenu and used to open and populate the + /// details section. + /// + public class SelectableDetailsMenuItem : SelectableMenuItem + { + public Creation creation; - public override void ApplyMenuOptions(PeltzerMain main) { - PeltzerMain.Instance.GetPolyMenuMain().OpenDetailsSection(creation); - main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); + public override void ApplyMenuOptions(PeltzerMain main) + { + PeltzerMain.Instance.GetPolyMenuMain().OpenDetailsSection(creation); + main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/controller/SelectableMenuItem.cs b/Assets/Scripts/model/controller/SelectableMenuItem.cs index 442ec860..02a2d64a 100644 --- a/Assets/Scripts/model/controller/SelectableMenuItem.cs +++ b/Assets/Scripts/model/controller/SelectableMenuItem.cs @@ -15,18 +15,21 @@ using UnityEngine; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.controller { - public class SelectableMenuItem : MonoBehaviour { - // Name to display when this menu item is hovered. Set in Unity editor. - public string hoverName; - public bool isActive = true; +namespace com.google.apps.peltzer.client.model.controller +{ + public class SelectableMenuItem : MonoBehaviour + { + // Name to display when this menu item is hovered. Set in Unity editor. + public string hoverName; + public bool isActive = true; - /// - /// Called whenever a SelectableMenuItem is touched. - /// - /// - public virtual void ApplyMenuOptions(PeltzerMain main) { + /// + /// Called whenever a SelectableMenuItem is touched. + /// + /// + public virtual void ApplyMenuOptions(PeltzerMain main) + { + } } - } } diff --git a/Assets/Scripts/model/controller/SelectablePaginationMenuItem.cs b/Assets/Scripts/model/controller/SelectablePaginationMenuItem.cs index 85af0d34..9c67e30e 100644 --- a/Assets/Scripts/model/controller/SelectablePaginationMenuItem.cs +++ b/Assets/Scripts/model/controller/SelectablePaginationMenuItem.cs @@ -14,23 +14,27 @@ using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.controller { +namespace com.google.apps.peltzer.client.model.controller +{ - /// - /// SelectableMenuItem that can be attached to the PolyMenu to change pages. - /// - public class SelectablePaginationMenuItem : PolyMenuButton { - public int pageChange; + /// + /// SelectableMenuItem that can be attached to the PolyMenu to change pages. + /// + public class SelectablePaginationMenuItem : PolyMenuButton + { + public int pageChange; - public override void ApplyMenuOptions(PeltzerMain main) { - if (isActive) { - main.GetPolyMenuMain().ApplyPageChange(pageChange); - main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); + public override void ApplyMenuOptions(PeltzerMain main) + { + if (isActive) + { + main.GetPolyMenuMain().ApplyPageChange(pageChange); + main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); - // Bump down slightly and back up to its position, to provide a visual indication that - // the user's click was registered. - StartBump(); - } + // Bump down slightly and back up to its position, to provide a visual indication that + // the user's click was registered. + StartBump(); + } + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/controller/SelectablePolyMenuItem.cs b/Assets/Scripts/model/controller/SelectablePolyMenuItem.cs index 6df4177e..7ff8ba5e 100644 --- a/Assets/Scripts/model/controller/SelectablePolyMenuItem.cs +++ b/Assets/Scripts/model/controller/SelectablePolyMenuItem.cs @@ -14,24 +14,27 @@ using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.controller { +namespace com.google.apps.peltzer.client.model.controller +{ - /// - /// SelectableMenuItem that can be attached to a palette to change the current page of the PolyMenu. - /// It is expected that these are configured in the Unity Editor with local: - /// - y position of DEFAULT_Y_POSITION - /// - y scale of DEFAULT_Y_SCALE - /// - public class SelectablePolyMenuItem : PolyMenuButton { - public int polyMenuIndex; + /// + /// SelectableMenuItem that can be attached to a palette to change the current page of the PolyMenu. + /// It is expected that these are configured in the Unity Editor with local: + /// - y position of DEFAULT_Y_POSITION + /// - y scale of DEFAULT_Y_SCALE + /// + public class SelectablePolyMenuItem : PolyMenuButton + { + public int polyMenuIndex; - public override void ApplyMenuOptions(PeltzerMain main) { - main.GetPolyMenuMain().ApplyMenuChange(polyMenuIndex); - main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); + public override void ApplyMenuOptions(PeltzerMain main) + { + main.GetPolyMenuMain().ApplyMenuChange(polyMenuIndex); + main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); - // Bump down slightly and back up to its position, to provide a visual indication that - // the user's click was registered. - StartBump(); + // Bump down slightly and back up to its position, to provide a visual indication that + // the user's click was registered. + StartBump(); + } } - } } diff --git a/Assets/Scripts/model/controller/ShapeToolheadAnimation.cs b/Assets/Scripts/model/controller/ShapeToolheadAnimation.cs index fd0ed708..dad40b3a 100644 --- a/Assets/Scripts/model/controller/ShapeToolheadAnimation.cs +++ b/Assets/Scripts/model/controller/ShapeToolheadAnimation.cs @@ -18,34 +18,42 @@ using com.google.apps.peltzer.client.model.main; using System.Collections.Generic; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// This class deals with the palette icon that shows for the Volume Inserter option. - /// - public class ShapeToolheadAnimation : MonoBehaviour { - // The available shapes. - private Dictionary mockShapes = new Dictionary(); - - void Start() { - mockShapes[Primitives.Shape.CONE] = ObjectFinder.ObjectById("ID_Cone"); - mockShapes[Primitives.Shape.CUBE] = ObjectFinder.ObjectById("ID_Cube"); - mockShapes[Primitives.Shape.CYLINDER] = ObjectFinder.ObjectById("ID_Cylinder"); - mockShapes[Primitives.Shape.TORUS] = ObjectFinder.ObjectById("ID_Torus"); - mockShapes[Primitives.Shape.SPHERE] = ObjectFinder.ObjectById("ID_Sphere"); - } - +namespace com.google.apps.peltzer.client.model.controller +{ /// - /// Handler for shape changed event from the shape menu. + /// This class deals with the palette icon that shows for the Volume Inserter option. /// - /// - public void ShapeChangedHandler(int shapeMenuItemId) { - foreach (KeyValuePair mockShape in mockShapes) { - if (shapeMenuItemId == (int) mockShape.Key) { - mockShape.Value.SetActive(true); - } else { - mockShape.Value.SetActive(false); + public class ShapeToolheadAnimation : MonoBehaviour + { + // The available shapes. + private Dictionary mockShapes = new Dictionary(); + + void Start() + { + mockShapes[Primitives.Shape.CONE] = ObjectFinder.ObjectById("ID_Cone"); + mockShapes[Primitives.Shape.CUBE] = ObjectFinder.ObjectById("ID_Cube"); + mockShapes[Primitives.Shape.CYLINDER] = ObjectFinder.ObjectById("ID_Cylinder"); + mockShapes[Primitives.Shape.TORUS] = ObjectFinder.ObjectById("ID_Torus"); + mockShapes[Primitives.Shape.SPHERE] = ObjectFinder.ObjectById("ID_Sphere"); + } + + /// + /// Handler for shape changed event from the shape menu. + /// + /// + public void ShapeChangedHandler(int shapeMenuItemId) + { + foreach (KeyValuePair mockShape in mockShapes) + { + if (shapeMenuItemId == (int)mockShape.Key) + { + mockShape.Value.SetActive(true); + } + else + { + mockShape.Value.SetActive(false); + } + } } - } } - } } diff --git a/Assets/Scripts/model/controller/ShapesMenu.cs b/Assets/Scripts/model/controller/ShapesMenu.cs index 099d6021..348b1794 100644 --- a/Assets/Scripts/model/controller/ShapesMenu.cs +++ b/Assets/Scripts/model/controller/ShapesMenu.cs @@ -24,415 +24,481 @@ using com.google.apps.peltzer.client.model.util; using com.google.apps.peltzer.client.tools; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Delegate called when the selected shape changes. - /// - /// - public delegate void ShapeMenuItemChangedHandler(int newShapeMenuItemId); - - /// - /// Shapes menu. This menu is the little tray of primitives that appears when the user is in the process of - /// selecting a shape to insert into the scene. It appears when the user is using the VolumeInserter tool. - /// This MonoBehaviour must be added to the object that represents the controller (so that the menu gets - /// anchored to the controller and moves around with it). - /// - public class ShapesMenu : MonoBehaviour { - // The shape menu uses item IDs to identify its items. The IDs coincide with the values of Primitives.Shape - // for the basic primitive types. In addition, we have these "special items" that are not primitives. - // Their IDs are negative in order not to conflict with values in Primitives.Shape. - public const int COPY_MODE_ID = -1; - public const int CUSTOM_SHAPE_ID = -2; - +namespace com.google.apps.peltzer.client.model.controller +{ /// - /// Defines the order in which shapes menu items appear in the menu. - /// We do this instead of using the menu item IDs directly because we want some flexibility to reorder - /// items if we want to, for UX purposes (say, move the "copy" item around, etc). Also, we can use this - /// to add or remove items based on build flags. We generate this list at runtime. + /// Delegate called when the selected shape changes. /// - private static readonly int[] MENU_ITEMS; + /// + public delegate void ShapeMenuItemChangedHandler(int newShapeMenuItemId); /// - /// Reverse dictionary that maps a shapes menu item ID to the its index in SHAPES_MENU_ITEMS. + /// Shapes menu. This menu is the little tray of primitives that appears when the user is in the process of + /// selecting a shape to insert into the scene. It appears when the user is using the VolumeInserter tool. + /// This MonoBehaviour must be added to the object that represents the controller (so that the menu gets + /// anchored to the controller and moves around with it). /// - private static readonly Dictionary INDEX_FOR_ID = new Dictionary(); - - /// - /// Size of the custom shape preview in the shapes menu, in world space. - /// - private const float SHAPES_MENU_CUSTOM_SHAPE_SIZE = 0.02f; - - private static readonly float SHAPE_MENU_ANIMATION_TIME = 0.1f; + public class ShapesMenu : MonoBehaviour + { + // The shape menu uses item IDs to identify its items. The IDs coincide with the values of Primitives.Shape + // for the basic primitive types. In addition, we have these "special items" that are not primitives. + // Their IDs are negative in order not to conflict with values in Primitives.Shape. + public const int COPY_MODE_ID = -1; + public const int CUSTOM_SHAPE_ID = -2; + + /// + /// Defines the order in which shapes menu items appear in the menu. + /// We do this instead of using the menu item IDs directly because we want some flexibility to reorder + /// items if we want to, for UX purposes (say, move the "copy" item around, etc). Also, we can use this + /// to add or remove items based on build flags. We generate this list at runtime. + /// + private static readonly int[] MENU_ITEMS; + + /// + /// Reverse dictionary that maps a shapes menu item ID to the its index in SHAPES_MENU_ITEMS. + /// + private static readonly Dictionary INDEX_FOR_ID = new Dictionary(); + + /// + /// Size of the custom shape preview in the shapes menu, in world space. + /// + private const float SHAPES_MENU_CUSTOM_SHAPE_SIZE = 0.02f; + + private static readonly float SHAPE_MENU_ANIMATION_TIME = 0.1f; + + /// + /// How long the shape menu remains on screen before automatically disappearing. + /// + private static readonly float SHAPE_MENU_SHOW_TIME = 1.5f; + + public bool showingShapeMenu { get; private set; } + private float showShapeMenuTime = -10f; // So we hide on first pass. + private int previousShapeMenuItemId; + + // Dictionary that stores the GameObjects that represent each menu item. + // IMPORTANT: this is not indexed by the menu item ID! This is indexed in the order that the items + // appear in the menu (same as ITEMS). + public GameObject[] shapesMenu; + + public event ShapeMenuItemChangedHandler ShapeMenuItemChangedHandler; + + private int currentItemId = (int)Primitives.Shape.CUBE; + public int CurrentItemId { get { return currentItemId; } } + + private WorldSpace worldSpace; + + /// + /// GameObject that represents the tip of the controller (where the menu will appear). + /// + private GameObject wandTip; + + private MeshRepresentationCache meshRepresentationCache; + + + static ShapesMenu() + { + int pos = 0; + if (Features.stampingEnabled) + { + MENU_ITEMS = new int[Primitives.NUM_SHAPES + 2]; + // The menu starts with "copy" and "custom shape", then come the primitives. + // If we ever want to change the order of these items in the menu, this is the place to do it. + MENU_ITEMS[pos++] = COPY_MODE_ID; + MENU_ITEMS[pos++] = CUSTOM_SHAPE_ID; + } + else + { + MENU_ITEMS = new int[Primitives.NUM_SHAPES]; + } + // Add the primitives. + foreach (Primitives.Shape shape in Enum.GetValues(typeof(Primitives.Shape))) + { + MENU_ITEMS[pos++] = (int)shape; + } + // Build the reverse dictionary. + for (int i = 0; i < MENU_ITEMS.Length; i++) + { + INDEX_FOR_ID[MENU_ITEMS[i]] = i; + } + } - /// - /// How long the shape menu remains on screen before automatically disappearing. - /// - private static readonly float SHAPE_MENU_SHOW_TIME = 1.5f; + /// + /// Initializes this object. Must be called after this behavior is added to the GameObject. + /// + public void Setup(WorldSpace worldSpace, GameObject wandTip, int initialMaterial, + MeshRepresentationCache meshRepresentationCache) + { + this.wandTip = wandTip; + this.worldSpace = worldSpace; + this.meshRepresentationCache = meshRepresentationCache; + GenerateShapesMenu(initialMaterial); + } - public bool showingShapeMenu { get; private set; } - private float showShapeMenuTime = -10f; // So we hide on first pass. - private int previousShapeMenuItemId; + private void GenerateShapesMenu(int material) + { + // We don't want the brush previews to resize with worldspace changes, so we create + // a dummy worldspace that is set for the identity transform. + WorldSpace identityWorldSpace = new WorldSpace(worldSpace.bounds); + + shapesMenu = new GameObject[MENU_ITEMS.Length]; + + // For each item on the menu, create a GameObject to represent it. + for (int i = 0; i < shapesMenu.Length; i++) + { + int id = MENU_ITEMS[i]; + GameObject obj; + if (id == COPY_MODE_ID) + { + obj = MeshHelper.GameObjectFromMMesh(identityWorldSpace, + Primitives.AxisAlignedCone(0, Vector3.zero, Vector3.one * 0.0125f, MaterialRegistry.YELLOW_ID)); + } + else if (id == CUSTOM_SHAPE_ID) + { + // We initially don't set a GameObject for CUSTOM_SHAPE_ID. We will only do that once the user configures + // a custom shape via the copy tool. + continue; + } + else if (IsBasicPrimitiveItem(id)) + { + obj = MeshHelper.GameObjectFromMMesh(worldSpace, Primitives.BuildPrimitive((Primitives.Shape)id, + Vector3.one * 0.0125f, Vector3.zero, 0, material)); + obj.gameObject.transform.rotation = PeltzerMain.Instance.peltzerController.LastRotationWorld; + } + else + { + throw new Exception("Invalid menu item ID " + id); + } + + obj.name = "ShapesMenuItem " + i; + // Set up the GameObject to show up in the menu. We will make it a child of our gameObject (the controller) + // so that it moves around with the controller. + obj.transform.parent = gameObject.transform; + obj.transform.localRotation = Quaternion.identity; + // Default shapes smaller for the menu. + obj.transform.localScale /= 1.6f; + MeshWithMaterialRenderer meshRenderer = obj.GetComponent(); + meshRenderer.ResetTransform(); + // The menu exists in world space, not model space, so indicate that MeshWithMaterialRenderer should + // use the object's world position to render, not a model space position. + meshRenderer.UseGameObjectPosition = true; + // Should ignore worldSpace rotation, so as to always give the user the same view of the menu. + meshRenderer.IgnoreWorldRotation = true; + // And, likewise, should ignore worldSpace scale, so as to always give the user the same view of the menu. + meshRenderer.IgnoreWorldScale = true; + // Make the item active or inactive as appropriate. + obj.SetActive(IsShapeMenuItemEnabled(id)); + // And finally add it to the menu array. + shapesMenu[i] = obj; + } - // Dictionary that stores the GameObjects that represent each menu item. - // IMPORTANT: this is not indexed by the menu item ID! This is indexed in the order that the items - // appear in the menu (same as ITEMS). - public GameObject[] shapesMenu; + // Put everything in its default position. + UpdateShapesMenu(); + Hide(); + } - public event ShapeMenuItemChangedHandler ShapeMenuItemChangedHandler; + /// + /// Returns whether or not the given shape menu item corresponds to a basic primitive type. + /// + /// The item. + /// True if and only if the menu item corresponds to a basic primitive. + public bool IsBasicPrimitiveItem(int itemId) + { + return itemId >= 0 && itemId < Primitives.NUM_SHAPES; + } - private int currentItemId = (int)Primitives.Shape.CUBE; - public int CurrentItemId { get { return currentItemId; } } + /// + /// Updates the material of the shapes menu. + /// + /// ID of the new material to use. + public void ChangeShapesMenuMaterial(int newMaterialId) + { + for (int i = 0; i < shapesMenu.Count(); i++) + { + // Only update the material on the items that represent basic primitives. + if (IsBasicPrimitiveItem(MENU_ITEMS[i])) + { + shapesMenu[i].GetComponent().OverrideWithNewMaterial(newMaterialId); + } + } + } - private WorldSpace worldSpace; + /// + /// Sets the custom primitive to display in the shapes menu. + /// + /// The meshes that constitute the custom primitive. + public void SetShapesMenuCustomShape(IEnumerable meshes) + { + AssertOrThrow.True(meshes.Count() > 0, "Can't set a custom shape with an empty list of meshes."); + // First destroy the previous object hierarchy, if any. + if (null != shapesMenu[INDEX_FOR_ID[CUSTOM_SHAPE_ID]]) + { + GameObject.DestroyImmediate(shapesMenu[INDEX_FOR_ID[CUSTOM_SHAPE_ID]]); + } + // Now we generate the preview for the custom shapes. + GameObject preview = GenerateCustomShapePreview(meshes); + // The preview starts out inactive. We will activate it later when the menu gets shown. + preview.SetActive(false); + shapesMenu[INDEX_FOR_ID[CUSTOM_SHAPE_ID]] = preview; + UpdateShapesMenu(); + } - /// - /// GameObject that represents the tip of the controller (where the menu will appear). - /// - private GameObject wandTip; - - private MeshRepresentationCache meshRepresentationCache; - - - static ShapesMenu() { - int pos = 0; - if (Features.stampingEnabled) { - MENU_ITEMS = new int[Primitives.NUM_SHAPES + 2]; - // The menu starts with "copy" and "custom shape", then come the primitives. - // If we ever want to change the order of these items in the menu, this is the place to do it. - MENU_ITEMS[pos++] = COPY_MODE_ID; - MENU_ITEMS[pos++] = CUSTOM_SHAPE_ID; - } else { - MENU_ITEMS = new int[Primitives.NUM_SHAPES]; - } - // Add the primitives. - foreach (Primitives.Shape shape in Enum.GetValues(typeof(Primitives.Shape))) { - MENU_ITEMS[pos++] = (int)shape; - } - // Build the reverse dictionary. - for (int i = 0; i < MENU_ITEMS.Length; i++) { - INDEX_FOR_ID[MENU_ITEMS[i]] = i; - } - } + /// + /// Generates previews for the given meshes, configured to be displayed in the shapes menu. + /// + /// The meshes to use for the preview + /// A GameObject hierarchy that contains all the previews that represent the given list of meshes. + /// These will be set up such that their local positions represent the positions of the mesh, scaled such + /// that the whole set of previews doesn't exceed the maximum size that would fit in the menu. + /// The returned GameObject will be parented to this behavior's GameObject. + private GameObject GenerateCustomShapePreview(IEnumerable meshes) + { + // We will use a "specially constructed" (a.k.a. "hacky") worldspace to ensure that the custom shape preview + // has the right size for the menu. + IEnumerator enumerator = meshes.GetEnumerator(); + AssertOrThrow.True(enumerator.MoveNext(), "Can't generate custom shape preview with no meshes."); + WorldSpace customWorldSpace = new WorldSpace(worldSpace.bounds); + GameObject preview = new GameObject(); + Bounds bounds = enumerator.Current.bounds; + do + { + bounds.Encapsulate(enumerator.Current.bounds); + } while (enumerator.MoveNext()); + + // Now we need to scale this preview such that it's the right size for the menu, + // instead of using its natural size. + // Let's figure out how big the bounding box is in world space. + // + // Note: in the future, we should figure out if/how rotation matters here. We're ignoring it for now: + float maxSideInWorldSpace = + Mathf.Max(bounds.size.x, bounds.size.y, bounds.size.z) * worldSpace.scale; + + // We want the bounding box's size to be about SHAPE_MENU_CUSTOM_SHAPE_SIZE, so let's calculate the scale + // factor that makes this true. + // If the size is too small (< 0.001f) we don't attempt to rescale, to avoid the risk of insanity when + // dividing by something that close to zero. + float scaleFactor = (maxSideInWorldSpace > 0.001f) ? SHAPES_MENU_CUSTOM_SHAPE_SIZE / maxSideInWorldSpace : 1.0f; + customWorldSpace.scale = scaleFactor; + + // Now that we know the scale factor and how much to translate each preview, let's go through them and set + // them up. + foreach (MMesh mesh in meshes) + { + GameObject thisPreview = meshRepresentationCache.GeneratePreview(mesh); + MeshWithMaterialRenderer renderer = thisPreview.GetComponent(); + renderer.worldSpace = customWorldSpace; + thisPreview.transform.SetParent(preview.transform, /* worldPositionStays */ true); + thisPreview.transform.localRotation = Quaternion.identity; + // Position this preview such that its local position is the offset to the bounding box's center + // and scaled such that it fits in the menu. + thisPreview.transform.localPosition = (mesh.offset - bounds.center) * scaleFactor; + } - /// - /// Initializes this object. Must be called after this behavior is added to the GameObject. - /// - public void Setup(WorldSpace worldSpace, GameObject wandTip, int initialMaterial, - MeshRepresentationCache meshRepresentationCache) { - this.wandTip = wandTip; - this.worldSpace = worldSpace; - this.meshRepresentationCache = meshRepresentationCache; - GenerateShapesMenu(initialMaterial); - } + // The preview should be parented to our gameObject (the controller) so that the preview follows + // the controller (as it's part of the shapes menu). + // We pass worldPositionStays=false because we want the object to be repositioned such that it + // lies its correct position in the new parent. + preview.transform.SetParent(gameObject.transform, /* worldPositionStays */ false); - private void GenerateShapesMenu(int material) { - // We don't want the brush previews to resize with worldspace changes, so we create - // a dummy worldspace that is set for the identity transform. - WorldSpace identityWorldSpace = new WorldSpace(worldSpace.bounds); - - shapesMenu = new GameObject[MENU_ITEMS.Length]; - - // For each item on the menu, create a GameObject to represent it. - for (int i = 0; i < shapesMenu.Length; i++) { - int id = MENU_ITEMS[i]; - GameObject obj; - if (id == COPY_MODE_ID) { - obj = MeshHelper.GameObjectFromMMesh(identityWorldSpace, - Primitives.AxisAlignedCone(0, Vector3.zero, Vector3.one * 0.0125f, MaterialRegistry.YELLOW_ID)); - } else if (id == CUSTOM_SHAPE_ID) { - // We initially don't set a GameObject for CUSTOM_SHAPE_ID. We will only do that once the user configures - // a custom shape via the copy tool. - continue; - } else if (IsBasicPrimitiveItem(id)) { - obj = MeshHelper.GameObjectFromMMesh(worldSpace, Primitives.BuildPrimitive((Primitives.Shape)id, - Vector3.one * 0.0125f, Vector3.zero, 0, material)); - obj.gameObject.transform.rotation = PeltzerMain.Instance.peltzerController.LastRotationWorld; - } else { - throw new Exception("Invalid menu item ID " + id); + return preview; } - obj.name = "ShapesMenuItem " + i; - // Set up the GameObject to show up in the menu. We will make it a child of our gameObject (the controller) - // so that it moves around with the controller. - obj.transform.parent = gameObject.transform; - obj.transform.localRotation = Quaternion.identity; - // Default shapes smaller for the menu. - obj.transform.localScale /= 1.6f; - MeshWithMaterialRenderer meshRenderer = obj.GetComponent(); - meshRenderer.ResetTransform(); - // The menu exists in world space, not model space, so indicate that MeshWithMaterialRenderer should - // use the object's world position to render, not a model space position. - meshRenderer.UseGameObjectPosition = true; - // Should ignore worldSpace rotation, so as to always give the user the same view of the menu. - meshRenderer.IgnoreWorldRotation = true; - // And, likewise, should ignore worldSpace scale, so as to always give the user the same view of the menu. - meshRenderer.IgnoreWorldScale = true; - // Make the item active or inactive as appropriate. - obj.SetActive(IsShapeMenuItemEnabled(id)); - // And finally add it to the menu array. - shapesMenu[i] = obj; - } - - // Put everything in its default position. - UpdateShapesMenu(); - Hide(); - } - - /// - /// Returns whether or not the given shape menu item corresponds to a basic primitive type. - /// - /// The item. - /// True if and only if the menu item corresponds to a basic primitive. - public bool IsBasicPrimitiveItem(int itemId) { - return itemId >= 0 && itemId < Primitives.NUM_SHAPES; - } + private void Update() + { + if (showingShapeMenu) + { + if (Time.time > (showShapeMenuTime + SHAPE_MENU_SHOW_TIME)) + { + // Done showing menu, hide everything. + Hide(); + } + else if (Time.time < (showShapeMenuTime + SHAPE_MENU_ANIMATION_TIME)) + { + UpdateShapesMenu(); + for (int i = 0; i < shapesMenu.Count(); i++) + { + int id = MENU_ITEMS[i]; + if (shapesMenu[i] != null) + { + shapesMenu[i].SetActive(IsShapeMenuItemEnabled(id) && id != currentItemId); + } + } + } + } - /// - /// Updates the material of the shapes menu. - /// - /// ID of the new material to use. - public void ChangeShapesMenuMaterial(int newMaterialId) { - for (int i = 0; i < shapesMenu.Count(); i++) { - // Only update the material on the items that represent basic primitives. - if (IsBasicPrimitiveItem(MENU_ITEMS[i])) { - shapesMenu[i].GetComponent().OverrideWithNewMaterial(newMaterialId); } - } - } - /// - /// Sets the custom primitive to display in the shapes menu. - /// - /// The meshes that constitute the custom primitive. - public void SetShapesMenuCustomShape(IEnumerable meshes) { - AssertOrThrow.True(meshes.Count() > 0, "Can't set a custom shape with an empty list of meshes."); - // First destroy the previous object hierarchy, if any. - if (null != shapesMenu[INDEX_FOR_ID[CUSTOM_SHAPE_ID]]) { - GameObject.DestroyImmediate(shapesMenu[INDEX_FOR_ID[CUSTOM_SHAPE_ID]]); - } - // Now we generate the preview for the custom shapes. - GameObject preview = GenerateCustomShapePreview(meshes); - // The preview starts out inactive. We will activate it later when the menu gets shown. - preview.SetActive(false); - shapesMenu[INDEX_FOR_ID[CUSTOM_SHAPE_ID]] = preview; - UpdateShapesMenu(); - } + public void SetShapeMenuItem(int newItemId, bool showMenu) + { + if (newItemId == currentItemId) return; + + // Lerp the previously-selected shape's scale down from what was showing in Volume Inserter. + if (!showingShapeMenu) + { + VolumeInserter volumeInserter = PeltzerMain.Instance.GetVolumeInserter(); + float oldScale = volumeInserter.GetScaleForScaleDelta(volumeInserter.scaleDelta); + float newScale = volumeInserter.GetScaleForScaleDelta(0) / worldSpace.scale; + float scaleDiff = oldScale / newScale; + shapesMenu[INDEX_FOR_ID[currentItemId]].GetComponent().AnimateScaleFrom(scaleDiff); + } - /// - /// Generates previews for the given meshes, configured to be displayed in the shapes menu. - /// - /// The meshes to use for the preview - /// A GameObject hierarchy that contains all the previews that represent the given list of meshes. - /// These will be set up such that their local positions represent the positions of the mesh, scaled such - /// that the whole set of previews doesn't exceed the maximum size that would fit in the menu. - /// The returned GameObject will be parented to this behavior's GameObject. - private GameObject GenerateCustomShapePreview(IEnumerable meshes) { - // We will use a "specially constructed" (a.k.a. "hacky") worldspace to ensure that the custom shape preview - // has the right size for the menu. - IEnumerator enumerator = meshes.GetEnumerator(); - AssertOrThrow.True(enumerator.MoveNext(), "Can't generate custom shape preview with no meshes."); - WorldSpace customWorldSpace = new WorldSpace(worldSpace.bounds); - GameObject preview = new GameObject(); - Bounds bounds = enumerator.Current.bounds; - do { - bounds.Encapsulate(enumerator.Current.bounds); - } while (enumerator.MoveNext()); - - // Now we need to scale this preview such that it's the right size for the menu, - // instead of using its natural size. - // Let's figure out how big the bounding box is in world space. - // - // Note: in the future, we should figure out if/how rotation matters here. We're ignoring it for now: - float maxSideInWorldSpace = - Mathf.Max(bounds.size.x, bounds.size.y, bounds.size.z) * worldSpace.scale; - - // We want the bounding box's size to be about SHAPE_MENU_CUSTOM_SHAPE_SIZE, so let's calculate the scale - // factor that makes this true. - // If the size is too small (< 0.001f) we don't attempt to rescale, to avoid the risk of insanity when - // dividing by something that close to zero. - float scaleFactor = (maxSideInWorldSpace > 0.001f) ? SHAPES_MENU_CUSTOM_SHAPE_SIZE / maxSideInWorldSpace : 1.0f; - customWorldSpace.scale = scaleFactor; - - // Now that we know the scale factor and how much to translate each preview, let's go through them and set - // them up. - foreach (MMesh mesh in meshes) { - GameObject thisPreview = meshRepresentationCache.GeneratePreview(mesh); - MeshWithMaterialRenderer renderer = thisPreview.GetComponent(); - renderer.worldSpace = customWorldSpace; - thisPreview.transform.SetParent(preview.transform, /* worldPositionStays */ true); - thisPreview.transform.localRotation = Quaternion.identity; - // Position this preview such that its local position is the offset to the bounding box's center - // and scaled such that it fits in the menu. - thisPreview.transform.localPosition = (mesh.offset - bounds.center) * scaleFactor; - } - - // The preview should be parented to our gameObject (the controller) so that the preview follows - // the controller (as it's part of the shapes menu). - // We pass worldPositionStays=false because we want the object to be repositioned such that it - // lies its correct position in the new parent. - preview.transform.SetParent(gameObject.transform, /* worldPositionStays */ false); - - return preview; - } + currentItemId = newItemId; - private void Update() { - if (showingShapeMenu) { - if (Time.time > (showShapeMenuTime + SHAPE_MENU_SHOW_TIME)) { - // Done showing menu, hide everything. - Hide(); - } else if (Time.time < (showShapeMenuTime + SHAPE_MENU_ANIMATION_TIME)) { - UpdateShapesMenu(); - for (int i = 0; i < shapesMenu.Count(); i++) { - int id = MENU_ITEMS[i]; - if (shapesMenu[i] != null) { - shapesMenu[i].SetActive(IsShapeMenuItemEnabled(id) && id != currentItemId); + if (showMenu) + { + showingShapeMenu = true; + showShapeMenuTime = Time.time; + } + else + { + showingShapeMenu = false; } - } - } - } - } + UpdateShapesMenu(); - public void SetShapeMenuItem(int newItemId, bool showMenu) { - if (newItemId == currentItemId) return; - - // Lerp the previously-selected shape's scale down from what was showing in Volume Inserter. - if (!showingShapeMenu) { - VolumeInserter volumeInserter = PeltzerMain.Instance.GetVolumeInserter(); - float oldScale = volumeInserter.GetScaleForScaleDelta(volumeInserter.scaleDelta); - float newScale = volumeInserter.GetScaleForScaleDelta(0) / worldSpace.scale; - float scaleDiff = oldScale / newScale; - shapesMenu[INDEX_FOR_ID[currentItemId]].GetComponent().AnimateScaleFrom(scaleDiff); - } - - currentItemId = newItemId; - - if (showMenu) { - showingShapeMenu = true; - showShapeMenuTime = Time.time; - } else { - showingShapeMenu = false; - } - - UpdateShapesMenu(); - - // Notify listeners. - if (ShapeMenuItemChangedHandler != null) { - ShapeMenuItemChangedHandler(newItemId); - } - } + // Notify listeners. + if (ShapeMenuItemChangedHandler != null) + { + ShapeMenuItemChangedHandler(newItemId); + } + } - /// - /// Returns whether the given shape menu item is enabled. Enabled items show in the menu and are - /// selectable. - /// - /// The item to check. - /// True if the item is enabled, false if not. - public bool IsShapeMenuItemEnabled(int itemId) { - // If stamping is not enabled, then the only items enabled are the basic primitives. - if (IsBasicPrimitiveItem(itemId)) { - // Basic primitives are always enabled. - return true; - } else if (Features.stampingEnabled && itemId == COPY_MODE_ID) { - // Copy mode is always enabled (if stamping is enabled in the build). - return true; - } else if (Features.stampingEnabled && itemId == CUSTOM_SHAPE_ID) { - // The "custom shape" mode is enabled if there is a custom shape. - return HasCustomShape(); - } - return false; - } + /// + /// Returns whether the given shape menu item is enabled. Enabled items show in the menu and are + /// selectable. + /// + /// The item to check. + /// True if the item is enabled, false if not. + public bool IsShapeMenuItemEnabled(int itemId) + { + // If stamping is not enabled, then the only items enabled are the basic primitives. + if (IsBasicPrimitiveItem(itemId)) + { + // Basic primitives are always enabled. + return true; + } + else if (Features.stampingEnabled && itemId == COPY_MODE_ID) + { + // Copy mode is always enabled (if stamping is enabled in the build). + return true; + } + else if (Features.stampingEnabled && itemId == CUSTOM_SHAPE_ID) + { + // The "custom shape" mode is enabled if there is a custom shape. + return HasCustomShape(); + } + return false; + } - private bool HasCustomShape() { - return null != shapesMenu[INDEX_FOR_ID[CUSTOM_SHAPE_ID]]; - } + private bool HasCustomShape() + { + return null != shapesMenu[INDEX_FOR_ID[CUSTOM_SHAPE_ID]]; + } - /// - /// Selects the next enabled item in the shapes menu. - /// - /// True if successful, false on failure (there are no more enabled items in that direction). - public bool SelectNextShapesMenuItem() { - int index = INDEX_FOR_ID[currentItemId]; - do { - index++; - if (index >= shapesMenu.Length) { - showingShapeMenu = true; - showShapeMenuTime = Time.time; - return false; + /// + /// Selects the next enabled item in the shapes menu. + /// + /// True if successful, false on failure (there are no more enabled items in that direction). + public bool SelectNextShapesMenuItem() + { + int index = INDEX_FOR_ID[currentItemId]; + do + { + index++; + if (index >= shapesMenu.Length) + { + showingShapeMenu = true; + showShapeMenuTime = Time.time; + return false; + } + } while (!IsShapeMenuItemEnabled(MENU_ITEMS[index])); + SetShapeMenuItem(MENU_ITEMS[index], /* showMenu */ true); + return true; } - } while (!IsShapeMenuItemEnabled(MENU_ITEMS[index])); - SetShapeMenuItem(MENU_ITEMS[index], /* showMenu */ true); - return true; - } - /// - /// Selects the previous enabled item in the shapes menu. - /// - /// True if successful, false on failure (there are no more enabled items in that direction). - public bool SelectPreviousShapesMenuItem() { - int index = INDEX_FOR_ID[currentItemId]; - do { - index--; - if (index < 0) { - showingShapeMenu = true; - showShapeMenuTime = Time.time; - return false; + /// + /// Selects the previous enabled item in the shapes menu. + /// + /// True if successful, false on failure (there are no more enabled items in that direction). + public bool SelectPreviousShapesMenuItem() + { + int index = INDEX_FOR_ID[currentItemId]; + do + { + index--; + if (index < 0) + { + showingShapeMenu = true; + showShapeMenuTime = Time.time; + return false; + } + } while (!IsShapeMenuItemEnabled(MENU_ITEMS[index])); + SetShapeMenuItem(MENU_ITEMS[index], /* showMenu */ true); + return true; } - } while (!IsShapeMenuItemEnabled(MENU_ITEMS[index])); - SetShapeMenuItem(MENU_ITEMS[index], /* showMenu */ true); - return true; - } - /// - /// Returns the order of the given menu item index amongst ENABLED menu items. - /// For example, if items 0 and 1 are disabled, then item 2 would have and order of 0, - /// item 3 would have an order of 1, etc. - /// - /// The item to check. - /// The order of the item amongst the enabled menu items. - private int GetOrderAmongEnabledItems(int index) { - int order = 0; - // The index amongst ENABLED items is simply the count of how many enabled items - // exist before it. - for (int i = 0; i < index; i++) { - if (IsShapeMenuItemEnabled(MENU_ITEMS[i])) order++; - } - return order; - } + /// + /// Returns the order of the given menu item index amongst ENABLED menu items. + /// For example, if items 0 and 1 are disabled, then item 2 would have and order of 0, + /// item 3 would have an order of 1, etc. + /// + /// The item to check. + /// The order of the item amongst the enabled menu items. + private int GetOrderAmongEnabledItems(int index) + { + int order = 0; + // The index amongst ENABLED items is simply the count of how many enabled items + // exist before it. + for (int i = 0; i < index; i++) + { + if (IsShapeMenuItemEnabled(MENU_ITEMS[i])) order++; + } + return order; + } - /// - /// Updates the shapes menu (to be called once per frame). - /// - private void UpdateShapesMenu() { - // Show the menu around the brush primitive. - int curOrder = GetOrderAmongEnabledItems(INDEX_FOR_ID[currentItemId]); - for (int i = 0; i < shapesMenu.Length; i++) { - if (shapesMenu[i] == null) continue; - int thisOrder = GetOrderAmongEnabledItems(i); - float xOff = (thisOrder - curOrder) * 0.06f; - if (Config.Instance.VrHardware == VrHardware.Vive) { - shapesMenu[i].transform.localPosition = wandTip.transform.localPosition + new Vector3(xOff, 0, 0.1f); - } else { - if (Config.Instance.sdkMode == SdkMode.SteamVR) { - shapesMenu[i].transform.localPosition = wandTip.transform.localPosition + new Vector3(xOff, -.075f, 0); - } else { - shapesMenu[i].transform.localPosition = wandTip.transform.localPosition + new Vector3(xOff, 0, 0.1f); - } + /// + /// Updates the shapes menu (to be called once per frame). + /// + private void UpdateShapesMenu() + { + // Show the menu around the brush primitive. + int curOrder = GetOrderAmongEnabledItems(INDEX_FOR_ID[currentItemId]); + for (int i = 0; i < shapesMenu.Length; i++) + { + if (shapesMenu[i] == null) continue; + int thisOrder = GetOrderAmongEnabledItems(i); + float xOff = (thisOrder - curOrder) * 0.06f; + if (Config.Instance.VrHardware == VrHardware.Vive) + { + shapesMenu[i].transform.localPosition = wandTip.transform.localPosition + new Vector3(xOff, 0, 0.1f); + } + else + { + if (Config.Instance.sdkMode == SdkMode.SteamVR) + { + shapesMenu[i].transform.localPosition = wandTip.transform.localPosition + new Vector3(xOff, -.075f, 0); + } + else + { + shapesMenu[i].transform.localPosition = wandTip.transform.localPosition + new Vector3(xOff, 0, 0.1f); + } + } + shapesMenu[i].transform.rotation = PeltzerMain.Instance.peltzerController.LastRotationWorld; + } } - shapesMenu[i].transform.rotation = PeltzerMain.Instance.peltzerController.LastRotationWorld; - } - } - /// - /// Hides the shapes menu, if it is currently showing. - /// - public void Hide() { - showingShapeMenu = false; - for (int i = 0; i < shapesMenu.Length; i++) { - if (shapesMenu[i] != null) { - shapesMenu[i].SetActive(false); + /// + /// Hides the shapes menu, if it is currently showing. + /// + public void Hide() + { + showingShapeMenu = false; + for (int i = 0; i < shapesMenu.Length; i++) + { + if (shapesMenu[i] != null) + { + shapesMenu[i].SetActive(false); + } + } } - } } - } } diff --git a/Assets/Scripts/model/controller/StickyRotationSnapper.cs b/Assets/Scripts/model/controller/StickyRotationSnapper.cs index f88a7402..e1a80c17 100644 --- a/Assets/Scripts/model/controller/StickyRotationSnapper.cs +++ b/Assets/Scripts/model/controller/StickyRotationSnapper.cs @@ -16,34 +16,39 @@ using System.Collections.Generic; using com.google.apps.peltzer.client.tools.utils; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Snaps a rotation to 90 degree angles. - /// Rather than just snap to the closest 90 degree angle (for which a helper class wouldn't be needed at all), - /// this class maintains state to make the current snapped angle "sticky", requiring the user to actually - /// rotate significantly away from the current snapped angle in order to snap to the next one. This makes the - /// snapping feel more stable, as there is no "border region" where snapping would keep changing constantly - /// as would be the case with plain snapping. - /// - public class StickyRotationSnapper { - // How much of an angle the user has to rotate away from the current snapped rotation in order to - // change the snapped rotation. This controls how "sticky" the snapping is. - private const float ANGLE_THRESHOLD = 55.0f; +namespace com.google.apps.peltzer.client.model.controller +{ + /// + /// Snaps a rotation to 90 degree angles. + /// Rather than just snap to the closest 90 degree angle (for which a helper class wouldn't be needed at all), + /// this class maintains state to make the current snapped angle "sticky", requiring the user to actually + /// rotate significantly away from the current snapped angle in order to snap to the next one. This makes the + /// snapping feel more stable, as there is no "border region" where snapping would keep changing constantly + /// as would be the case with plain snapping. + /// + public class StickyRotationSnapper + { + // How much of an angle the user has to rotate away from the current snapped rotation in order to + // change the snapped rotation. This controls how "sticky" the snapping is. + private const float ANGLE_THRESHOLD = 55.0f; - public Quaternion snappedRotation { get; private set; } - public Quaternion unsnappedRotation { get; private set; } + public Quaternion snappedRotation { get; private set; } + public Quaternion unsnappedRotation { get; private set; } - public StickyRotationSnapper(Quaternion initialRotation) { - unsnappedRotation = initialRotation; - snappedRotation = GridUtils.SnapToNearest(unsnappedRotation, Quaternion.identity, 90f); - } + public StickyRotationSnapper(Quaternion initialRotation) + { + unsnappedRotation = initialRotation; + snappedRotation = GridUtils.SnapToNearest(unsnappedRotation, Quaternion.identity, 90f); + } - public Quaternion UpdateRotation(Quaternion currentRotation) { - unsnappedRotation = currentRotation; - if (Quaternion.Angle(snappedRotation, unsnappedRotation) >= ANGLE_THRESHOLD) { - snappedRotation = GridUtils.SnapToNearest(unsnappedRotation, Quaternion.identity, 90f); - } - return snappedRotation; + public Quaternion UpdateRotation(Quaternion currentRotation) + { + unsnappedRotation = currentRotation; + if (Quaternion.Angle(snappedRotation, unsnappedRotation) >= ANGLE_THRESHOLD) + { + snappedRotation = GridUtils.SnapToNearest(unsnappedRotation, Quaternion.identity, 90f); + } + return snappedRotation; + } } - } } diff --git a/Assets/Scripts/model/controller/ToastController.cs b/Assets/Scripts/model/controller/ToastController.cs index 08754f43..015fb089 100644 --- a/Assets/Scripts/model/controller/ToastController.cs +++ b/Assets/Scripts/model/controller/ToastController.cs @@ -16,62 +16,71 @@ using UnityEngine.UI; using System.Collections; -namespace com.google.apps.peltzer.client.model.controller { - public class ToastController : MonoBehaviour { - private static float TOTAL_TOAST_TIME_S = 5f; - private static float TOAST_APEX_Z_LOCATION = .2f; - private static float TOAST_VELOCITY = .1f; +namespace com.google.apps.peltzer.client.model.controller +{ + public class ToastController : MonoBehaviour + { + private static float TOTAL_TOAST_TIME_S = 5f; + private static float TOAST_APEX_Z_LOCATION = .2f; + private static float TOAST_VELOCITY = .1f; - private Image background; - private Text toastText; - private float toastStart; - private bool toastActive = false; + private Image background; + private Text toastText; + private float toastStart; + private bool toastActive = false; - void Start() { - background = transform.Find("Background").GetComponent(); - Color bgColor = Color.white; - bgColor.a = 0f; - background.color = bgColor; - toastText = transform.Find("ToastText").GetComponent(); - } + void Start() + { + background = transform.Find("Background").GetComponent(); + Color bgColor = Color.white; + bgColor.a = 0f; + background.color = bgColor; + toastText = transform.Find("ToastText").GetComponent(); + } - void Update() { - if (!toastActive) { - return; - } + void Update() + { + if (!toastActive) + { + return; + } - float currentTime = Time.time; - float toastTime = currentTime - toastStart; - if (toastTime < 1f) { - Vector3 currPos = gameObject.transform.localPosition; - currPos = new Vector3(currPos.x, currPos.y, - Mathf.Min(currPos.z + TOAST_VELOCITY * Time.deltaTime, TOAST_APEX_Z_LOCATION)); - gameObject.transform.localPosition = currPos; + float currentTime = Time.time; + float toastTime = currentTime - toastStart; + if (toastTime < 1f) + { + Vector3 currPos = gameObject.transform.localPosition; + currPos = new Vector3(currPos.x, currPos.y, + Mathf.Min(currPos.z + TOAST_VELOCITY * Time.deltaTime, TOAST_APEX_Z_LOCATION)); + gameObject.transform.localPosition = currPos; - Color currBackgroundColor = background.color; - currBackgroundColor.a = Mathf.Min(currBackgroundColor.a + Time.deltaTime, 1f); - background.color = currBackgroundColor; - } - if (toastTime > 4f) { - Vector3 currPos = gameObject.transform.localPosition; - currPos = new Vector3(currPos.x, currPos.y, Mathf.Max(currPos.z - TOAST_VELOCITY * Time.deltaTime, .1f)); - gameObject.transform.localPosition = currPos; + Color currBackgroundColor = background.color; + currBackgroundColor.a = Mathf.Min(currBackgroundColor.a + Time.deltaTime, 1f); + background.color = currBackgroundColor; + } + if (toastTime > 4f) + { + Vector3 currPos = gameObject.transform.localPosition; + currPos = new Vector3(currPos.x, currPos.y, Mathf.Max(currPos.z - TOAST_VELOCITY * Time.deltaTime, .1f)); + gameObject.transform.localPosition = currPos; - Color currBackgroundColor = background.color; - currBackgroundColor.a = Mathf.Max(currBackgroundColor.a - Time.deltaTime, 0f); - background.color = currBackgroundColor; - } - if (toastTime > TOTAL_TOAST_TIME_S) { - toastActive = false; - toastText.text = ""; - background.color = new Color(1f, 1f, 1f, 0f); - } - } + Color currBackgroundColor = background.color; + currBackgroundColor.a = Mathf.Max(currBackgroundColor.a - Time.deltaTime, 0f); + background.color = currBackgroundColor; + } + if (toastTime > TOTAL_TOAST_TIME_S) + { + toastActive = false; + toastText.text = ""; + background.color = new Color(1f, 1f, 1f, 0f); + } + } - public void ShowToastMessage(string message) { - toastText.text = message; - toastStart = Time.time; - toastActive = true; + public void ShowToastMessage(string message) + { + toastText.text = message; + toastStart = Time.time; + toastActive = true; + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/controller/ToggleMenuItem.cs b/Assets/Scripts/model/controller/ToggleMenuItem.cs index be97b4d0..96bd9f3f 100644 --- a/Assets/Scripts/model/controller/ToggleMenuItem.cs +++ b/Assets/Scripts/model/controller/ToggleMenuItem.cs @@ -17,62 +17,72 @@ using UnityEngine; using System.Collections.Generic; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Subclass for a toggleable menu item that enables or disabled a feature. Different feature changes can be - /// specified for turning on and off the feature. Feature changes are specified as a comma delimited string, where - /// each entry is the string form of the feature name prepended by either a '+' or a '-' which controls whether the - /// feature is enabled or disabled. For example a featureStringOn of "+featureA,-featureB,+featureC" will cause - /// featureA and featureC to be enabled when this is toggled on, and featureB to be disabled. - /// - public class ToggleMenuItem : MenuActionItem { - // Whether this is initially enabled or not - if it is, featureStringOn should match the starting state of the - // features it modifies. - public bool enabled; - // The set of feature changes to apply when this is toggled on, formatted as a comma delimited list of features, - // each prepended by either a '+' or a '-' to denote that it is being turned on or off. - public String featureStringOn; - // The set of feature changes to apply when this is toggled off, formatted as a comma delimited list of features, - // each prepended by either a '+' or a '-' to denote that it is being turned on or off. - public String featureStringOff; - // The GameObject that displays text when this is enabled. - public GameObject enabledText; - // The GameObject that displays text when this is disabled. - public GameObject disabledText; - // Other toggle menu items that become enabled when this toggle menu is enabled. - public List enableDepedentToggles; - // Other toggle menu items that become disabled when this toggle menu is disabled. - public List disableDependentToggles; +namespace com.google.apps.peltzer.client.model.controller +{ + /// + /// Subclass for a toggleable menu item that enables or disabled a feature. Different feature changes can be + /// specified for turning on and off the feature. Feature changes are specified as a comma delimited string, where + /// each entry is the string form of the feature name prepended by either a '+' or a '-' which controls whether the + /// feature is enabled or disabled. For example a featureStringOn of "+featureA,-featureB,+featureC" will cause + /// featureA and featureC to be enabled when this is toggled on, and featureB to be disabled. + /// + public class ToggleMenuItem : MenuActionItem + { + // Whether this is initially enabled or not - if it is, featureStringOn should match the starting state of the + // features it modifies. + public bool enabled; + // The set of feature changes to apply when this is toggled on, formatted as a comma delimited list of features, + // each prepended by either a '+' or a '-' to denote that it is being turned on or off. + public String featureStringOn; + // The set of feature changes to apply when this is toggled off, formatted as a comma delimited list of features, + // each prepended by either a '+' or a '-' to denote that it is being turned on or off. + public String featureStringOff; + // The GameObject that displays text when this is enabled. + public GameObject enabledText; + // The GameObject that displays text when this is disabled. + public GameObject disabledText; + // Other toggle menu items that become enabled when this toggle menu is enabled. + public List enableDepedentToggles; + // Other toggle menu items that become disabled when this toggle menu is disabled. + public List disableDependentToggles; - public void Start() { - enabledText.SetActive(enabled); - disabledText.SetActive(!enabled); - base.Start(); - } - - public override void ApplyMenuOptions(PeltzerMain main) { - if (!ActionIsAllowed()) return; - SetEnabled(!enabled); - if (enabled) { - Features.ToggleFeatureString(featureStringOn); - foreach (ToggleMenuItem enableDependentToggle in enableDepedentToggles) { - enableDependentToggle.SetEnabled(true); + public void Start() + { + enabledText.SetActive(enabled); + disabledText.SetActive(!enabled); + base.Start(); } - } else { - Features.ToggleFeatureString(featureStringOff); - foreach (ToggleMenuItem disableDependentToggle in disableDependentToggles) { - disableDependentToggle.SetEnabled(false); + + public override void ApplyMenuOptions(PeltzerMain main) + { + if (!ActionIsAllowed()) return; + SetEnabled(!enabled); + if (enabled) + { + Features.ToggleFeatureString(featureStringOn); + foreach (ToggleMenuItem enableDependentToggle in enableDepedentToggles) + { + enableDependentToggle.SetEnabled(true); + } + } + else + { + Features.ToggleFeatureString(featureStringOff); + foreach (ToggleMenuItem disableDependentToggle in disableDependentToggles) + { + disableDependentToggle.SetEnabled(false); + } + } + main.InvokeMenuAction(action); + main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); + StartBump(); } - } - main.InvokeMenuAction(action); - main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); - StartBump(); - } - public void SetEnabled(bool newState) { - enabled = newState; - enabledText.SetActive(enabled); - disabledText.SetActive(!enabled); + public void SetEnabled(bool newState) + { + enabled = newState; + enabledText.SetActive(enabled); + disabledText.SetActive(!enabled); + } } - } } diff --git a/Assets/Scripts/model/controller/TooltipManager.cs b/Assets/Scripts/model/controller/TooltipManager.cs index 8d5191d9..d7dee7d3 100644 --- a/Assets/Scripts/model/controller/TooltipManager.cs +++ b/Assets/Scripts/model/controller/TooltipManager.cs @@ -16,110 +16,119 @@ using System.Collections.Generic; using UnityEngine; -namespace com.google.apps.peltzer.client.model.controller { - public struct ControllerTooltip { - public String tooltipLabel; - public String tooltipText; - public float textWidth; - - public ControllerTooltip(String label, String text, float textWidth) { - tooltipLabel = label; - tooltipText = text; - this.textWidth = textWidth; - } - } - - /// - /// Manages tooltips for one UI element (to ensure multiple tooltips for the same button can't be shown - /// at the same time). - /// - public class TooltipManager { - private const float padding = 0.008f; - - private Dictionary tooltips; - - private GameObject root; - private GameObject leftTip; - private Transform leftTipL; - private TextMesh leftTipText; - private Transform leftBg; - private GameObject rightTip; - private Transform rightTipL; - private TextMesh rightTipText; - private Transform rightBg; - private bool isActive = false; - - public TooltipManager(IEnumerable tooltips, GameObject root, GameObject leftTip, - GameObject rightTip) { - this.root = root; - this.leftTip = leftTip; - this.leftTipText = leftTip.GetComponentInChildren(); - this.leftBg = leftTip.transform.Find("bg"); - this.leftTipL = leftTip.transform.Find("tipL"); - this.rightTip = rightTip; - this.rightTipText = rightTip.GetComponentInChildren(); - this.rightBg = rightTip.transform.Find("bg"); - this.rightTipL = rightTip.transform.Find("tipL"); - this.tooltips = new Dictionary(); - foreach (ControllerTooltip tipSpec in tooltips) { - this.tooltips[tipSpec.tooltipLabel] = tipSpec; - } - TurnOff(); - } +namespace com.google.apps.peltzer.client.model.controller +{ + public struct ControllerTooltip + { + public String tooltipLabel; + public String tooltipText; + public float textWidth; - /// - /// Turns off the tooltip. - /// - public void TurnOff() { - root.SetActive(false); - isActive = false; + public ControllerTooltip(String label, String text, float textWidth) + { + tooltipLabel = label; + tooltipText = text; + this.textWidth = textWidth; + } } /// - /// Turns on the tooltip with the specified label. + /// Manages tooltips for one UI element (to ensure multiple tooltips for the same button can't be shown + /// at the same time). /// - /// - public void TurnOn(String label) { - - float bgWidth = tooltips[label].textWidth; - rightTipText.text = tooltips[label].tooltipText; - - Vector3 rootPos = rightTip.transform.localPosition; - rootPos.x = bgWidth + padding; - rightTip.transform.localPosition = rootPos; - - Vector3 tipLPos = rightTipL.localPosition; - tipLPos.x = -bgWidth; - rightTipL.localPosition = tipLPos; - - Vector3 bgScale = rightBg.localScale; - bgScale.x = bgWidth; - rightBg.localScale = new Vector3(tooltips[label].textWidth, rightBg.localScale.y, rightBg.localScale.z); - - leftTipText.text = tooltips[label].tooltipText; - - rootPos = leftTip.transform.localPosition; - rootPos.x = -(bgWidth + padding); - leftTip.transform.localPosition = rootPos; - - tipLPos = leftTipL.localPosition; - tipLPos.x = bgWidth; - leftTipL.localPosition = tipLPos; - - bgScale = leftBg.localScale; - bgScale.x = bgWidth; - leftBg.localScale = bgScale; - - root.SetActive(true); - isActive = true; - } + public class TooltipManager + { + private const float padding = 0.008f; - /// - /// Is a tooltip currently showing? - /// - /// - public bool IsActive() { - return isActive; + private Dictionary tooltips; + + private GameObject root; + private GameObject leftTip; + private Transform leftTipL; + private TextMesh leftTipText; + private Transform leftBg; + private GameObject rightTip; + private Transform rightTipL; + private TextMesh rightTipText; + private Transform rightBg; + private bool isActive = false; + + public TooltipManager(IEnumerable tooltips, GameObject root, GameObject leftTip, + GameObject rightTip) + { + this.root = root; + this.leftTip = leftTip; + this.leftTipText = leftTip.GetComponentInChildren(); + this.leftBg = leftTip.transform.Find("bg"); + this.leftTipL = leftTip.transform.Find("tipL"); + this.rightTip = rightTip; + this.rightTipText = rightTip.GetComponentInChildren(); + this.rightBg = rightTip.transform.Find("bg"); + this.rightTipL = rightTip.transform.Find("tipL"); + this.tooltips = new Dictionary(); + foreach (ControllerTooltip tipSpec in tooltips) + { + this.tooltips[tipSpec.tooltipLabel] = tipSpec; + } + TurnOff(); + } + + /// + /// Turns off the tooltip. + /// + public void TurnOff() + { + root.SetActive(false); + isActive = false; + } + + /// + /// Turns on the tooltip with the specified label. + /// + /// + public void TurnOn(String label) + { + + float bgWidth = tooltips[label].textWidth; + rightTipText.text = tooltips[label].tooltipText; + + Vector3 rootPos = rightTip.transform.localPosition; + rootPos.x = bgWidth + padding; + rightTip.transform.localPosition = rootPos; + + Vector3 tipLPos = rightTipL.localPosition; + tipLPos.x = -bgWidth; + rightTipL.localPosition = tipLPos; + + Vector3 bgScale = rightBg.localScale; + bgScale.x = bgWidth; + rightBg.localScale = new Vector3(tooltips[label].textWidth, rightBg.localScale.y, rightBg.localScale.z); + + leftTipText.text = tooltips[label].tooltipText; + + rootPos = leftTip.transform.localPosition; + rootPos.x = -(bgWidth + padding); + leftTip.transform.localPosition = rootPos; + + tipLPos = leftTipL.localPosition; + tipLPos.x = bgWidth; + leftTipL.localPosition = tipLPos; + + bgScale = leftBg.localScale; + bgScale.x = bgWidth; + leftBg.localScale = bgScale; + + root.SetActive(true); + isActive = true; + } + + /// + /// Is a tooltip currently showing? + /// + /// + public bool IsActive() + { + return isActive; + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/controller/TouchPadLocation.cs b/Assets/Scripts/model/controller/TouchPadLocation.cs index 67b50f18..2ce247f3 100644 --- a/Assets/Scripts/model/controller/TouchPadLocation.cs +++ b/Assets/Scripts/model/controller/TouchPadLocation.cs @@ -15,53 +15,59 @@ using UnityEngine; using System.Collections; -namespace com.google.apps.peltzer.client.model.controller { - /// - /// Helper for octant locations on a controller's touchpad. - /// For controller selections that are on the axis, use top, bottom, left, right. - /// For controller selections that are diagnonal, user northeast, northwest, etc. - /// - public enum TouchpadLocation { NONE, TOP, BOTTOM, RIGHT, LEFT, CENTER } - public static class TouchpadLocationHelper { - private static readonly float TWO_PI = Mathf.PI * 2f; - private static readonly float PI_OVER_4 = Mathf.PI * .25f; - private static readonly float CENTER_BUTTON_RADIUS = 0.333f; - +namespace com.google.apps.peltzer.client.model.controller +{ /// - /// Given an x,y co-ordinate on the touchpad, returns which quadrant - /// that co-ordinate maps to. Quadrants are defined as 90-degree areas - /// with the TOP area at -45 degrees to 45 degrees and other areas - /// similarly offset from the cardinal directions. + /// Helper for octant locations on a controller's touchpad. + /// For controller selections that are on the axis, use top, bottom, left, right. + /// For controller selections that are diagnonal, user northeast, northwest, etc. /// - public static TouchpadLocation GetTouchpadLocation(Vector2 position) { - // Check for the center. - float radius = position.magnitude; - if (radius <= CENTER_BUTTON_RADIUS) { - return TouchpadLocation.CENTER; - } + public enum TouchpadLocation { NONE, TOP, BOTTOM, RIGHT, LEFT, CENTER } + public static class TouchpadLocationHelper + { + private static readonly float TWO_PI = Mathf.PI * 2f; + private static readonly float PI_OVER_4 = Mathf.PI * .25f; + private static readonly float CENTER_BUTTON_RADIUS = 0.333f; + + /// + /// Given an x,y co-ordinate on the touchpad, returns which quadrant + /// that co-ordinate maps to. Quadrants are defined as 90-degree areas + /// with the TOP area at -45 degrees to 45 degrees and other areas + /// similarly offset from the cardinal directions. + /// + public static TouchpadLocation GetTouchpadLocation(Vector2 position) + { + // Check for the center. + float radius = position.magnitude; + if (radius <= CENTER_BUTTON_RADIUS) + { + return TouchpadLocation.CENTER; + } - // Find the quadrant. - float angle = Mathf.Atan2(position.x, position.y); - if (angle < 0) { - angle += TWO_PI; - } - int octant = (int)Mathf.Floor(angle / PI_OVER_4); - switch(octant) { - case 0: - case 7: - return TouchpadLocation.TOP; - case 1: - case 2: - return TouchpadLocation.RIGHT; - case 3: - case 4: - return TouchpadLocation.BOTTOM; - case 5: - case 6: - return TouchpadLocation.LEFT; - default: - return TouchpadLocation.NONE; - } + // Find the quadrant. + float angle = Mathf.Atan2(position.x, position.y); + if (angle < 0) + { + angle += TWO_PI; + } + int octant = (int)Mathf.Floor(angle / PI_OVER_4); + switch (octant) + { + case 0: + case 7: + return TouchpadLocation.TOP; + case 1: + case 2: + return TouchpadLocation.RIGHT; + case 3: + case 4: + return TouchpadLocation.BOTTOM; + case 5: + case 6: + return TouchpadLocation.LEFT; + default: + return TouchpadLocation.NONE; + } + } } - } } diff --git a/Assets/Scripts/model/controller/VideoActionItem.cs b/Assets/Scripts/model/controller/VideoActionItem.cs index 80bd6767..4d95b1aa 100644 --- a/Assets/Scripts/model/controller/VideoActionItem.cs +++ b/Assets/Scripts/model/controller/VideoActionItem.cs @@ -18,76 +18,82 @@ using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.controller { - - /// - /// SelectableMenuItem that can be attached to items on the palette file menu. - /// - public class VideoActionItem : MenuActionItem { - // The video viewer will appear to the side of the button that was pressed to begin the video. - public static Vector3 VIDEO_VIEWER_OFFSET_LEFT_HANDED = new Vector3(1.9f, 0, 2.4f); - public static Vector3 VIDEO_VIEWER_OFFSET_RIGHT_HANDED = new Vector3(1.9f, 0, -2.4f); - - // The filename of the video this MenuActionItem will play. - public string videoFilename; +namespace com.google.apps.peltzer.client.model.controller +{ /// - /// When this button is clicked, if it is currently enabled/allowed, then we make a noise, animate the button - /// and begin playing the video. + /// SelectableMenuItem that can be attached to items on the palette file menu. /// - public override void ApplyMenuOptions(PeltzerMain main) { - if (!ActionIsAllowed()) return; + public class VideoActionItem : MenuActionItem + { + // The video viewer will appear to the side of the button that was pressed to begin the video. + public static Vector3 VIDEO_VIEWER_OFFSET_LEFT_HANDED = new Vector3(1.9f, 0, 2.4f); + public static Vector3 VIDEO_VIEWER_OFFSET_RIGHT_HANDED = new Vector3(1.9f, 0, -2.4f); - PlayVideo(); - main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); - StartBump(); - } + // The filename of the video this MenuActionItem will play. + public string videoFilename; + /// + /// When this button is clicked, if it is currently enabled/allowed, then we make a noise, animate the button + /// and begin playing the video. + /// + public override void ApplyMenuOptions(PeltzerMain main) + { + if (!ActionIsAllowed()) return; - /// - /// Begins playing the video specified in videoFilename. - /// - /// There is exactly one video viewer in the scene. It will be displayed if hidden, and any previous video will - /// be immediately switched out in favour of this video. The video viewer will be placed in the scene near to - /// the button represented by this script's transform, but may later be moved with the grab tool. - /// - /// Videos are streamed from the local StreamingAssets folder rather than pre-buffered, due to performance issues - /// on Windows in the current Unity Video Preparation path. - /// - private void PlayVideo() { - // Find the video viewer and set it active. - GameObject videoViewer = PeltzerMain.Instance.GetVideoViewer(); - videoViewer.SetActive(true); + PlayVideo(); + main.audioLibrary.PlayClip(main.audioLibrary.menuSelectSound); + StartBump(); + } + + + /// + /// Begins playing the video specified in videoFilename. + /// + /// There is exactly one video viewer in the scene. It will be displayed if hidden, and any previous video will + /// be immediately switched out in favour of this video. The video viewer will be placed in the scene near to + /// the button represented by this script's transform, but may later be moved with the grab tool. + /// + /// Videos are streamed from the local StreamingAssets folder rather than pre-buffered, due to performance issues + /// on Windows in the current Unity Video Preparation path. + /// + private void PlayVideo() + { + // Find the video viewer and set it active. + GameObject videoViewer = PeltzerMain.Instance.GetVideoViewer(); + videoViewer.SetActive(true); - // Offset the video viewer from the button, placing it nicely by the menu. - Vector3 change = PeltzerMain.Instance.peltzerController.handedness == Handedness.RIGHT ? - VIDEO_VIEWER_OFFSET_RIGHT_HANDED : - VIDEO_VIEWER_OFFSET_LEFT_HANDED; - Vector3 newLocalPos = transform.localPosition + change; - videoViewer.transform.position = transform.TransformPoint(newLocalPos); - videoViewer.transform.rotation = transform.rotation * Quaternion.Euler(0, 180, 0); - VideoPlayer player = videoViewer.GetComponent(); + // Offset the video viewer from the button, placing it nicely by the menu. + Vector3 change = PeltzerMain.Instance.peltzerController.handedness == Handedness.RIGHT ? + VIDEO_VIEWER_OFFSET_RIGHT_HANDED : + VIDEO_VIEWER_OFFSET_LEFT_HANDED; + Vector3 newLocalPos = transform.localPosition + change; + videoViewer.transform.position = transform.TransformPoint(newLocalPos); + videoViewer.transform.rotation = transform.rotation * Quaternion.Euler(0, 180, 0); + VideoPlayer player = videoViewer.GetComponent(); - // Stop any existing video that was playing. - if (player.isPlaying) { - player.Stop(); - } + // Stop any existing video that was playing. + if (player.isPlaying) + { + player.Stop(); + } - // Prepare this video. We use streaming rather than pre-buffering for performance reasons. Quoth Unity: - // "A fix to make preparation in another thread is ongoing, we'll make it available as soon as possible." - // https://forum.unity.com/threads/videoplayer-seturl-and-videoplayer-prepare-very-expensive.465128/ - player.url = Path.Combine(Application.streamingAssetsPath, Path.Combine("Videos", videoFilename)); + // Prepare this video. We use streaming rather than pre-buffering for performance reasons. Quoth Unity: + // "A fix to make preparation in another thread is ongoing, we'll make it available as soon as possible." + // https://forum.unity.com/threads/videoplayer-seturl-and-videoplayer-prepare-very-expensive.465128/ + player.url = Path.Combine(Application.streamingAssetsPath, Path.Combine("Videos", videoFilename)); - // Set up the audio for this video. A little ugly but I can't find a better way. - player.controlledAudioTrackCount = 1; - AudioSource audioSource = GetComponent(); - if (audioSource == null) { - audioSource = gameObject.AddComponent(); - } - player.SetTargetAudioSource(0, audioSource); + // Set up the audio for this video. A little ugly but I can't find a better way. + player.controlledAudioTrackCount = 1; + AudioSource audioSource = GetComponent(); + if (audioSource == null) + { + audioSource = gameObject.AddComponent(); + } + player.SetTargetAudioSource(0, audioSource); - // Stream this video. - player.Play(); + // Stream this video. + player.Play(); + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/core/AddMeshCommand.cs b/Assets/Scripts/model/core/AddMeshCommand.cs index 2bf35193..f6b93904 100644 --- a/Assets/Scripts/model/core/AddMeshCommand.cs +++ b/Assets/Scripts/model/core/AddMeshCommand.cs @@ -12,37 +12,44 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace com.google.apps.peltzer.client.model.core { - /// - /// Command that adds an MMesh to the model. - /// - public class AddMeshCommand : Command { - public const string COMMAND_NAME = "add"; +namespace com.google.apps.peltzer.client.model.core +{ + /// + /// Command that adds an MMesh to the model. + /// + public class AddMeshCommand : Command + { + public const string COMMAND_NAME = "add"; - private readonly MMesh mesh; - private readonly bool useInsertEffect; + private readonly MMesh mesh; + private readonly bool useInsertEffect; - public AddMeshCommand(MMesh mesh, bool useInsertEffect = false) { - this.mesh = mesh.Clone(); - this.useInsertEffect = useInsertEffect; - } + public AddMeshCommand(MMesh mesh, bool useInsertEffect = false) + { + this.mesh = mesh.Clone(); + this.useInsertEffect = useInsertEffect; + } - public void ApplyToModel(Model model) { - // Clone this mesh so that the mutable one added to the model doesn't affect - // this immutable command. - model.AddMesh(mesh, useInsertEffect); - } + public void ApplyToModel(Model model) + { + // Clone this mesh so that the mutable one added to the model doesn't affect + // this immutable command. + model.AddMesh(mesh, useInsertEffect); + } - public Command GetUndoCommand(Model model) { - return new DeleteMeshCommand(mesh.id); - } + public Command GetUndoCommand(Model model) + { + return new DeleteMeshCommand(mesh.id); + } - public MMesh GetMeshClone() { - return mesh.Clone(); - } + public MMesh GetMeshClone() + { + return mesh.Clone(); + } - public int GetMeshId() { - return mesh.id; + public int GetMeshId() + { + return mesh.id; + } } - } } diff --git a/Assets/Scripts/model/core/AddReferenceImageCommand.cs b/Assets/Scripts/model/core/AddReferenceImageCommand.cs index 3319547f..aaecaf88 100644 --- a/Assets/Scripts/model/core/AddReferenceImageCommand.cs +++ b/Assets/Scripts/model/core/AddReferenceImageCommand.cs @@ -15,27 +15,32 @@ using com.google.apps.peltzer.client.desktop_app; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.core { - /// - /// This command adds a reference image to the scene. This is different from other commands in the following ways: - /// * It does not modify the model - /// * It adds GameObjects to the scene - /// * It does not serialize into a peltzer file - /// * See bug for a little more information/background - /// - public class AddReferenceImageCommand : Command { - private MoveableReferenceImage.SetupParams setupParams; +namespace com.google.apps.peltzer.client.model.core +{ + /// + /// This command adds a reference image to the scene. This is different from other commands in the following ways: + /// * It does not modify the model + /// * It adds GameObjects to the scene + /// * It does not serialize into a peltzer file + /// * See bug for a little more information/background + /// + public class AddReferenceImageCommand : Command + { + private MoveableReferenceImage.SetupParams setupParams; - public AddReferenceImageCommand(MoveableReferenceImage.SetupParams setupParams) { - this.setupParams = setupParams; - } + public AddReferenceImageCommand(MoveableReferenceImage.SetupParams setupParams) + { + this.setupParams = setupParams; + } - public void ApplyToModel(Model model) { - PeltzerMain.Instance.referenceImageManager.CreateReferenceImage(setupParams); - } + public void ApplyToModel(Model model) + { + PeltzerMain.Instance.referenceImageManager.CreateReferenceImage(setupParams); + } - public Command GetUndoCommand(Model model) { - return new DeleteReferenceImageCommand(setupParams); + public Command GetUndoCommand(Model model) + { + return new DeleteReferenceImageCommand(setupParams); + } } - } } diff --git a/Assets/Scripts/model/core/ChangeFacePropertiesCommand.cs b/Assets/Scripts/model/core/ChangeFacePropertiesCommand.cs index 0f40ec09..e35d5604 100644 --- a/Assets/Scripts/model/core/ChangeFacePropertiesCommand.cs +++ b/Assets/Scripts/model/core/ChangeFacePropertiesCommand.cs @@ -14,73 +14,88 @@ using System.Collections.Generic; -namespace com.google.apps.peltzer.client.model.core { - /// - /// Command for changing the properties of faces on a MMesh. This command only applies to a single - /// mesh but can change one or more faces. - /// - /// This command is used for painting, which consists of changing the material of one or more faces. - /// Note that this command can be set up to apply the same properties to all indicated faces, or - /// apply different properties to each face. - /// - public class ChangeFacePropertiesCommand : Command { - public const string COMMAND_NAME = "changeFaceProperties"; - - /// - /// The mesh ID whose faces are to be affected. - /// - private readonly int meshId; - +namespace com.google.apps.peltzer.client.model.core +{ /// - /// The FaceProperties to apply to each face in the mesh (map from face ID to the face properties to apply to it). - /// Only one of propertiesForAllFaces or propertiesByFaceId should be non-null. + /// Command for changing the properties of faces on a MMesh. This command only applies to a single + /// mesh but can change one or more faces. + /// + /// This command is used for painting, which consists of changing the material of one or more faces. + /// Note that this command can be set up to apply the same properties to all indicated faces, or + /// apply different properties to each face. /// - private readonly Dictionary propertiesByFaceId; + public class ChangeFacePropertiesCommand : Command + { + public const string COMMAND_NAME = "changeFaceProperties"; - /// - /// Properties to apply to ALL faces. - /// Only one of propertiesForAllFaces or propertiesByFaceId should be non-null. - /// - private readonly FaceProperties? propertiesForAllFaces; + /// + /// The mesh ID whose faces are to be affected. + /// + private readonly int meshId; - public ChangeFacePropertiesCommand(int meshId, Dictionary propertiesByFaceId) { - this.meshId = meshId; - this.propertiesByFaceId = propertiesByFaceId; - } + /// + /// The FaceProperties to apply to each face in the mesh (map from face ID to the face properties to apply to it). + /// Only one of propertiesForAllFaces or propertiesByFaceId should be non-null. + /// + private readonly Dictionary propertiesByFaceId; - public ChangeFacePropertiesCommand(int meshId, FaceProperties propertiesForAllFaces) { - this.meshId = meshId; - this.propertiesForAllFaces = propertiesForAllFaces; - } + /// + /// Properties to apply to ALL faces. + /// Only one of propertiesForAllFaces or propertiesByFaceId should be non-null. + /// + private readonly FaceProperties? propertiesForAllFaces; - public int GetMeshId() { - return meshId; - } + public ChangeFacePropertiesCommand(int meshId, Dictionary propertiesByFaceId) + { + this.meshId = meshId; + this.propertiesByFaceId = propertiesByFaceId; + } - public void ApplyToModel(Model model) { - if (propertiesForAllFaces != null) { - model.ChangeAllFaceProperties(meshId, propertiesForAllFaces.Value); - } else { - model.ChangeFaceProperties(meshId, propertiesByFaceId); - } - } + public ChangeFacePropertiesCommand(int meshId, FaceProperties propertiesForAllFaces) + { + this.meshId = meshId; + this.propertiesForAllFaces = propertiesForAllFaces; + } - public Command GetUndoCommand(Model model) { - MMesh mesh = model.GetMesh(meshId); - Dictionary undoProps; + public int GetMeshId() + { + return meshId; + } - if (propertiesByFaceId == null) { - undoProps = new Dictionary(mesh.faceCount); - foreach (Face face in mesh.GetFaces()) { - undoProps[face.id] = face.properties; + public void ApplyToModel(Model model) + { + if (propertiesForAllFaces != null) + { + model.ChangeAllFaceProperties(meshId, propertiesForAllFaces.Value); + } + else + { + model.ChangeFaceProperties(meshId, propertiesByFaceId); + } } - } else { - undoProps = new Dictionary(propertiesByFaceId.Count); - foreach (int faceId in propertiesByFaceId.Keys) { - undoProps[faceId] = mesh.GetFace(faceId).properties; + + public Command GetUndoCommand(Model model) + { + MMesh mesh = model.GetMesh(meshId); + Dictionary undoProps; + + if (propertiesByFaceId == null) + { + undoProps = new Dictionary(mesh.faceCount); + foreach (Face face in mesh.GetFaces()) + { + undoProps[face.id] = face.properties; + } + } + else + { + undoProps = new Dictionary(propertiesByFaceId.Count); + foreach (int faceId in propertiesByFaceId.Keys) + { + undoProps[faceId] = mesh.GetFace(faceId).properties; + } + } + return new ChangeFacePropertiesCommand(meshId, undoProps); } - } - return new ChangeFacePropertiesCommand(meshId, undoProps); } - } } diff --git a/Assets/Scripts/model/core/Command.cs b/Assets/Scripts/model/core/Command.cs index 40c5003f..3f3e7b9f 100644 --- a/Assets/Scripts/model/core/Command.cs +++ b/Assets/Scripts/model/core/Command.cs @@ -12,26 +12,28 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace com.google.apps.peltzer.client.model.core { - /// - /// A command represents a mutation of the Model. For example, adding, moving, modifying or deleting meshes - /// are commands. In general, UI code should only mutate the Model by using a Command because that allows - /// undo/redo to work correctly. - /// - public interface Command { +namespace com.google.apps.peltzer.client.model.core +{ /// - /// Mutate the model with this command. + /// A command represents a mutation of the Model. For example, adding, moving, modifying or deleting meshes + /// are commands. In general, UI code should only mutate the Model by using a Command because that allows + /// undo/redo to work correctly. /// - /// The model. - void ApplyToModel(Model model); + public interface Command + { + /// + /// Mutate the model with this command. + /// + /// The model. + void ApplyToModel(Model model); - /// - /// Create a command that will undo this Command. This will be - /// called before ApplyToModel so that it will have the desired - /// end state available to it. - /// - /// The model. - /// The undo command. - Command GetUndoCommand(Model model); - } + /// + /// Create a command that will undo this Command. This will be + /// called before ApplyToModel so that it will have the desired + /// end state available to it. + /// + /// The model. + /// The undo command. + Command GetUndoCommand(Model model); + } } diff --git a/Assets/Scripts/model/core/CompositeCommand.cs b/Assets/Scripts/model/core/CompositeCommand.cs index 7e060136..716c2864 100644 --- a/Assets/Scripts/model/core/CompositeCommand.cs +++ b/Assets/Scripts/model/core/CompositeCommand.cs @@ -14,39 +14,47 @@ using System.Collections.Generic; -namespace com.google.apps.peltzer.client.model.core { - - /// - /// A Command that is a composite of several other Commands. - /// - public class CompositeCommand : Command { - public const string COMMAND_NAME = "composite"; - - private readonly List commands; - - public CompositeCommand(List commands) { - this.commands = commands; - } - - public void ApplyToModel(Model model) { - foreach(Command command in commands) { - command.ApplyToModel(model); - } - } - - public Command GetUndoCommand(Model model) { - List undoCommands = new List(commands.Count); - foreach(Command command in commands) { - undoCommands.Add(command.GetUndoCommand(model)); - } - // Undo should be applied in reverse order. - undoCommands.Reverse(); - return new CompositeCommand(undoCommands); - } - - // Visible for testing. - public List GetCommands() { - return commands; +namespace com.google.apps.peltzer.client.model.core +{ + + /// + /// A Command that is a composite of several other Commands. + /// + public class CompositeCommand : Command + { + public const string COMMAND_NAME = "composite"; + + private readonly List commands; + + public CompositeCommand(List commands) + { + this.commands = commands; + } + + public void ApplyToModel(Model model) + { + foreach (Command command in commands) + { + command.ApplyToModel(model); + } + } + + public Command GetUndoCommand(Model model) + { + List undoCommands = new List(commands.Count); + foreach (Command command in commands) + { + undoCommands.Add(command.GetUndoCommand(model)); + } + // Undo should be applied in reverse order. + undoCommands.Reverse(); + return new CompositeCommand(undoCommands); + } + + // Visible for testing. + public List GetCommands() + { + return commands; + } } - } } diff --git a/Assets/Scripts/model/core/CopyMeshCommand.cs b/Assets/Scripts/model/core/CopyMeshCommand.cs index 137328d5..756359ec 100644 --- a/Assets/Scripts/model/core/CopyMeshCommand.cs +++ b/Assets/Scripts/model/core/CopyMeshCommand.cs @@ -14,23 +14,27 @@ using System.Collections.Generic; -namespace com.google.apps.peltzer.client.model.core { - /// - /// A command consists of making a copy of an existing mesh. - /// - public class CopyMeshCommand : CompositeCommand { - internal readonly int copiedFromId; - internal readonly MMesh copy; +namespace com.google.apps.peltzer.client.model.core +{ + /// + /// A command consists of making a copy of an existing mesh. + /// + public class CopyMeshCommand : CompositeCommand + { + internal readonly int copiedFromId; + internal readonly MMesh copy; - public CopyMeshCommand(int copiedFromId, MMesh copy) : base(new List() { + public CopyMeshCommand(int copiedFromId, MMesh copy) : base(new List() { new AddMeshCommand(copy) - }) { - this.copiedFromId = copiedFromId; - this.copy = copy; - } + }) + { + this.copiedFromId = copiedFromId; + this.copy = copy; + } - public int GetCopyMeshId() { - return copy.id; + public int GetCopyMeshId() + { + return copy.id; + } } - } } diff --git a/Assets/Scripts/model/core/DeleteMeshCommand.cs b/Assets/Scripts/model/core/DeleteMeshCommand.cs index a1f11828..7b759adc 100644 --- a/Assets/Scripts/model/core/DeleteMeshCommand.cs +++ b/Assets/Scripts/model/core/DeleteMeshCommand.cs @@ -12,25 +12,30 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace com.google.apps.peltzer.client.model.core { - /// - /// Delete a mesh from the model. - /// - public class DeleteMeshCommand : Command { - private readonly int meshId; +namespace com.google.apps.peltzer.client.model.core +{ + /// + /// Delete a mesh from the model. + /// + public class DeleteMeshCommand : Command + { + private readonly int meshId; - public DeleteMeshCommand (int meshId) { - this.meshId = meshId; - } + public DeleteMeshCommand(int meshId) + { + this.meshId = meshId; + } - public void ApplyToModel(Model model) { - model.DeleteMesh(meshId); - } + public void ApplyToModel(Model model) + { + model.DeleteMesh(meshId); + } - public Command GetUndoCommand(Model model) { - return new AddMeshCommand(model.GetMesh(meshId)); - } + public Command GetUndoCommand(Model model) + { + return new AddMeshCommand(model.GetMesh(meshId)); + } - public int MeshId { get { return meshId; } } - } + public int MeshId { get { return meshId; } } + } } diff --git a/Assets/Scripts/model/core/DeleteReferenceImageCommand.cs b/Assets/Scripts/model/core/DeleteReferenceImageCommand.cs index 5c3f8929..f4e94735 100644 --- a/Assets/Scripts/model/core/DeleteReferenceImageCommand.cs +++ b/Assets/Scripts/model/core/DeleteReferenceImageCommand.cs @@ -15,35 +15,40 @@ using com.google.apps.peltzer.client.desktop_app; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.core { - /// - /// This command deletes a reference image from the scene. It's different from other commands in the following ways: - /// * It does not modify the model - /// * It deletes a GameObject from the scene - /// * It does not serialize into a peltzer file - /// * See bug for a little more information/background - /// - public class DeleteReferenceImageCommand : Command { - private MoveableReferenceImage.SetupParams setupParams; +namespace com.google.apps.peltzer.client.model.core +{ + /// + /// This command deletes a reference image from the scene. It's different from other commands in the following ways: + /// * It does not modify the model + /// * It deletes a GameObject from the scene + /// * It does not serialize into a peltzer file + /// * See bug for a little more information/background + /// + public class DeleteReferenceImageCommand : Command + { + private MoveableReferenceImage.SetupParams setupParams; - public DeleteReferenceImageCommand(MoveableReferenceImage.SetupParams setupParams) { - this.setupParams = setupParams; - } + public DeleteReferenceImageCommand(MoveableReferenceImage.SetupParams setupParams) + { + this.setupParams = setupParams; + } - public void ApplyToModel(Model model) { - PeltzerMain.Instance.referenceImageManager.DeleteReferenceImage(setupParams.refImageId); + public void ApplyToModel(Model model) + { + PeltzerMain.Instance.referenceImageManager.DeleteReferenceImage(setupParams.refImageId); - // In case a reference image has been removed with an undo action, make sure the palette - // and menu are reenabled. - PeltzerMain.Instance.restrictionManager.paletteAllowed = true; - PeltzerMain.Instance.restrictionManager.menuActionsAllowed = true; - } + // In case a reference image has been removed with an undo action, make sure the palette + // and menu are reenabled. + PeltzerMain.Instance.restrictionManager.paletteAllowed = true; + PeltzerMain.Instance.restrictionManager.menuActionsAllowed = true; + } - public Command GetUndoCommand(Model model) { - // When a delete is undone, we wish for the reference image to be re-attached to the controller. See bug. - MoveableReferenceImage.SetupParams newSetupParams = setupParams; - newSetupParams.attachToController = newSetupParams.initialInsertion; - return new AddReferenceImageCommand(newSetupParams); + public Command GetUndoCommand(Model model) + { + // When a delete is undone, we wish for the reference image to be re-attached to the controller. See bug. + MoveableReferenceImage.SetupParams newSetupParams = setupParams; + newSetupParams.attachToController = newSetupParams.initialInsertion; + return new AddReferenceImageCommand(newSetupParams); + } } - } } diff --git a/Assets/Scripts/model/core/EdgeKey.cs b/Assets/Scripts/model/core/EdgeKey.cs index f918aec6..c4ab660f 100644 --- a/Assets/Scripts/model/core/EdgeKey.cs +++ b/Assets/Scripts/model/core/EdgeKey.cs @@ -12,54 +12,64 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace com.google.apps.peltzer.client.model.core { - /// - /// A canonical id for an edge, which includes the id of the mesh it belongs to. - /// - public class EdgeKey { - private readonly int _meshId; - private readonly int _vertexId1; - private readonly int _vertexId2; - private readonly int _hashCode; +namespace com.google.apps.peltzer.client.model.core +{ + /// + /// A canonical id for an edge, which includes the id of the mesh it belongs to. + /// + public class EdgeKey + { + private readonly int _meshId; + private readonly int _vertexId1; + private readonly int _vertexId2; + private readonly int _hashCode; - public EdgeKey(int meshId, int vertexId1, int vertexId2) { - this._meshId = meshId; - if (vertexId1 < vertexId2) { - this._vertexId1 = vertexId1; - this._vertexId2 = vertexId2; - } else { - this._vertexId1 = vertexId2; - this._vertexId2 = vertexId1; - } - // Hashcode suggested by Effective Java and Jon Skeet: - // http://stackoverflow.com/questions/11742593/what-is-the-hashcode-for-a-custom-class-having-just-two-int-properties - _hashCode = 17; - _hashCode = _hashCode * 31 + _meshId; - _hashCode = _hashCode * 31 + _vertexId1; - _hashCode = _hashCode * 31 + _vertexId2; - } + public EdgeKey(int meshId, int vertexId1, int vertexId2) + { + this._meshId = meshId; + if (vertexId1 < vertexId2) + { + this._vertexId1 = vertexId1; + this._vertexId2 = vertexId2; + } + else + { + this._vertexId1 = vertexId2; + this._vertexId2 = vertexId1; + } + // Hashcode suggested by Effective Java and Jon Skeet: + // http://stackoverflow.com/questions/11742593/what-is-the-hashcode-for-a-custom-class-having-just-two-int-properties + _hashCode = 17; + _hashCode = _hashCode * 31 + _meshId; + _hashCode = _hashCode * 31 + _vertexId1; + _hashCode = _hashCode * 31 + _vertexId2; + } - public override bool Equals(object obj) { - return Equals(obj as EdgeKey); - } + public override bool Equals(object obj) + { + return Equals(obj as EdgeKey); + } - public bool Equals(EdgeKey otherKey) { - return otherKey != null - && _meshId == otherKey._meshId - && _vertexId1 == otherKey._vertexId1 - && _vertexId2 == otherKey._vertexId2; - } + public bool Equals(EdgeKey otherKey) + { + return otherKey != null + && _meshId == otherKey._meshId + && _vertexId1 == otherKey._vertexId1 + && _vertexId2 == otherKey._vertexId2; + } - public override int GetHashCode() { - return _hashCode; - } + public override int GetHashCode() + { + return _hashCode; + } - public bool ContainsVertex(int vertexId) { - return _vertexId1 == vertexId || _vertexId2 == vertexId; - } + public bool ContainsVertex(int vertexId) + { + return _vertexId1 == vertexId || _vertexId2 == vertexId; + } - public int meshId { get { return _meshId; } } - public int vertexId1 { get { return _vertexId1; } } - public int vertexId2 { get { return _vertexId2; } } - } + public int meshId { get { return _meshId; } } + public int vertexId1 { get { return _vertexId1; } } + public int vertexId2 { get { return _vertexId2; } } + } } diff --git a/Assets/Scripts/model/core/Face.cs b/Assets/Scripts/model/core/Face.cs index 5a6db419..49f190dd 100644 --- a/Assets/Scripts/model/core/Face.cs +++ b/Assets/Scripts/model/core/Face.cs @@ -18,285 +18,316 @@ using com.google.apps.peltzer.client.model.render; using UnityEngine; -namespace com.google.apps.peltzer.client.model.core { - /// - /// A polygonal face of a MMesh. Vertices must be specified in clockwise - /// order. Edges may not cross. Each Vertex must have a normal relative - /// to this face. - /// - public class Face { - // The id of this face (unique within the mesh) - private readonly int _id; - // The ordered collection of vertex ids that comprise the face, in clockwise order. - private readonly ReadOnlyCollection _vertexIds; - // We support two different triangulations as the renderer uses triangulation one way, and the modelling tools - // another. The renderer needs indices into its vertex buffer, and therefore wants triangulation to return triangle - // indices relative to the vertices in the face - it will apply an offset to those, and add them to the mesh's - // triangle array. More straightforwardly, modeling operations need to look up the relevant vertices from the - // MMesh, and hence require the vertex id. - - // This triangulation is a list of triangles whose indices are vertex ids. This is used by mesh validation among - // others. This representation is used to allow easy lookup of vertex data from the MMesh. - private List _modelTriangulation; - - // This triangulation is a list of triangles which index into this face's _vertexIds collection. This representation - // is required for adding triangles to a Unity Mesh, but isn't as helpful for general modelling operations. - private List _renderTriangulation; - - // The face normal. Prior to face being added to a mesh via committing a GeometryOperation, this may not be set - // (which is represented by having a value of Vector3.zero) - private Vector3 _normal; - - // The properties of the face - primarily the material. - private FaceProperties _properties; - - // Read-only getters. - public int id { get { return _id; } } - public ReadOnlyCollection vertexIds { get { return _vertexIds; } } - public Vector3 normal { get { return _normal; } } - public FaceProperties properties { get { return _properties; }} - - // Cached vertex data, used to optimize construction of full mesh data for rendering. - private List cachedMeshSpacePositions = null; - private List cachedColors = null; - private List cachedRenderNormals = null; - +namespace com.google.apps.peltzer.client.model.core +{ /// - /// Constructs a face with no normal. This constructor should only be used when it is certain that the normal will - /// be calculated later (ie, by Mesh.CommitOperation) + /// A polygonal face of a MMesh. Vertices must be specified in clockwise + /// order. Edges may not cross. Each Vertex must have a normal relative + /// to this face. /// - /// Face id - /// Vertex ids for face in clockwise winding order. - /// Face properties - private Face(int id, ReadOnlyCollection vertexIds, FaceProperties properties) { - _id = id; - _vertexIds = vertexIds; - _normal = Vector3.zero; - - _properties = properties; - _modelTriangulation = null; - _renderTriangulation = null; - - // Allocating capacity here to avoid needing to do it on every fetch - capacity is retained after Clear(). - cachedMeshSpacePositions = new List(vertexIds.Count); - cachedRenderNormals = new List(vertexIds.Count); - cachedColors = new List(vertexIds.Count); - RecalcColorCache(); - } + public class Face + { + // The id of this face (unique within the mesh) + private readonly int _id; + // The ordered collection of vertex ids that comprise the face, in clockwise order. + private readonly ReadOnlyCollection _vertexIds; + // We support two different triangulations as the renderer uses triangulation one way, and the modelling tools + // another. The renderer needs indices into its vertex buffer, and therefore wants triangulation to return triangle + // indices relative to the vertices in the face - it will apply an offset to those, and add them to the mesh's + // triangle array. More straightforwardly, modeling operations need to look up the relevant vertices from the + // MMesh, and hence require the vertex id. - // Constructs a face with an unset normal - the normal will be calculated - public static Face FaceWithPendingNormal(int id, ReadOnlyCollection vertexIds, FaceProperties properties) { - return new Face(id, vertexIds, properties); - } - - // Constructs a face with an unset normal - the normal will be calculated - public static Face FaceWithPendingNormal(int id, List vertices, FaceProperties properties) { - List indices = new List(vertices.Count); - for (int i = 0; i < vertices.Count; i++) { - indices.Add(vertices[i].id); - } - return new Face(id, indices.AsReadOnly(), properties); - } + // This triangulation is a list of triangles whose indices are vertex ids. This is used by mesh validation among + // others. This representation is used to allow easy lookup of vertex data from the MMesh. + private List _modelTriangulation; - /// - /// Constructs a face with the supplied normal. This constructor should only be used when a face is being created with - /// the same normal as a preexisting face - otherwise one of the normal calculating constructors should be used. - /// - /// Face id - /// Vertex ids for face in clockwise winding order. - /// Normal from another face this face should match - /// Face properties - public Face(int id, ReadOnlyCollection vertexIds, Vector3 normal, FaceProperties properties) { - _id = id; - _vertexIds = vertexIds; - _normal = normal; - - _properties = properties; - _modelTriangulation = null; - _renderTriangulation = null; - - // Allocating capacity here to avoid needing to do it on every fetch - capacity is retained after Clear(). - cachedMeshSpacePositions = new List(vertexIds.Count); - cachedRenderNormals = new List(vertexIds.Count); - cachedColors = new List(vertexIds.Count); - RecalcColorCache(); - } - - /// - /// Constructs a face, calculating its normal. - /// - /// Face id - /// Vertex ids for face in clockwise winding order. - /// Dictionary of vertex ids to vertex data. - /// Face properties - public Face(int id, ReadOnlyCollection vertexIds, Dictionary verticesById, FaceProperties properties) { - _id = id; - _vertexIds = vertexIds; - _normal = MeshMath.CalculateNormal(vertexIds, verticesById); - _properties = properties; - _modelTriangulation = null; - _renderTriangulation = null; - // Allocating capacity here to avoid needing to do it on every fetch - capacity is retained after Clear(). - cachedMeshSpacePositions = new List(vertexIds.Count); - cachedRenderNormals = new List(vertexIds.Count); - cachedColors = new List(vertexIds.Count); - RecalcColorCache(); - } - - /// - /// Private constructor - exists to support Clone(). - /// - private Face(int id, ReadOnlyCollection vertexIds, Vector3 normal, FaceProperties properties, - List modelTriangulation, List renderTriangulation, - List cachedMeshSpacePositions, List cachedRenderNormals, List cachedColors) { - _id = id; - _vertexIds = vertexIds; - _normal = normal; - _properties = properties; - _modelTriangulation = modelTriangulation; - _renderTriangulation = renderTriangulation; - - this.cachedMeshSpacePositions = cachedMeshSpacePositions; - this.cachedRenderNormals = cachedRenderNormals; - this.cachedColors = cachedColors; - } + // This triangulation is a list of triangles which index into this face's _vertexIds collection. This representation + // is required for adding triangles to a Unity Mesh, but isn't as helpful for general modelling operations. + private List _renderTriangulation; - /// - /// Returns the triangulation of this face, indexed with the mmesh's vertex ids. - /// - /// - public List GetTriangulation(MMesh mesh) { - if (_modelTriangulation != null) return _modelTriangulation; - - _modelTriangulation = FaceTriangulator.TriangulateFace(mesh, this); - return _modelTriangulation; - } - - /// - /// Returns the triangulation of this face, indexed with the mmesh's vertex ids. - /// - /// - public List GetTriangulation(MMesh.GeometryOperation operation) { - if (_modelTriangulation != null) return _modelTriangulation; - - _modelTriangulation = FaceTriangulator.TriangulateFace(operation, this); - return _modelTriangulation; - } - - /// - /// Returns the triangulation of this face, indexed with the indices of this face's vertexIds list. - /// ie, a triangle might be 0, 2, 3, referencing the vertices in _vertexIds[0], [2], and [3]. - /// This triangulation is used for rendering. - /// - /// - public List GetRenderTriangulation(MMesh mesh) { - if (_renderTriangulation != null) return _renderTriangulation; - - if (vertexIds.Count == 3) { - // Triangulating a triangle is pretty easy :-) - _renderTriangulation = new List() { new Triangle(0, 1, 2) }; - - } else if (vertexIds.Count == 4 && MeshHelper.IsQuadFaceConvex(mesh, this)) { - // Triangulating a convex quad is also pretty easy. - _renderTriangulation = new List() { new Triangle(0, 1, 2), new Triangle(0, 2, 3) }; - } - else { - List verts = new List(vertexIds.Count); - // We really want the offset into the list, so we re-id the vertex with its index. - for (int i = 0; i < vertexIds.Count; i++) { - verts.Add(new Vertex(i, mesh.VertexPositionInMeshCoords(vertexIds[i]))); + // The face normal. Prior to face being added to a mesh via committing a GeometryOperation, this may not be set + // (which is represented by having a value of Vector3.zero) + private Vector3 _normal; + + // The properties of the face - primarily the material. + private FaceProperties _properties; + + // Read-only getters. + public int id { get { return _id; } } + public ReadOnlyCollection vertexIds { get { return _vertexIds; } } + public Vector3 normal { get { return _normal; } } + public FaceProperties properties { get { return _properties; } } + + // Cached vertex data, used to optimize construction of full mesh data for rendering. + private List cachedMeshSpacePositions = null; + private List cachedColors = null; + private List cachedRenderNormals = null; + + /// + /// Constructs a face with no normal. This constructor should only be used when it is certain that the normal will + /// be calculated later (ie, by Mesh.CommitOperation) + /// + /// Face id + /// Vertex ids for face in clockwise winding order. + /// Face properties + private Face(int id, ReadOnlyCollection vertexIds, FaceProperties properties) + { + _id = id; + _vertexIds = vertexIds; + _normal = Vector3.zero; + + _properties = properties; + _modelTriangulation = null; + _renderTriangulation = null; + + // Allocating capacity here to avoid needing to do it on every fetch - capacity is retained after Clear(). + cachedMeshSpacePositions = new List(vertexIds.Count); + cachedRenderNormals = new List(vertexIds.Count); + cachedColors = new List(vertexIds.Count); + RecalcColorCache(); } - _renderTriangulation = FaceTriangulator.Triangulate(verts); - } - - return _renderTriangulation; - } - /// - /// Recalculate the normal for this face, using the supplied vertex data. - /// - public void RecalculateNormal(Dictionary verticesById) { - _normal = MeshMath.CalculateNormal(vertexIds, verticesById); - cachedRenderNormals.Clear(); - for (int i = 0; i < vertexIds.Count; i++) { - cachedRenderNormals.Add(_normal); - } - } + // Constructs a face with an unset normal - the normal will be calculated + public static Face FaceWithPendingNormal(int id, ReadOnlyCollection vertexIds, FaceProperties properties) + { + return new Face(id, vertexIds, properties); + } - /// - /// Clears the vertex cache (because one of them has been modified). Cache will be recalculated next time the - /// vertices are accessed. - /// - public void InvalidateVertexCache() { - cachedMeshSpacePositions.Clear(); - } - - /// - /// Returns a list of mesh space positions for each vertex in the face in clockwise order, used for building a - /// renderable mesh. - /// - public List GetMeshSpaceVertices(MMesh mesh) { - if (cachedMeshSpacePositions.Count == 0) RecalcMeshSpacePositions(mesh); - return cachedMeshSpacePositions; - } + // Constructs a face with an unset normal - the normal will be calculated + public static Face FaceWithPendingNormal(int id, List vertices, FaceProperties properties) + { + List indices = new List(vertices.Count); + for (int i = 0; i < vertices.Count; i++) + { + indices.Add(vertices[i].id); + } + return new Face(id, indices.AsReadOnly(), properties); + } - /// - /// Returns a list of vertex colors for each vertex in the face in clockwise order, used for building a - /// renderable mesh. - /// - public List GetColors() { - return cachedColors; - } + /// + /// Constructs a face with the supplied normal. This constructor should only be used when a face is being created with + /// the same normal as a preexisting face - otherwise one of the normal calculating constructors should be used. + /// + /// Face id + /// Vertex ids for face in clockwise winding order. + /// Normal from another face this face should match + /// Face properties + public Face(int id, ReadOnlyCollection vertexIds, Vector3 normal, FaceProperties properties) + { + _id = id; + _vertexIds = vertexIds; + _normal = normal; - private void RecalcMeshSpacePositions(MMesh mesh) { - cachedMeshSpacePositions.Clear(); - for (int i = 0; i < vertexIds.Count; i++) { - cachedMeshSpacePositions.Add(mesh.VertexPositionInMeshCoords(vertexIds[i])); - } - } + _properties = properties; + _modelTriangulation = null; + _renderTriangulation = null; - private void RecalcColorCache() { - cachedColors.Clear(); - Color32 color = MaterialRegistry.GetMaterialColor32ById(_properties.materialId); - int count = vertexIds.Count; - for (int i = 0; i < count; i++) { - cachedColors.Add(color); - } - } + // Allocating capacity here to avoid needing to do it on every fetch - capacity is retained after Clear(). + cachedMeshSpacePositions = new List(vertexIds.Count); + cachedRenderNormals = new List(vertexIds.Count); + cachedColors = new List(vertexIds.Count); + RecalcColorCache(); + } - /// - /// Returns a list of vertex normals for each vertex in the face in clockwise order, used for building a - /// renderable mesh. - /// - public List GetRenderNormals(MMesh mesh) { - if (cachedRenderNormals.Count == 0) { - _normal = MeshMath.CalculateMeshSpaceNormal(this, mesh); - int count = vertexIds.Count; - for (int i = 0; i < count; i++) { - cachedRenderNormals.Add(_normal); + /// + /// Constructs a face, calculating its normal. + /// + /// Face id + /// Vertex ids for face in clockwise winding order. + /// Dictionary of vertex ids to vertex data. + /// Face properties + public Face(int id, ReadOnlyCollection vertexIds, Dictionary verticesById, FaceProperties properties) + { + _id = id; + _vertexIds = vertexIds; + _normal = MeshMath.CalculateNormal(vertexIds, verticesById); + _properties = properties; + _modelTriangulation = null; + _renderTriangulation = null; + // Allocating capacity here to avoid needing to do it on every fetch - capacity is retained after Clear(). + cachedMeshSpacePositions = new List(vertexIds.Count); + cachedRenderNormals = new List(vertexIds.Count); + cachedColors = new List(vertexIds.Count); + RecalcColorCache(); } - } - return cachedRenderNormals; - } - public void SetProperties(FaceProperties properties) { - _properties = properties; - RecalcColorCache(); - } - - public Face Clone() { - // Properties is a value object, so no need to clone. But we still need to - // make a new Face, so that the properties aren't shared. - return new Face( - _id, - _vertexIds, - _normal, - _properties, - _modelTriangulation, - _renderTriangulation, - new List(cachedMeshSpacePositions), - new List(cachedRenderNormals), - new List(cachedColors)); + /// + /// Private constructor - exists to support Clone(). + /// + private Face(int id, ReadOnlyCollection vertexIds, Vector3 normal, FaceProperties properties, + List modelTriangulation, List renderTriangulation, + List cachedMeshSpacePositions, List cachedRenderNormals, List cachedColors) + { + _id = id; + _vertexIds = vertexIds; + _normal = normal; + _properties = properties; + _modelTriangulation = modelTriangulation; + _renderTriangulation = renderTriangulation; + + this.cachedMeshSpacePositions = cachedMeshSpacePositions; + this.cachedRenderNormals = cachedRenderNormals; + this.cachedColors = cachedColors; + } + + /// + /// Returns the triangulation of this face, indexed with the mmesh's vertex ids. + /// + /// + public List GetTriangulation(MMesh mesh) + { + if (_modelTriangulation != null) return _modelTriangulation; + + _modelTriangulation = FaceTriangulator.TriangulateFace(mesh, this); + return _modelTriangulation; + } + + /// + /// Returns the triangulation of this face, indexed with the mmesh's vertex ids. + /// + /// + public List GetTriangulation(MMesh.GeometryOperation operation) + { + if (_modelTriangulation != null) return _modelTriangulation; + + _modelTriangulation = FaceTriangulator.TriangulateFace(operation, this); + return _modelTriangulation; + } + + /// + /// Returns the triangulation of this face, indexed with the indices of this face's vertexIds list. + /// ie, a triangle might be 0, 2, 3, referencing the vertices in _vertexIds[0], [2], and [3]. + /// This triangulation is used for rendering. + /// + /// + public List GetRenderTriangulation(MMesh mesh) + { + if (_renderTriangulation != null) return _renderTriangulation; + + if (vertexIds.Count == 3) + { + // Triangulating a triangle is pretty easy :-) + _renderTriangulation = new List() { new Triangle(0, 1, 2) }; + + } + else if (vertexIds.Count == 4 && MeshHelper.IsQuadFaceConvex(mesh, this)) + { + // Triangulating a convex quad is also pretty easy. + _renderTriangulation = new List() { new Triangle(0, 1, 2), new Triangle(0, 2, 3) }; + } + else + { + List verts = new List(vertexIds.Count); + // We really want the offset into the list, so we re-id the vertex with its index. + for (int i = 0; i < vertexIds.Count; i++) + { + verts.Add(new Vertex(i, mesh.VertexPositionInMeshCoords(vertexIds[i]))); + } + _renderTriangulation = FaceTriangulator.Triangulate(verts); + } + + return _renderTriangulation; + } + + /// + /// Recalculate the normal for this face, using the supplied vertex data. + /// + public void RecalculateNormal(Dictionary verticesById) + { + _normal = MeshMath.CalculateNormal(vertexIds, verticesById); + cachedRenderNormals.Clear(); + for (int i = 0; i < vertexIds.Count; i++) + { + cachedRenderNormals.Add(_normal); + } + } + + /// + /// Clears the vertex cache (because one of them has been modified). Cache will be recalculated next time the + /// vertices are accessed. + /// + public void InvalidateVertexCache() + { + cachedMeshSpacePositions.Clear(); + } + + /// + /// Returns a list of mesh space positions for each vertex in the face in clockwise order, used for building a + /// renderable mesh. + /// + public List GetMeshSpaceVertices(MMesh mesh) + { + if (cachedMeshSpacePositions.Count == 0) RecalcMeshSpacePositions(mesh); + return cachedMeshSpacePositions; + } + + /// + /// Returns a list of vertex colors for each vertex in the face in clockwise order, used for building a + /// renderable mesh. + /// + public List GetColors() + { + return cachedColors; + } + + private void RecalcMeshSpacePositions(MMesh mesh) + { + cachedMeshSpacePositions.Clear(); + for (int i = 0; i < vertexIds.Count; i++) + { + cachedMeshSpacePositions.Add(mesh.VertexPositionInMeshCoords(vertexIds[i])); + } + } + + private void RecalcColorCache() + { + cachedColors.Clear(); + Color32 color = MaterialRegistry.GetMaterialColor32ById(_properties.materialId); + int count = vertexIds.Count; + for (int i = 0; i < count; i++) + { + cachedColors.Add(color); + } + } + + /// + /// Returns a list of vertex normals for each vertex in the face in clockwise order, used for building a + /// renderable mesh. + /// + public List GetRenderNormals(MMesh mesh) + { + if (cachedRenderNormals.Count == 0) + { + _normal = MeshMath.CalculateMeshSpaceNormal(this, mesh); + int count = vertexIds.Count; + for (int i = 0; i < count; i++) + { + cachedRenderNormals.Add(_normal); + } + } + return cachedRenderNormals; + } + + public void SetProperties(FaceProperties properties) + { + _properties = properties; + RecalcColorCache(); + } + + public Face Clone() + { + // Properties is a value object, so no need to clone. But we still need to + // make a new Face, so that the properties aren't shared. + return new Face( + _id, + _vertexIds, + _normal, + _properties, + _modelTriangulation, + _renderTriangulation, + new List(cachedMeshSpacePositions), + new List(cachedRenderNormals), + new List(cachedColors)); + } } - } } diff --git a/Assets/Scripts/model/core/FaceKey.cs b/Assets/Scripts/model/core/FaceKey.cs index 33656ccc..f14d1552 100644 --- a/Assets/Scripts/model/core/FaceKey.cs +++ b/Assets/Scripts/model/core/FaceKey.cs @@ -12,35 +12,41 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace com.google.apps.peltzer.client.model.core { - /// - /// A canonical id for a face, which includes the id of the mesh it belongs to. - /// - public class FaceKey { - private readonly int _meshId; - private readonly int _faceId; - private readonly int _hashCode; +namespace com.google.apps.peltzer.client.model.core +{ + /// + /// A canonical id for a face, which includes the id of the mesh it belongs to. + /// + public class FaceKey + { + private readonly int _meshId; + private readonly int _faceId; + private readonly int _hashCode; - public FaceKey(int meshId, int faceId) { - _meshId = meshId; - _faceId = faceId; - // 31 is a good number: http://stackoverflow.com/questions/299304/why-does-javas-hashcode-in-string-use-31-as-a-multiplier - _hashCode = (151 + meshId) * 31 + faceId; - } + public FaceKey(int meshId, int faceId) + { + _meshId = meshId; + _faceId = faceId; + // 31 is a good number: http://stackoverflow.com/questions/299304/why-does-javas-hashcode-in-string-use-31-as-a-multiplier + _hashCode = (151 + meshId) * 31 + faceId; + } - public override bool Equals(object obj) { - return Equals(obj as FaceKey); - } + public override bool Equals(object obj) + { + return Equals(obj as FaceKey); + } - public bool Equals(FaceKey otherKey) { - return otherKey != null && _faceId == otherKey._faceId && _meshId == otherKey._meshId; - } + public bool Equals(FaceKey otherKey) + { + return otherKey != null && _faceId == otherKey._faceId && _meshId == otherKey._meshId; + } - public override int GetHashCode() { - return _hashCode; - } + public override int GetHashCode() + { + return _hashCode; + } - public int meshId { get { return _meshId; } } - public int faceId { get { return _faceId; } } - } + public int meshId { get { return _meshId; } } + public int faceId { get { return _faceId; } } + } } diff --git a/Assets/Scripts/model/core/FaceProperties.cs b/Assets/Scripts/model/core/FaceProperties.cs index cbccc506..fc594794 100644 --- a/Assets/Scripts/model/core/FaceProperties.cs +++ b/Assets/Scripts/model/core/FaceProperties.cs @@ -12,16 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace com.google.apps.peltzer.client.model.core { +namespace com.google.apps.peltzer.client.model.core +{ - /// - /// Properties for Faces. A value-type. - /// - public struct FaceProperties { - public int materialId { get; private set; } + /// + /// Properties for Faces. A value-type. + /// + public struct FaceProperties + { + public int materialId { get; private set; } - public FaceProperties(int materialId) { - this.materialId = materialId; + public FaceProperties(int materialId) + { + this.materialId = materialId; + } } - } } diff --git a/Assets/Scripts/model/core/GeometryOperation.cs b/Assets/Scripts/model/core/GeometryOperation.cs index 3b7512bc..889b645e 100644 --- a/Assets/Scripts/model/core/GeometryOperation.cs +++ b/Assets/Scripts/model/core/GeometryOperation.cs @@ -19,128 +19,140 @@ using System.Collections.ObjectModel; using UnityEngine; -namespace com.google.apps.peltzer.client.model.core { - // Partial to allow GeometryOperation to be an inner class of MMesh without needing to live in MMesh.cs - public partial class MMesh { - /// - /// Represents and manages an operation on the geometry of a MMesh. Use pattern is to call MMesh.StartOperation to - /// begin the operation, the various Add/Modify/Delete methods to compose the operation, and then Commit() to commit - /// the operation. - /// Changes are applied in the following order: - /// 1. Deletion of Faces - /// 2. Deletion of Vertices - /// 3. Addition/Modification of Faces - /// 4. Addition/Modification of Vertices - /// 5. Update of reverse table - /// - /// No validation of operations (ie, can't create a face using a deleted vertex id) is performed yet, but may be done - /// in the future (perhaps only in editor mode). - /// - public class GeometryOperation { - // Has this operation already been committed - an operation should not be committed twice. - private bool committed; - - // The MMesh the geometry operations this class contains is targeting. - private MMesh targetMesh; - - // Treat as if friend-classed to MMesh. Don't directly use from anywhere else. - public HashSet addedVertices; - - // Treat as if friend-classed to MMesh. Don't directly use from anywhere else. - public Dictionary modifiedVertices; - - // Treat as if friend-classed to MMesh. Don't directly use from anywhere else. - public HashSet deletedVertices; - - // Treat as if friend-classed to MMesh. Don't directly use from anywhere else. - public HashSet addedFaces; - - // Treat as if friend-classed to MMesh. Don't directly use from anywhere else. - public Dictionary modifiedFaces; - - // Treat as if friend-classed to MMesh. Don't directly use from anywhere else. - public HashSet deletedFaces; - - private readonly System.Random random = new System.Random(); - - /// - /// DO NOT USE THIS CONSTRUCTOR. - /// - /// Unless you're in MMesh, then it's okay. - /// - /// Blame C# for not having anything analagous to package private or friend classes. - /// - /// The mmesh this operation is targeting. - public GeometryOperation(MMesh targetMesh) { - this.targetMesh = targetMesh; - committed = false; - addedVertices = new HashSet(); - modifiedVertices = new Dictionary(); - deletedVertices = new HashSet(); - - addedFaces = new HashSet(); - modifiedFaces = new Dictionary(); - deletedFaces = new HashSet(); - } - - /// - /// Gets the position of the specified vertex in model space, using updated data from the GeometryOperation if - /// possible. - /// - public Vector3 GetCurrentVertexPositionModelSpace(int id) { - Vertex vertex; - if (modifiedVertices.TryGetValue(id, out vertex)) { - return targetMesh.MeshCoordsToModelCoords(vertex.loc); - } +namespace com.google.apps.peltzer.client.model.core +{ + // Partial to allow GeometryOperation to be an inner class of MMesh without needing to live in MMesh.cs + public partial class MMesh + { + /// + /// Represents and manages an operation on the geometry of a MMesh. Use pattern is to call MMesh.StartOperation to + /// begin the operation, the various Add/Modify/Delete methods to compose the operation, and then Commit() to commit + /// the operation. + /// Changes are applied in the following order: + /// 1. Deletion of Faces + /// 2. Deletion of Vertices + /// 3. Addition/Modification of Faces + /// 4. Addition/Modification of Vertices + /// 5. Update of reverse table + /// + /// No validation of operations (ie, can't create a face using a deleted vertex id) is performed yet, but may be done + /// in the future (perhaps only in editor mode). + /// + public class GeometryOperation + { + // Has this operation already been committed - an operation should not be committed twice. + private bool committed; + + // The MMesh the geometry operations this class contains is targeting. + private MMesh targetMesh; + + // Treat as if friend-classed to MMesh. Don't directly use from anywhere else. + public HashSet addedVertices; + + // Treat as if friend-classed to MMesh. Don't directly use from anywhere else. + public Dictionary modifiedVertices; + + // Treat as if friend-classed to MMesh. Don't directly use from anywhere else. + public HashSet deletedVertices; + + // Treat as if friend-classed to MMesh. Don't directly use from anywhere else. + public HashSet addedFaces; + + // Treat as if friend-classed to MMesh. Don't directly use from anywhere else. + public Dictionary modifiedFaces; + + // Treat as if friend-classed to MMesh. Don't directly use from anywhere else. + public HashSet deletedFaces; + + private readonly System.Random random = new System.Random(); + + /// + /// DO NOT USE THIS CONSTRUCTOR. + /// + /// Unless you're in MMesh, then it's okay. + /// + /// Blame C# for not having anything analagous to package private or friend classes. + /// + /// The mmesh this operation is targeting. + public GeometryOperation(MMesh targetMesh) + { + this.targetMesh = targetMesh; + committed = false; + addedVertices = new HashSet(); + modifiedVertices = new Dictionary(); + deletedVertices = new HashSet(); + + addedFaces = new HashSet(); + modifiedFaces = new Dictionary(); + deletedFaces = new HashSet(); + } + + /// + /// Gets the position of the specified vertex in model space, using updated data from the GeometryOperation if + /// possible. + /// + public Vector3 GetCurrentVertexPositionModelSpace(int id) + { + Vertex vertex; + if (modifiedVertices.TryGetValue(id, out vertex)) + { + return targetMesh.MeshCoordsToModelCoords(vertex.loc); + } #if UNITY_EDITOR if (deletedVertices.Contains(id)) { throw new Exception("Tried to get position of deleted vertex: " + id); } #endif - return targetMesh.VertexPositionInModelCoords(id); - } - - /// - /// Gets the position of the specified vertex in mesh space, using updated data from the GeometryOperation if - /// possible. - /// - public Vector3 GetCurrentVertexPositionMeshSpace(int id) { - Vertex vertex; - if (modifiedVertices.TryGetValue(id, out vertex)) { - return vertex.loc; - } + return targetMesh.VertexPositionInModelCoords(id); + } + + /// + /// Gets the position of the specified vertex in mesh space, using updated data from the GeometryOperation if + /// possible. + /// + public Vector3 GetCurrentVertexPositionMeshSpace(int id) + { + Vertex vertex; + if (modifiedVertices.TryGetValue(id, out vertex)) + { + return vertex.loc; + } #if UNITY_EDITOR if (deletedVertices.Contains(id)) { throw new Exception("Tried to get position of deleted vertex: " + id); } #endif - return targetMesh.VertexPositionInMeshCoords(id); - } - - /// - /// Gets the Vertex, using updated data from the GeometryOperation if possible; - /// - public Vertex GetCurrentVertex(int id) { - Vertex vertex; - if (modifiedVertices.TryGetValue(id, out vertex)) { - return vertex; - } + return targetMesh.VertexPositionInMeshCoords(id); + } + + /// + /// Gets the Vertex, using updated data from the GeometryOperation if possible; + /// + public Vertex GetCurrentVertex(int id) + { + Vertex vertex; + if (modifiedVertices.TryGetValue(id, out vertex)) + { + return vertex; + } #if UNITY_EDITOR if (deletedVertices.Contains(id)) { throw new Exception("Tried to get position of deleted vertex: " + id); } #endif - return targetMesh.verticesById[id]; - } - - /// - /// Gets the data for the specified face, using updated data from the GeometryOperation if possible. - /// - public Face GetCurrentFace(int id) { - Face face; - if (modifiedFaces.TryGetValue(id, out face)) { - return face; - } + return targetMesh.verticesById[id]; + } + + /// + /// Gets the data for the specified face, using updated data from the GeometryOperation if possible. + /// + public Face GetCurrentFace(int id) + { + Face face; + if (modifiedFaces.TryGetValue(id, out face)) + { + return face; + } #if UNITY_EDITOR // Only perform check when in editor. if (deletedFaces.Contains(id)) { @@ -148,373 +160,417 @@ public Face GetCurrentFace(int id) { } #endif - return targetMesh.GetFace(id); - } - - /// - /// Gets the data for the specified face, using updated data from the GeometryOperation if possible. - /// - public bool TryGetCurrentFace(int id, out Face outFace) { - outFace = null; - - if (modifiedFaces.TryGetValue(id, out outFace)) { - return true; - } - if (deletedFaces.Contains(id)) { - return false; - } + return targetMesh.GetFace(id); + } - return targetMesh.TryGetFace(id, out outFace); - } + /// + /// Gets the data for the specified face, using updated data from the GeometryOperation if possible. + /// + public bool TryGetCurrentFace(int id, out Face outFace) + { + outFace = null; + + if (modifiedFaces.TryGetValue(id, out outFace)) + { + return true; + } + if (deletedFaces.Contains(id)) + { + return false; + } + + return targetMesh.TryGetFace(id, out outFace); + } - private int GenerateVertexId() { - return targetMesh.GenerateVertexId(addedVertices); - } + private int GenerateVertexId() + { + return targetMesh.GenerateVertexId(addedVertices); + } - private int GenerateFaceId() { - return targetMesh.GenerateFaceId(addedFaces); - } + private int GenerateFaceId() + { + return targetMesh.GenerateFaceId(addedFaces); + } - /// - /// Adds a vertex with the given model space position to the mesh. - /// - /// The model space coordinates of the new vertex. - public Vertex AddVertexModelSpace(Vector3 pos) { - Vertex vertex = new Vertex(GenerateVertexId(), targetMesh.ModelCoordsToMeshCoords(pos)); + /// + /// Adds a vertex with the given model space position to the mesh. + /// + /// The model space coordinates of the new vertex. + public Vertex AddVertexModelSpace(Vector3 pos) + { + Vertex vertex = new Vertex(GenerateVertexId(), targetMesh.ModelCoordsToMeshCoords(pos)); #if GEOM_OP_VERBOSE_LOGGING Debug.Log("Adding vertex " + vertex.id); - #endif - addedVertices.Add(vertex.id); - modifiedVertices[vertex.id] = vertex; - return vertex; - } - - /// - /// Adds a vertex with the given mesh space position to the mesh. - /// - /// The mesh space coordinates of the new vertex. - public Vertex AddVertexMeshSpace(Vector3 pos) { - Vertex vertex = new Vertex(GenerateVertexId(), pos); +#endif + addedVertices.Add(vertex.id); + modifiedVertices[vertex.id] = vertex; + return vertex; + } + + /// + /// Adds a vertex with the given mesh space position to the mesh. + /// + /// The mesh space coordinates of the new vertex. + public Vertex AddVertexMeshSpace(Vector3 pos) + { + Vertex vertex = new Vertex(GenerateVertexId(), pos); #if GEOM_OP_VERBOSE_LOGGING Debug.Log("Adding vertex " + vertex.id); - #endif - - addedVertices.Add(vertex.id); - modifiedVertices[vertex.id] = vertex; - return vertex; - } - - /// - /// Modifies the vertex with the supplied ID, moving it to the supplied model space coordinates. - /// - /// The id of the target vertex. - /// The model space coordinates to move the vertex to. - public Vertex ModifyVertexModelSpace(int id, Vector3 pos) { +#endif + + addedVertices.Add(vertex.id); + modifiedVertices[vertex.id] = vertex; + return vertex; + } + + /// + /// Modifies the vertex with the supplied ID, moving it to the supplied model space coordinates. + /// + /// The id of the target vertex. + /// The model space coordinates to move the vertex to. + public Vertex ModifyVertexModelSpace(int id, Vector3 pos) + { #if UNITY_EDITOR CheckVertexIsModifiable(id); #endif - Vertex vertex = new Vertex(id, targetMesh.ModelCoordsToMeshCoords(pos)); - modifiedVertices[id] = vertex; - return vertex; - } - - /// - /// Modifies the vertex with the supplied ID, moving it to the supplied mesh space coordinates. - /// - /// The id of the target vertex. - /// The mesh space coordinates to move the vertex to. - public Vertex ModifyVertexMeshSpace(int id, Vector3 pos) { + Vertex vertex = new Vertex(id, targetMesh.ModelCoordsToMeshCoords(pos)); + modifiedVertices[id] = vertex; + return vertex; + } + + /// + /// Modifies the vertex with the supplied ID, moving it to the supplied mesh space coordinates. + /// + /// The id of the target vertex. + /// The mesh space coordinates to move the vertex to. + public Vertex ModifyVertexMeshSpace(int id, Vector3 pos) + { #if UNITY_EDITOR CheckVertexIsModifiable(id); #endif - Vertex vertex = new Vertex(id, pos); - modifiedVertices[id] = vertex; - return vertex; - } - - /// - /// Modifies the vertex with the id and mesh position contained in the supplied Vertex. - /// - /// Vertex containing updated data. - public void ModifyVertex(Vertex vertex) { + Vertex vertex = new Vertex(id, pos); + modifiedVertices[id] = vertex; + return vertex; + } + + /// + /// Modifies the vertex with the id and mesh position contained in the supplied Vertex. + /// + /// Vertex containing updated data. + public void ModifyVertex(Vertex vertex) + { #if UNITY_EDITOR CheckVertexIsModifiable(vertex.id); #endif - modifiedVertices[vertex.id] = vertex; - } + modifiedVertices[vertex.id] = vertex; + } - /// - /// Modifies the vertices with the ids and mesh space position contained in the supplied Dictionary. - /// - /// Dictionary containing id to updated Vertex map. - public void ModifyVertices(Dictionary vertices) { - foreach (KeyValuePair pair in vertices) { + /// + /// Modifies the vertices with the ids and mesh space position contained in the supplied Dictionary. + /// + /// Dictionary containing id to updated Vertex map. + public void ModifyVertices(Dictionary vertices) + { + foreach (KeyValuePair pair in vertices) + { #if UNITY_EDITOR CheckVertexIsModifiable(pair.Key); #endif - modifiedVertices[pair.Key] = pair.Value; - } - } - - /// - /// Modifies the vertices with the ids and mesh space position contained in the supplied Vertex enumerable. - /// - /// Dictionary containing id to updated Vertex map. - public void ModifyVertices(Dictionary.ValueCollection vertices) { - foreach (Vertex vertex in vertices) { + modifiedVertices[pair.Key] = pair.Value; + } + } + + /// + /// Modifies the vertices with the ids and mesh space position contained in the supplied Vertex enumerable. + /// + /// Dictionary containing id to updated Vertex map. + public void ModifyVertices(Dictionary.ValueCollection vertices) + { + foreach (Vertex vertex in vertices) + { #if UNITY_EDITOR CheckVertexIsModifiable(vertex.id); #endif - modifiedVertices[vertex.id] = vertex; - } - } + modifiedVertices[vertex.id] = vertex; + } + } - /// - /// Deletes the vertex with the supplied ID. - /// - public void DeleteVertex(int id) { + /// + /// Deletes the vertex with the supplied ID. + /// + public void DeleteVertex(int id) + { #if GEOM_OP_VERBOSE_LOGGING Debug.Log("Deleting vertex " + id); - #endif - deletedVertices.Add(id); - } - - /// - /// Returns the target MMesh for this operation. - /// - public MMesh GetMesh() { - return targetMesh; - } - - /// - /// Adds a face composed of the supplied vertex indices, and with the supplied properties. - /// - /// The indices that should form the face - /// The face properties of the new face - public Face AddFace(List indices, FaceProperties properties) { - Face face = Face.FaceWithPendingNormal(GenerateFaceId(), - indices.AsReadOnly(), - properties); - addedFaces.Add(face.id); - modifiedFaces[face.id] = face; - return face; - } - - /// - /// Adds a face composed of the supplied vertex indices, and with the supplied properties. - /// - /// The vertices that should form the face - /// The face properties of the new face - public Face AddFace(List vertices, FaceProperties properties) { - Face face = Face.FaceWithPendingNormal(GenerateFaceId(), - vertices, - properties); - addedFaces.Add(face.id); - modifiedFaces[face.id] = face; - return face; - } - - /// - /// Replaces the face with the supplied id, changing its indices and properties to the supplied ones. - /// - /// The face id to change - /// The updated indices that should comprise the face - /// The updated face properties of the face - public Face ModifyFace(int id, List indices, FaceProperties properties) { +#endif + deletedVertices.Add(id); + } + + /// + /// Returns the target MMesh for this operation. + /// + public MMesh GetMesh() + { + return targetMesh; + } + + /// + /// Adds a face composed of the supplied vertex indices, and with the supplied properties. + /// + /// The indices that should form the face + /// The face properties of the new face + public Face AddFace(List indices, FaceProperties properties) + { + Face face = Face.FaceWithPendingNormal(GenerateFaceId(), + indices.AsReadOnly(), + properties); + addedFaces.Add(face.id); + modifiedFaces[face.id] = face; + return face; + } + + /// + /// Adds a face composed of the supplied vertex indices, and with the supplied properties. + /// + /// The vertices that should form the face + /// The face properties of the new face + public Face AddFace(List vertices, FaceProperties properties) + { + Face face = Face.FaceWithPendingNormal(GenerateFaceId(), + vertices, + properties); + addedFaces.Add(face.id); + modifiedFaces[face.id] = face; + return face; + } + + /// + /// Replaces the face with the supplied id, changing its indices and properties to the supplied ones. + /// + /// The face id to change + /// The updated indices that should comprise the face + /// The updated face properties of the face + public Face ModifyFace(int id, List indices, FaceProperties properties) + { #if GEOM_OP_VERBOSE_LOGGING Debug.Log("Modifying face " + id); - #endif +#endif #if UNITY_EDITOR CheckFaceIsModifiable(id); #endif - Face face = Face.FaceWithPendingNormal(id, - indices.AsReadOnly(), - properties); - modifiedFaces[id] = face; - return face; - } - - /// - /// Replaces the face with the supplied id, changing its indices and properties to the supplied ones. - /// - /// The face id to change - /// The updated indices that should comprise the face - /// The updated face properties of the face - public Face ModifyFace(int id, ReadOnlyCollection indices, FaceProperties properties) { + Face face = Face.FaceWithPendingNormal(id, + indices.AsReadOnly(), + properties); + modifiedFaces[id] = face; + return face; + } + + /// + /// Replaces the face with the supplied id, changing its indices and properties to the supplied ones. + /// + /// The face id to change + /// The updated indices that should comprise the face + /// The updated face properties of the face + public Face ModifyFace(int id, ReadOnlyCollection indices, FaceProperties properties) + { #if GEOM_OP_VERBOSE_LOGGING Debug.Log("Modifying face " + id); foreach (int vertId in indices) { Debug.Log(" " + vertId); } - #endif +#endif #if UNITY_EDITOR CheckFaceIsModifiable(id); #endif - Face face = Face.FaceWithPendingNormal(id, - indices, - properties); - modifiedFaces[id] = face; - return face; - } - - private void CheckFaceIsModifiable(int id) { - if (deletedFaces.Contains(id)) { - throw new Exception("Face " + id + " is in deletedFaces and can't be modified."); - } - if (!(addedFaces.Contains(id) || targetMesh.HasFace(id))) { - throw new Exception("Face " + id + " doesn't exist either in mesh or in added faces."); - } - } + Face face = Face.FaceWithPendingNormal(id, + indices, + properties); + modifiedFaces[id] = face; + return face; + } - private void CheckVertexIsModifiable(int id) { - if (deletedVertices.Contains(id)) { - throw new Exception("Vertex " + id + " is in deletedVertices and can't be modified."); - } - if (!(addedVertices.Contains(id) || targetMesh.verticesById.ContainsKey(id))) { - throw new Exception("Vertex " + id + " doesn't exist either in mesh or in added vertices."); - } - } + private void CheckFaceIsModifiable(int id) + { + if (deletedFaces.Contains(id)) + { + throw new Exception("Face " + id + " is in deletedFaces and can't be modified."); + } + if (!(addedFaces.Contains(id) || targetMesh.HasFace(id))) + { + throw new Exception("Face " + id + " doesn't exist either in mesh or in added faces."); + } + } + + private void CheckVertexIsModifiable(int id) + { + if (deletedVertices.Contains(id)) + { + throw new Exception("Vertex " + id + " is in deletedVertices and can't be modified."); + } + if (!(addedVertices.Contains(id) || targetMesh.verticesById.ContainsKey(id))) + { + throw new Exception("Vertex " + id + " doesn't exist either in mesh or in added vertices."); + } + } - /// - /// Uses the supplied face to replace the face that has the same id. - /// - public Face ModifyFace(Face face) { - #if GEOM_OP_VERBOSE_LOGGING + /// + /// Uses the supplied face to replace the face that has the same id. + /// + public Face ModifyFace(Face face) + { +#if GEOM_OP_VERBOSE_LOGGING Debug.Log("Modifying face " + face.id + " to"); foreach (int vertId in face.vertexIds) { Debug.Log(" " + vertId); } - #endif - #if UNITY_EDITOR +#endif +#if UNITY_EDITOR CheckFaceIsModifiable(face.id); - #endif - modifiedFaces[face.id] = face; - return face; - } - - /// - /// Deletes the face with the supplied id. - /// - public void DeleteFace(int id) { - #if GEOM_OP_VERBOSE_LOGGING +#endif + modifiedFaces[face.id] = face; + return face; + } + + /// + /// Deletes the face with the supplied id. + /// + public void DeleteFace(int id) + { +#if GEOM_OP_VERBOSE_LOGGING Debug.Log("Deleting face " + id); - #endif - deletedFaces.Add(id); - } - - /// - /// Commits a geometry operation targeting this mesh. - /// - /// - private void CommitOperation(bool recalculateNormals) { - // Until all tools cause the reverse table to be updated - it will enter inaccurate - // states. Until then, we need to disable updates and use of it (while still letting allowing us to build out and - // test its functionality for controlled cases). - - #if UNITY_EDITOR +#endif + deletedFaces.Add(id); + } + + /// + /// Commits a geometry operation targeting this mesh. + /// + /// + private void CommitOperation(bool recalculateNormals) + { + // Until all tools cause the reverse table to be updated - it will enter inaccurate + // states. Until then, we need to disable updates and use of it (while still letting allowing us to build out and + // test its functionality for controlled cases). + +#if UNITY_EDITOR if (!targetMesh.operationInProgress) { throw new Exception("Attempted to commit operation when mesh has no operation in progress."); } - #endif +#endif - foreach (int id in deletedFaces) { - foreach (int vertIndex in targetMesh.facesById[id].vertexIds) { - #if GEOM_OP_VERBOSE_LOGGING + foreach (int id in deletedFaces) + { + foreach (int vertIndex in targetMesh.facesById[id].vertexIds) + { +#if GEOM_OP_VERBOSE_LOGGING Debug.Log("RT Update: Removing face " + id + " from vert " + vertIndex); - #endif - targetMesh.reverseTable[vertIndex].Remove(id); - } - - targetMesh.facesById.Remove(id); - } - - foreach (int id in deletedVertices) { - targetMesh.verticesById.Remove(id); - #if GEOM_OP_VERBOSE_LOGGING - Debug.Log("RT Update: Removing vertex " + id); - #endif - targetMesh.reverseTable.Remove(id); - - } +#endif + targetMesh.reverseTable[vertIndex].Remove(id); + } - if (modifiedFaces.Count > 0 || modifiedVertices.Count > 0) { - HashSet faceIdsToRecalc = new HashSet(); + targetMesh.facesById.Remove(id); + } - // Handle new and modified faces - foreach (KeyValuePair pair in modifiedFaces) { - Face oldFace; - if (targetMesh.TryGetFace(pair.Key, out oldFace)) { - foreach (int vertIndex in oldFace.vertexIds) { - if(deletedVertices.Contains(vertIndex)) continue; - #if GEOM_OP_VERBOSE_LOGGING + foreach (int id in deletedVertices) + { + targetMesh.verticesById.Remove(id); +#if GEOM_OP_VERBOSE_LOGGING + Debug.Log("RT Update: Removing vertex " + id); +#endif + targetMesh.reverseTable.Remove(id); + + } + + if (modifiedFaces.Count > 0 || modifiedVertices.Count > 0) + { + HashSet faceIdsToRecalc = new HashSet(); + + // Handle new and modified faces + foreach (KeyValuePair pair in modifiedFaces) + { + Face oldFace; + if (targetMesh.TryGetFace(pair.Key, out oldFace)) + { + foreach (int vertIndex in oldFace.vertexIds) + { + if (deletedVertices.Contains(vertIndex)) continue; +#if GEOM_OP_VERBOSE_LOGGING Debug.Log("RT Update: Removing face " + pair.Key + " from vert " + vertIndex); - #endif - targetMesh.reverseTable[vertIndex].Remove(pair.Key); - } - } - - targetMesh.facesById[pair.Key] = pair.Value; - - foreach (int vertIndex in pair.Value.vertexIds) { - HashSet faceSet; - if (!targetMesh.reverseTable.TryGetValue(vertIndex, out faceSet)) { - faceSet = new HashSet(); - #if GEOM_OP_VERBOSE_LOGGING +#endif + targetMesh.reverseTable[vertIndex].Remove(pair.Key); + } + } + + targetMesh.facesById[pair.Key] = pair.Value; + + foreach (int vertIndex in pair.Value.vertexIds) + { + HashSet faceSet; + if (!targetMesh.reverseTable.TryGetValue(vertIndex, out faceSet)) + { + faceSet = new HashSet(); +#if GEOM_OP_VERBOSE_LOGGING Debug.Log("RT Update: Adding vertex " + vertIndex); - #endif - targetMesh.reverseTable[vertIndex] = faceSet; - } - #if GEOM_OP_VERBOSE_LOGGING +#endif + targetMesh.reverseTable[vertIndex] = faceSet; + } +#if GEOM_OP_VERBOSE_LOGGING Debug.Log("RT Update: Adding face " + pair.Key + " to vert " + vertIndex); - #endif - faceSet.Add(pair.Key); - - } - faceIdsToRecalc.Add(pair.Key); - } +#endif + faceSet.Add(pair.Key); - // Handle new and modified verts - foreach (KeyValuePair pair in modifiedVertices) { - #if GEOM_OP_VERBOSE_LOGGING - Debug.Log("RT Update: Modifying vertex " + pair.Key); - #endif - targetMesh.verticesById[pair.Key] = pair.Value; - foreach (int faceId in targetMesh.reverseTable[pair.Key]) { - faceIdsToRecalc.Add(faceId); - } - } + } + faceIdsToRecalc.Add(pair.Key); + } - foreach (int faceId in faceIdsToRecalc) { - Face face = targetMesh.facesById[faceId]; - face.InvalidateVertexCache(); - if (recalculateNormals) { - face.RecalculateNormal(targetMesh.verticesById); - } - } - } + // Handle new and modified verts + foreach (KeyValuePair pair in modifiedVertices) + { +#if GEOM_OP_VERBOSE_LOGGING + Debug.Log("RT Update: Modifying vertex " + pair.Key); +#endif + targetMesh.verticesById[pair.Key] = pair.Value; + foreach (int faceId in targetMesh.reverseTable[pair.Key]) + { + faceIdsToRecalc.Add(faceId); + } + } + + foreach (int faceId in faceIdsToRecalc) + { + Face face = targetMesh.facesById[faceId]; + face.InvalidateVertexCache(); + if (recalculateNormals) + { + face.RecalculateNormal(targetMesh.verticesById); + } + } + } - #if UNITY_EDITOR +#if UNITY_EDITOR try { - #endif - targetMesh.FinishOperation(); - #if UNITY_EDITOR +#endif + targetMesh.FinishOperation(); +#if UNITY_EDITOR } catch (Exception ex) { DumpOperation(); throw ex; } - #endif - } - - /// - /// Commits the changes in this geometry operation to the target MMesh. Should only be called once. - /// - /// If in Unity Editor, this can throw an exception if the operation has already been - /// committed. - public void Commit() { - if (!committed) { - #if GEOM_OP_VERBOSE_LOGGING +#endif + } + + /// + /// Commits the changes in this geometry operation to the target MMesh. Should only be called once. + /// + /// If in Unity Editor, this can throw an exception if the operation has already been + /// committed. + public void Commit() + { + if (!committed) + { +#if GEOM_OP_VERBOSE_LOGGING Debug.Log("Mesh " + targetMesh.id + " commit info."); foreach (Vertex vert in targetMesh.verticesById.Values) { Debug.Log("Mesh has vert " + vert.id); @@ -530,57 +586,67 @@ public void Commit() { foreach (Vertex vert in modifiedVertices.Values) { Debug.Log("Modification has vert " + vert.id); } - #endif +#endif - CommitOperation(true /* recalculateNormals */); - return; - } - #if UNITY_EDITOR + CommitOperation(true /* recalculateNormals */); + return; + } +#if UNITY_EDITOR throw new Exception("Attemped to commit an already committed GeometryOperation."); - #endif - } - - /// - /// Commits the changes in this geometry operation to the target MMesh without triggering recalculation for modified - /// faces. Should only be called once. - /// - public void CommitWithoutRecalculation() { - if (!committed) { - CommitOperation(false /* recalculateNormals */); - return; - } - #if UNITY_EDITOR +#endif + } + + /// + /// Commits the changes in this geometry operation to the target MMesh without triggering recalculation for modified + /// faces. Should only be called once. + /// + public void CommitWithoutRecalculation() + { + if (!committed) + { + CommitOperation(false /* recalculateNormals */); + return; + } +#if UNITY_EDITOR throw new Exception("Attemped to commit an already committed GeometryOperation."); - #endif - } +#endif + } - private void DumpOperation() { - foreach (int vertId in addedVertices) { - Debug.Log(" adding vert: " + vertId); - } - foreach (int vertId in deletedVertices) { - Debug.Log(" deleting vert: " + vertId); - } - foreach (int vertId in modifiedVertices.Keys) { - Debug.Log(" modifying vert: " + vertId + " to " + MeshUtil.Vector3ToString(modifiedVertices[vertId].loc)); - } - - foreach (int faceId in addedFaces) { - Debug.Log(" adding face: " + faceId); - } - foreach (int faceId in deletedFaces) { - Debug.Log(" deleting face: " + faceId); - } - foreach (int faceId in modifiedFaces.Keys) { - Debug.Log(" modifying face: " + faceId + " to :"); - String outString = "["; - foreach (int vertId in modifiedFaces[faceId].vertexIds) { - outString += vertId + ", "; - } - outString += "]"; - Debug.Log(" " + outString); + private void DumpOperation() + { + foreach (int vertId in addedVertices) + { + Debug.Log(" adding vert: " + vertId); + } + foreach (int vertId in deletedVertices) + { + Debug.Log(" deleting vert: " + vertId); + } + foreach (int vertId in modifiedVertices.Keys) + { + Debug.Log(" modifying vert: " + vertId + " to " + MeshUtil.Vector3ToString(modifiedVertices[vertId].loc)); + } + + foreach (int faceId in addedFaces) + { + Debug.Log(" adding face: " + faceId); + } + foreach (int faceId in deletedFaces) + { + Debug.Log(" deleting face: " + faceId); + } + foreach (int faceId in modifiedFaces.Keys) + { + Debug.Log(" modifying face: " + faceId + " to :"); + String outString = "["; + foreach (int vertId in modifiedFaces[faceId].vertexIds) + { + outString += vertId + ", "; + } + outString += "]"; + Debug.Log(" " + outString); + } + } } - } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/core/HideVideoViewerCommand.cs b/Assets/Scripts/model/core/HideVideoViewerCommand.cs index 1caa5e15..e0c594ae 100644 --- a/Assets/Scripts/model/core/HideVideoViewerCommand.cs +++ b/Assets/Scripts/model/core/HideVideoViewerCommand.cs @@ -14,17 +14,21 @@ using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.core { - /// - /// This command makes the video viewer invisible. It does not affect the Model. - /// - public class HideVideoViewerCommand : Command { - public void ApplyToModel(Model model) { - PeltzerMain.Instance.GetVideoViewer().SetActive(false); - } +namespace com.google.apps.peltzer.client.model.core +{ + /// + /// This command makes the video viewer invisible. It does not affect the Model. + /// + public class HideVideoViewerCommand : Command + { + public void ApplyToModel(Model model) + { + PeltzerMain.Instance.GetVideoViewer().SetActive(false); + } - public Command GetUndoCommand(Model model) { - return new ShowVideoViewerCommand(); + public Command GetUndoCommand(Model model) + { + return new ShowVideoViewerCommand(); + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/core/Hole.cs b/Assets/Scripts/model/core/Hole.cs index 24f38a04..69481487 100644 --- a/Assets/Scripts/model/core/Hole.cs +++ b/Assets/Scripts/model/core/Hole.cs @@ -17,29 +17,32 @@ using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.model.core { - - /// - /// A hole in a Face. - /// - public class Hole { - private readonly ReadOnlyCollection _vertexIds; - private ReadOnlyCollection _normals; - - // Read-only getters. - public ReadOnlyCollection vertexIds { get { return _vertexIds; } } - public ReadOnlyCollection normals { get { return _normals; } } +namespace com.google.apps.peltzer.client.model.core +{ /// - /// Create a new hole. + /// A hole in a Face. /// - /// Vertex ids of the border in counterclockwise order. - /// Normals for the given vertices. - public Hole(ReadOnlyCollection vertexIds, ReadOnlyCollection normals) { - AssertOrThrow.True(vertexIds.Count == normals.Count, - "Must have same number of vertices and normals."); - _vertexIds = vertexIds; - _normals = normals; + public class Hole + { + private readonly ReadOnlyCollection _vertexIds; + private ReadOnlyCollection _normals; + + // Read-only getters. + public ReadOnlyCollection vertexIds { get { return _vertexIds; } } + public ReadOnlyCollection normals { get { return _normals; } } + + /// + /// Create a new hole. + /// + /// Vertex ids of the border in counterclockwise order. + /// Normals for the given vertices. + public Hole(ReadOnlyCollection vertexIds, ReadOnlyCollection normals) + { + AssertOrThrow.True(vertexIds.Count == normals.Count, + "Must have same number of vertices and normals."); + _vertexIds = vertexIds; + _normals = normals; + } } - } } diff --git a/Assets/Scripts/model/core/MMesh.cs b/Assets/Scripts/model/core/MMesh.cs index a36ecc7a..1bbbc3fd 100644 --- a/Assets/Scripts/model/core/MMesh.cs +++ b/Assets/Scripts/model/core/MMesh.cs @@ -25,825 +25,936 @@ using com.google.apps.peltzer.client.model.util; using com.google.apps.peltzer.client.serialization; -namespace com.google.apps.peltzer.client.model.core { - /// - /// An MMesh represents a mesh in the model. Named 'MMesh' to avoid ambiguity with Unity meshes. An MMesh is - /// what you normally think of as a mesh: a piece of geometry made of vertices, edges and faces. - /// - /// Although not strictly enforced everywhere, a general principle is that once an MMesh is in the model, - /// it is immutable. Mutations of an MMesh actually consist of replacing it with a new MMesh that's the result - /// of the mutation. The fact that cloning an MMesh is relatively cheap makes that model viable. - /// - /// The coordinates of the components of a mesh (vertices, normals, etc) are represented in what is called - /// Mesh Space, which is the private frame of reference of the mesh itself (not shared with other meshes - /// in the model). The offset and rotation properties of the mesh indicate how to convert from Mesh Space to - /// Model Space. For example, if a mesh has a vertex with coordinates (1, 2, 3) in Mesh Space, and the offset - /// of the mesh is (1000, 2000, 3000), then the position of that vertex in Model Space would be (1001, 2002, 3003). - /// - /// An MMesh can be attached to (and detached from) a GameObject to make it render on the screen. In particular, - /// it is rendered by the MeshWithMaterialRenderer behavior, which takes the mesh, transforms into World Space - /// and renders it. - /// - /// Partial class is to allow GeometryOperation to be an inner class but have its own file. - /// - public partial class MMesh { +namespace com.google.apps.peltzer.client.model.core +{ /// - /// For generating unique ids. - /// - private static readonly System.Random rand = new System.Random(); - - // Max sizes. TODO(bug): Tune these and enforce them, temp value for now. - public static readonly int MAX_FACES = 20000; - - /// - /// Special group ID value meaning "no group". - /// - public const int GROUP_NONE = 0; - - /// - /// ID of this MMesh in the model. See ChangeId() for notes on setting this. - /// - private int _id; - - /// - /// Offset (position) of this MMesh in model space (for transforming coordinates from Mesh Space to Model Space). - /// - public Vector3 _offset; - - /// - /// Rotation of this MMesh in model space (for transforming coordinates from Mesh Space to Model Space). - /// - public Quaternion _rotation = Quaternion.identity; - - private Dictionary verticesById; - private Dictionary facesById; - - public int vertexCount { get { return verticesById.Count; }} - public int faceCount { get { return facesById.Count; }} - - /// - /// Reverse table which provides vertex id to face id lookup - each vertex maps to the set of faces in which it is - /// used. - /// Until all mesh modification is fully converted to use GeometryOperation - http://bug the only safe way to - /// use this data is by calling RecalcReverseTable - /// - public Dictionary> reverseTable; - - /// - /// Bounds of this mesh in model coordinates. NOT in mesh coordinates. - /// - public Bounds bounds { get; private set; } - - /// - /// ID of the group to which this mesh belongs, or GROUP_NONE if this mesh is ungrouped. - /// Meshes with the same groupId belong to the same group and stay together during - /// selection/move/etc. - /// - public int groupId { get; set; } - - /// - /// The IDs of the original assets that originated this mesh (for remixing attribution). - /// When a set of meshes is imported from Zandria, this ID is set on each of the meshes. - /// To cut down on garbage generation, null is used instead of the empty set if there - /// is no attribution. - /// - public HashSet remixIds { get; set; } - - // Read-only getters. - public int id { get { return _id; } } - - private Vector3 _offsetJitter; - - public MMesh(int id, Vector3 offset, Quaternion rotation, - Dictionary verticesById, Dictionary facesById, - int groupId = GROUP_NONE, HashSet remixIds = null) { - _id = id; - this._offset = offset; - System.Random rand = new System.Random(); - - this._offsetJitter = new Vector3((float)(rand.NextDouble() - 0.5) / 5000f, - (float)(rand.NextDouble() - 0.5) / 5000f, - (float)(rand.NextDouble() - 0.5) / 5000f); - this._rotation = rotation; - this.verticesById = verticesById; - this.facesById = facesById; - this.groupId = groupId; - this.remixIds = remixIds; - this.reverseTable = new Dictionary>(); - RecalcBounds(); - RecalcReverseTable(); - } - - public MMesh(int id, Vector3 offset, Quaternion rotation, - Dictionary verticesById, Dictionary facesById, - Bounds bounds, Dictionary> reverseTable, int groupId = GROUP_NONE, - HashSet remixIds = null) { - _id = id; - System.Random rand = new System.Random(); - - this._offsetJitter = new Vector3((float)(rand.NextDouble() - 0.5) / 5000f, - (float)(rand.NextDouble() - 0.5) / 5000f, - (float)(rand.NextDouble() - 0.5) / 5000f); - this._offset = offset; - this._rotation = rotation; - this.verticesById = verticesById; - this.facesById = facesById; - this.bounds = bounds; - this.reverseTable = reverseTable; - this.groupId = groupId; - this.remixIds = remixIds; - } - - /// - /// Deep copy of Mesh. - /// - /// The copy. - public MMesh Clone() { - Dictionary facesCloned = new Dictionary(facesById.Count); - foreach (KeyValuePair pair in facesById) { - facesCloned.Add(pair.Key, pair.Value.Clone()); - } - - Dictionary> reverseTableCloned = new Dictionary>(reverseTable.Count); + /// An MMesh represents a mesh in the model. Named 'MMesh' to avoid ambiguity with Unity meshes. An MMesh is + /// what you normally think of as a mesh: a piece of geometry made of vertices, edges and faces. + /// + /// Although not strictly enforced everywhere, a general principle is that once an MMesh is in the model, + /// it is immutable. Mutations of an MMesh actually consist of replacing it with a new MMesh that's the result + /// of the mutation. The fact that cloning an MMesh is relatively cheap makes that model viable. + /// + /// The coordinates of the components of a mesh (vertices, normals, etc) are represented in what is called + /// Mesh Space, which is the private frame of reference of the mesh itself (not shared with other meshes + /// in the model). The offset and rotation properties of the mesh indicate how to convert from Mesh Space to + /// Model Space. For example, if a mesh has a vertex with coordinates (1, 2, 3) in Mesh Space, and the offset + /// of the mesh is (1000, 2000, 3000), then the position of that vertex in Model Space would be (1001, 2002, 3003). + /// + /// An MMesh can be attached to (and detached from) a GameObject to make it render on the screen. In particular, + /// it is rendered by the MeshWithMaterialRenderer behavior, which takes the mesh, transforms into World Space + /// and renders it. + /// + /// Partial class is to allow GeometryOperation to be an inner class but have its own file. + /// + public partial class MMesh + { + /// + /// For generating unique ids. + /// + private static readonly System.Random rand = new System.Random(); + + // Max sizes. TODO(bug): Tune these and enforce them, temp value for now. + public static readonly int MAX_FACES = 20000; + + /// + /// Special group ID value meaning "no group". + /// + public const int GROUP_NONE = 0; + + /// + /// ID of this MMesh in the model. See ChangeId() for notes on setting this. + /// + private int _id; + + /// + /// Offset (position) of this MMesh in model space (for transforming coordinates from Mesh Space to Model Space). + /// + public Vector3 _offset; + + /// + /// Rotation of this MMesh in model space (for transforming coordinates from Mesh Space to Model Space). + /// + public Quaternion _rotation = Quaternion.identity; + + private Dictionary verticesById; + private Dictionary facesById; + + public int vertexCount { get { return verticesById.Count; } } + public int faceCount { get { return facesById.Count; } } + + /// + /// Reverse table which provides vertex id to face id lookup - each vertex maps to the set of faces in which it is + /// used. + /// Until all mesh modification is fully converted to use GeometryOperation - http://bug the only safe way to + /// use this data is by calling RecalcReverseTable + /// + public Dictionary> reverseTable; + + /// + /// Bounds of this mesh in model coordinates. NOT in mesh coordinates. + /// + public Bounds bounds { get; private set; } + + /// + /// ID of the group to which this mesh belongs, or GROUP_NONE if this mesh is ungrouped. + /// Meshes with the same groupId belong to the same group and stay together during + /// selection/move/etc. + /// + public int groupId { get; set; } + + /// + /// The IDs of the original assets that originated this mesh (for remixing attribution). + /// When a set of meshes is imported from Zandria, this ID is set on each of the meshes. + /// To cut down on garbage generation, null is used instead of the empty set if there + /// is no attribution. + /// + public HashSet remixIds { get; set; } + + // Read-only getters. + public int id { get { return _id; } } + + private Vector3 _offsetJitter; + + public MMesh(int id, Vector3 offset, Quaternion rotation, + Dictionary verticesById, Dictionary facesById, + int groupId = GROUP_NONE, HashSet remixIds = null) + { + _id = id; + this._offset = offset; + System.Random rand = new System.Random(); + + this._offsetJitter = new Vector3((float)(rand.NextDouble() - 0.5) / 5000f, + (float)(rand.NextDouble() - 0.5) / 5000f, + (float)(rand.NextDouble() - 0.5) / 5000f); + this._rotation = rotation; + this.verticesById = verticesById; + this.facesById = facesById; + this.groupId = groupId; + this.remixIds = remixIds; + this.reverseTable = new Dictionary>(); + RecalcBounds(); + RecalcReverseTable(); + } - foreach (KeyValuePair> pair in reverseTable) { - reverseTableCloned[pair.Key] = new HashSet(pair.Value); - } + public MMesh(int id, Vector3 offset, Quaternion rotation, + Dictionary verticesById, Dictionary facesById, + Bounds bounds, Dictionary> reverseTable, int groupId = GROUP_NONE, + HashSet remixIds = null) + { + _id = id; + System.Random rand = new System.Random(); + + this._offsetJitter = new Vector3((float)(rand.NextDouble() - 0.5) / 5000f, + (float)(rand.NextDouble() - 0.5) / 5000f, + (float)(rand.NextDouble() - 0.5) / 5000f); + this._offset = offset; + this._rotation = rotation; + this.verticesById = verticesById; + this.facesById = facesById; + this.bounds = bounds; + this.reverseTable = reverseTable; + this.groupId = groupId; + this.remixIds = remixIds; + } - Dictionary verticesCloned = new Dictionary(verticesById); - HashSet remixIdsCloned = remixIds == null ? null : new HashSet(remixIds); - - return new MMesh(id, - _offset, - _rotation, - verticesCloned, - facesCloned, - bounds, - reverseTableCloned, - groupId, - remixIdsCloned); - } + /// + /// Deep copy of Mesh. + /// + /// The copy. + public MMesh Clone() + { + Dictionary facesCloned = new Dictionary(facesById.Count); + foreach (KeyValuePair pair in facesById) + { + facesCloned.Add(pair.Key, pair.Value.Clone()); + } + + Dictionary> reverseTableCloned = new Dictionary>(reverseTable.Count); + + foreach (KeyValuePair> pair in reverseTable) + { + reverseTableCloned[pair.Key] = new HashSet(pair.Value); + } + + Dictionary verticesCloned = new Dictionary(verticesById); + HashSet remixIdsCloned = remixIds == null ? null : new HashSet(remixIds); + + return new MMesh(id, + _offset, + _rotation, + verticesCloned, + facesCloned, + bounds, + reverseTableCloned, + groupId, + remixIdsCloned); + } - /// - /// Deep copy of Mesh while updating the id. - /// This preserves the remixId. - /// - /// The copy. - public MMesh CloneWithNewId(int newId) { - return CloneWithNewIdAndGroup(newId, groupId); - } + /// + /// Deep copy of Mesh while updating the id. + /// This preserves the remixId. + /// + /// The copy. + public MMesh CloneWithNewId(int newId) + { + return CloneWithNewIdAndGroup(newId, groupId); + } - /// - /// Deep copy of Mesh while updating the id and group ID. - /// This preserves the remixId. - /// - /// The copy. - public MMesh CloneWithNewIdAndGroup(int newId, int newGroupId) { - Dictionary facesCloned = new Dictionary(facesById.Count); - foreach (KeyValuePair pair in facesById) { - facesCloned.Add(pair.Key, pair.Value.Clone()); - } + /// + /// Deep copy of Mesh while updating the id and group ID. + /// This preserves the remixId. + /// + /// The copy. + public MMesh CloneWithNewIdAndGroup(int newId, int newGroupId) + { + Dictionary facesCloned = new Dictionary(facesById.Count); + foreach (KeyValuePair pair in facesById) + { + facesCloned.Add(pair.Key, pair.Value.Clone()); + } + + return new MMesh(newId, + _offset, + _rotation, + new Dictionary(verticesById), + facesCloned, + bounds, + new Dictionary>(reverseTable), + newGroupId, + remixIds == null ? null : new HashSet(remixIds)); + } - return new MMesh(newId, - _offset, - _rotation, - new Dictionary(verticesById), - facesCloned, - bounds, - new Dictionary>(reverseTable), - newGroupId, - remixIds == null ? null : new HashSet(remixIds)); - } + /// + /// Changes the ID of this mesh. USE WITH GREAT CARE. + /// Tools, the model, the undo/redo stacks and who knows what else might be holding a reference to the + /// previous ID, these will all become corrupt and throw exceptions if you change this without updating + /// them. + /// This should probably only be used where a mesh has not yet been added to the model, and we're just + /// updating the mesh's ID because it would collide with an existing ID. + /// + public void ChangeId(int newId) + { + _id = newId; + } - /// - /// Changes the ID of this mesh. USE WITH GREAT CARE. - /// Tools, the model, the undo/redo stacks and who knows what else might be holding a reference to the - /// previous ID, these will all become corrupt and throw exceptions if you change this without updating - /// them. - /// This should probably only be used where a mesh has not yet been added to the model, and we're just - /// updating the mesh's ID because it would collide with an existing ID. - /// - public void ChangeId(int newId) { - _id = newId; - } + /// + /// Changes the Group ID of this mesh. Much safer than ChangeId above, but still read the commentary for + /// that method and take care when using this method. + /// + public void ChangeGroupId(int newGroupId) + { + groupId = newGroupId; + } - /// - /// Changes the Group ID of this mesh. Much safer than ChangeId above, but still read the commentary for - /// that method and take care when using this method. - /// - public void ChangeGroupId(int newGroupId) { - groupId = newGroupId; - } + /// + /// Changes the remix IDs of this mesh to be the (single) remix ID given. + /// + /// The new remixId to set, or null to mean none. + public void ChangeRemixId(string remixId) + { + if (remixId == null) + { + remixIds = null; + } + else + { + if (remixIds == null) + { + remixIds = new HashSet(); + } + else + { + remixIds.Clear(); + } + remixIds.Add(remixId); + } + } - /// - /// Changes the remix IDs of this mesh to be the (single) remix ID given. - /// - /// The new remixId to set, or null to mean none. - public void ChangeRemixId(string remixId) { - if (remixId == null) { - remixIds = null; - } else { - if (remixIds == null) { - remixIds = new HashSet(); - } else { - remixIds.Clear(); - } - remixIds.Add(remixId); - } - } + private bool operationInProgress = false; - private bool operationInProgress = false; - - /// - /// Starts a GeometryOperation targeting this mesh. - /// - public GeometryOperation StartOperation() { - #if UNITY_EDITOR + /// + /// Starts a GeometryOperation targeting this mesh. + /// + public GeometryOperation StartOperation() + { +#if UNITY_EDITOR // It may be possible to OT geometry ops someday, in which case this restriction won't be necessary if (operationInProgress) { throw new Exception("Attempted to start an operation on mesh " + id + " when one is already in progress"); } - #endif - operationInProgress = true; - return new GeometryOperation(this); - } +#endif + operationInProgress = true; + return new GeometryOperation(this); + } - // Reset operationInProgress bit - private void FinishOperation() { - operationInProgress = false; - #if MMESH_PARANOID_INTEGRITY_CHECK + // Reset operationInProgress bit + private void FinishOperation() + { + operationInProgress = false; +#if MMESH_PARANOID_INTEGRITY_CHECK CheckReverseTableIntegrity(); - #endif - } - - /// - /// Get the bounds for this mesh. - /// - /// The bounds, in model coordinates. - public Bounds GetBounds() { - return bounds; - } +#endif + } - /// - /// Calculates and returns the bounds of a face in this mesh, in model space. - /// - public Bounds CalculateFaceBoundsInModelSpace(int faceId) { - Face face; - if (!facesById.TryGetValue(faceId, out face)) { - throw new Exception("Tried to get bounds for non-existent face"); - } + /// + /// Get the bounds for this mesh. + /// + /// The bounds, in model coordinates. + public Bounds GetBounds() + { + return bounds; + } - // This code is duplicated in RecalcBounds below. - float minX = float.MaxValue, minY = float.MaxValue, minZ = float.MaxValue; - float maxX = float.MinValue, maxY = float.MinValue, maxZ = float.MinValue; - foreach (int vertexId in face.vertexIds) { - Vector3 loc = MeshCoordsToModelCoords(verticesById[vertexId].loc); - minX = Mathf.Min(minX, loc.x); - minY = Mathf.Min(minY, loc.y); - minZ = Mathf.Min(minZ, loc.z); - maxX = Mathf.Max(maxX, loc.x); - maxY = Mathf.Max(maxY, loc.y); - maxZ = Mathf.Max(maxZ, loc.z); - } - return new Bounds( - /* center */ new Vector3((minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2), - /* size */ new Vector3(maxX - minX, maxY - minY, maxZ - minZ)); - } + /// + /// Calculates and returns the bounds of a face in this mesh, in model space. + /// + public Bounds CalculateFaceBoundsInModelSpace(int faceId) + { + Face face; + if (!facesById.TryGetValue(faceId, out face)) + { + throw new Exception("Tried to get bounds for non-existent face"); + } + + // This code is duplicated in RecalcBounds below. + float minX = float.MaxValue, minY = float.MaxValue, minZ = float.MaxValue; + float maxX = float.MinValue, maxY = float.MinValue, maxZ = float.MinValue; + foreach (int vertexId in face.vertexIds) + { + Vector3 loc = MeshCoordsToModelCoords(verticesById[vertexId].loc); + minX = Mathf.Min(minX, loc.x); + minY = Mathf.Min(minY, loc.y); + minZ = Mathf.Min(minZ, loc.z); + maxX = Mathf.Max(maxX, loc.x); + maxY = Mathf.Max(maxY, loc.y); + maxZ = Mathf.Max(maxZ, loc.z); + } + return new Bounds( + /* center */ new Vector3((minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2), + /* size */ new Vector3(maxX - minX, maxY - minY, maxZ - minZ)); + } - /// - /// Get the position of the given vertex id in model coordinates. - /// - public Vector3 VertexPositionInModelCoords(int vertexId) { - return ToModelCoords(verticesById[vertexId]); - } - - /// - /// Get the position of the given vertex id in mesh coordinates. - /// - public Vector3 VertexPositionInMeshCoords(int vertexId) { - return verticesById[vertexId].loc; - } + /// + /// Get the position of the given vertex id in model coordinates. + /// + public Vector3 VertexPositionInModelCoords(int vertexId) + { + return ToModelCoords(verticesById[vertexId]); + } - public Vertex GetVertex(int vertexId) { - return verticesById[vertexId]; - } + /// + /// Get the position of the given vertex id in mesh coordinates. + /// + public Vector3 VertexPositionInMeshCoords(int vertexId) + { + return verticesById[vertexId].loc; + } - /// - /// Get vertex ids to iterate over. - /// Returned as dictionary key collection for efficiency. - /// - public Dictionary.KeyCollection GetVertexIds() { - return verticesById.Keys; - } - - /// - /// Returns whether the mesh contains a vertex with the given id. - /// - public bool HasVertex(int id) { - return verticesById.ContainsKey(id); - } - - /// - /// Get vertices to iterate over. - /// Returned as dictionary key collection for efficiency. - /// - public Dictionary.ValueCollection GetVertices() { - return verticesById.Values; - } - - /// - /// Get vertex ids to iterate over. - /// Returned as dictionary key collection for efficiency. - /// - public Dictionary.KeyCollection GetFaceIds() { - return facesById.Keys; - } + public Vertex GetVertex(int vertexId) + { + return verticesById[vertexId]; + } - /// - /// Returns whether the mesh contains a face with the given id. - /// - public bool HasFace(int id) { - return facesById.ContainsKey(id); - } - - /// - /// Get faces to iterate over. - /// Returned as dictionary key collection for efficiency. - /// - public Dictionary.ValueCollection GetFaces() { - return facesById.Values; - } - - /// - /// Gets the face with the supplied id. - /// - public Face GetFace(int id) { - return facesById[id]; - } - - /// - /// Tries to get the face with the supplied id. - /// - public bool TryGetFace(int id, out Face outFace) { - return facesById.TryGetValue(id, out outFace); - } + /// + /// Get vertex ids to iterate over. + /// Returned as dictionary key collection for efficiency. + /// + public Dictionary.KeyCollection GetVertexIds() + { + return verticesById.Keys; + } - /// - /// Converts an arbitrary point in model space to this mesh's coordinate system. - /// - /// Some point in model space. - /// Same point, but in mesh space. - public Vector3 ModelCoordsToMeshCoords(Vector3 pointInModelSpace) { - return Quaternion.Inverse(_rotation) * (pointInModelSpace - offset); - } + /// + /// Returns whether the mesh contains a vertex with the given id. + /// + public bool HasVertex(int id) + { + return verticesById.ContainsKey(id); + } - /// - /// Get the position of the given vertex in model coordinates. - /// - private Vector3 ToModelCoords(Vertex vertex) { - return (_rotation * vertex.loc) + offset; - } + /// + /// Get vertices to iterate over. + /// Returned as dictionary key collection for efficiency. + /// + public Dictionary.ValueCollection GetVertices() + { + return verticesById.Values; + } - /// - /// Get the position of the given mesh-space position in model coordinates. - /// - public Vector3 MeshCoordsToModelCoords(Vector3 loc) { - return (_rotation * loc) + offset; - } + /// + /// Get vertex ids to iterate over. + /// Returned as dictionary key collection for efficiency. + /// + public Dictionary.KeyCollection GetFaceIds() + { + return facesById.Keys; + } - /// - /// Recalculate the bounds for this mesh. Should be called whenever any of the vertices move. - /// This is more efficient than Unity's default bounds calculation, as it can avoid calculating the center - /// and size until the end of the operation (whereas Unity's contract requires correct state after each - /// encapsulation). - /// - public void RecalcBounds() { - // This code is duplicated in CalculateFaceBounds above for maximum efficiency, given that this method - // is an extreme hotspot. - float minX = float.MaxValue, minY = float.MaxValue, minZ = float.MaxValue; - float maxX = float.MinValue, maxY = float.MinValue, maxZ = float.MinValue; - foreach (Vertex vert in verticesById.Values) { - Vector3 loc = MeshCoordsToModelCoords(vert.loc); - minX = Mathf.Min(minX, loc.x); - minY = Mathf.Min(minY, loc.y); - minZ = Mathf.Min(minZ, loc.z); - maxX = Mathf.Max(maxX, loc.x); - maxY = Mathf.Max(maxY, loc.y); - maxZ = Mathf.Max(maxZ, loc.z); - } - bounds = new Bounds( - /* center */ new Vector3((minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2), - /* size */ new Vector3(maxX - minX, maxY - minY, maxZ - minZ)); - } - - /// - /// Calculate the bounds for this mesh, applying an additional offset and rotation on top of the mesh's current - /// offset and rotation. - /// - public Bounds CalculateBounds(Vector3 additionalOffset, Quaternion additionalRotation) { - Vector3 totalOffset = this.offset + additionalOffset; - Quaternion totalRotation = Math3d.Normalize(this._rotation * additionalRotation); - - // This code is duplicated in CalculateFaceBounds above for maximum efficiency, given that this method - // is an extreme hotspot. - float minX = float.MaxValue, minY = float.MaxValue, minZ = float.MaxValue; - float maxX = float.MinValue, maxY = float.MinValue, maxZ = float.MinValue; - foreach (Vertex vert in verticesById.Values) { - Vector3 loc = (totalRotation * vert.loc) + totalOffset; - minX = Mathf.Min(minX, loc.x); - minY = Mathf.Min(minY, loc.y); - minZ = Mathf.Min(minZ, loc.z); - maxX = Mathf.Max(maxX, loc.x); - maxY = Mathf.Max(maxY, loc.y); - maxZ = Mathf.Max(maxZ, loc.z); - } - return new Bounds( - /* center */ new Vector3((minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2), - /* size */ new Vector3(maxX - minX, maxY - minY, maxZ - minZ)); - } + /// + /// Returns whether the mesh contains a face with the given id. + /// + public bool HasFace(int id) + { + return facesById.ContainsKey(id); + } - /// - /// Recalculates the reverse table - a table that maps vertices to faces. - /// Until all geometry modification has been converted to GeometryOperation, calling this is the only way to - /// ensure that the reverse table is accurate. After the GeometryOperation transition is done, the reverse table - /// should always be valid and this can likely be deprecated as an external method. - /// - public void RecalcReverseTable() { - reverseTable.Clear(); - foreach (Face face in facesById.Values) { - for (int i = 0; i < face.vertexIds.Count; i++) { - int curVert = face.vertexIds[i]; - HashSet faceIds; - if (!reverseTable.TryGetValue(curVert, out faceIds)) { - faceIds = new HashSet(); - reverseTable[curVert] = faceIds; - } - faceIds.Add(face.id); + /// + /// Get faces to iterate over. + /// Returned as dictionary key collection for efficiency. + /// + public Dictionary.ValueCollection GetFaces() + { + return facesById.Values; } - } - } - /// - /// Debug utility that prints the reverse table to console. - /// Don't commit code that calls this. - /// - public void PrintReverseTable() { - Debug.Log("Mesh " + id + " Reverse Table:"); - foreach (int vertId in reverseTable.Keys) { - String line = vertId + ": ["; - foreach (int faceId in reverseTable[vertId]) { - line += faceId + ", "; - } - line += "]"; - Debug.Log(line); - } - } + /// + /// Gets the face with the supplied id. + /// + public Face GetFace(int id) + { + return facesById[id]; + } - /// - /// Debug utility that prints all vertices to console. - /// Don't commit code that calls this. - /// - public void PrintVerts() { - Debug.Log("Mesh " + id + " Vertices"); - foreach (int vertId in verticesById.Keys) { - Debug.Log(" " + vertId + ": " + MeshUtil.Vector3ToString(verticesById[vertId].loc)); - } - } - - /// - /// Debug utility that prints all faces vertex ids to console. - /// Don't commit code that calls this. - /// - public void PrintFaces() { - Debug.Log("Mesh " + id + " Faces"); - foreach (int faceId in facesById.Keys) { - String faceString = " " + faceId + ": ["; - for (int i = 0; i < facesById[faceId].vertexIds.Count; i++) { - faceString += facesById[faceId].vertexIds[i] + ", "; - } - faceString += "]"; - Debug.Log(faceString); - } - } + /// + /// Tries to get the face with the supplied id. + /// + public bool TryGetFace(int id, out Face outFace) + { + return facesById.TryGetValue(id, out outFace); + } - /// - /// Generates a new ID that does not refer to any existing face. - /// - /// A new face id. - public int GenerateFaceId() { - int faceId; - do { - faceId = rand.Next(); - } while (facesById.ContainsKey(faceId)); - return faceId; - } - - /// - /// Generates a new ID that does not refer to any existing face. - /// - /// A new face id. - public int GenerateFaceId(HashSet excludedIds) { - int faceId; - do { - faceId = rand.Next(); - } while (facesById.ContainsKey(faceId) || excludedIds.Contains(faceId)); - return faceId; - } + /// + /// Converts an arbitrary point in model space to this mesh's coordinate system. + /// + /// Some point in model space. + /// Same point, but in mesh space. + public Vector3 ModelCoordsToMeshCoords(Vector3 pointInModelSpace) + { + return Quaternion.Inverse(_rotation) * (pointInModelSpace - offset); + } - /// - /// Generates a new ID that does not refer to any existing vertex. - /// - /// A new vertex id. - public int GenerateVertexId() { - int vertexId; - do { - vertexId = rand.Next(); - } while (verticesById.ContainsKey(vertexId)); - return vertexId; - } - - /// - /// Generates a new ID that does not refer to any existing vertex. - /// - /// A new vertex id. - public int GenerateVertexId(HashSet excludedIds) { - int vertexId; - do { - vertexId = rand.Next(); - } while (verticesById.ContainsKey(vertexId) || excludedIds.Contains(vertexId)); - return vertexId; - } + /// + /// Get the position of the given vertex in model coordinates. + /// + private Vector3 ToModelCoords(Vertex vertex) + { + return (_rotation * vertex.loc) + offset; + } - // Override for the below. - public static void AttachMeshToGameObject( - WorldSpace worldSpace, GameObject gameObject, MMesh mesh, - bool updateOnly = false, MaterialAndColor materialOverride = null) { - Dictionary components; - AttachMeshToGameObject(worldSpace, gameObject, mesh, out components, updateOnly, materialOverride); - } + /// + /// Get the position of the given mesh-space position in model coordinates. + /// + public Vector3 MeshCoordsToModelCoords(Vector3 loc) + { + return (_rotation * loc) + offset; + } - /// - /// Creates, or updates, the Mesh representation of an MMesh, implemented through our MeshWithMaterialRenderer. - /// The rotation and position/positionModelSpace of the incoming gameObject are irrelevant, as they will be - /// set at the end of this method. - /// - /// The final product will be a Mesh with vertex co-ordinates in 'model space' (set around a centroid of Vector3.0) - /// attached to a GameObject with its positionModelSpace set to the mesh's offset, and its rotation - /// set to the mesh's rotation. - /// - /// Thrown if the GameObject does - /// not have the required Components. - public static void AttachMeshToGameObject( - WorldSpace worldSpace, GameObject gameObject, MMesh mesh, - out Dictionary components, - bool updateOnly = false, MaterialAndColor materialOverride = null) { - // Get or add renderer to GameObject. - MeshWithMaterialRenderer renderer = gameObject.GetComponent(); - if (renderer == null) { - renderer = gameObject.AddComponent(); - renderer.Init(worldSpace); - } + /// + /// Recalculate the bounds for this mesh. Should be called whenever any of the vertices move. + /// This is more efficient than Unity's default bounds calculation, as it can avoid calculating the center + /// and size until the end of the operation (whereas Unity's contract requires correct state after each + /// encapsulation). + /// + public void RecalcBounds() + { + // This code is duplicated in CalculateFaceBounds above for maximum efficiency, given that this method + // is an extreme hotspot. + float minX = float.MaxValue, minY = float.MaxValue, minZ = float.MaxValue; + float maxX = float.MinValue, maxY = float.MinValue, maxZ = float.MinValue; + foreach (Vertex vert in verticesById.Values) + { + Vector3 loc = MeshCoordsToModelCoords(vert.loc); + minX = Mathf.Min(minX, loc.x); + minY = Mathf.Min(minY, loc.y); + minZ = Mathf.Min(minZ, loc.z); + maxX = Mathf.Max(maxX, loc.x); + maxY = Mathf.Max(maxY, loc.y); + maxZ = Mathf.Max(maxZ, loc.z); + } + bounds = new Bounds( + /* center */ new Vector3((minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2), + /* size */ new Vector3(maxX - minX, maxY - minY, maxZ - minZ)); + } - // Try to use the cached mesh and just update positions/normals. - if (updateOnly) { - components = null; - if (materialOverride != null) { - renderer.OverrideWithNewMaterial(materialOverride); + /// + /// Calculate the bounds for this mesh, applying an additional offset and rotation on top of the mesh's current + /// offset and rotation. + /// + public Bounds CalculateBounds(Vector3 additionalOffset, Quaternion additionalRotation) + { + Vector3 totalOffset = this.offset + additionalOffset; + Quaternion totalRotation = Math3d.Normalize(this._rotation * additionalRotation); + + // This code is duplicated in CalculateFaceBounds above for maximum efficiency, given that this method + // is an extreme hotspot. + float minX = float.MaxValue, minY = float.MaxValue, minZ = float.MaxValue; + float maxX = float.MinValue, maxY = float.MinValue, maxZ = float.MinValue; + foreach (Vertex vert in verticesById.Values) + { + Vector3 loc = (totalRotation * vert.loc) + totalOffset; + minX = Mathf.Min(minX, loc.x); + minY = Mathf.Min(minY, loc.y); + minZ = Mathf.Min(minZ, loc.z); + maxX = Mathf.Max(maxX, loc.x); + maxY = Mathf.Max(maxY, loc.y); + maxZ = Mathf.Max(maxZ, loc.z); + } + return new Bounds( + /* center */ new Vector3((minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2), + /* size */ new Vector3(maxX - minX, maxY - minY, maxZ - minZ)); } - MeshHelper.UpdateMeshes(mesh, renderer.meshes); - } else { - // We get vert positions in model space, not mesh space. - renderer.meshes = MeshHelper.MeshFromMMesh(mesh, /* useModelSpace */ false, out components, materialOverride); - } - // Position & rotate the gameObject to match the incoming mesh's position and rotation. - renderer.SetPositionModelSpace(mesh.offset); - renderer.SetOrientationModelSpace(mesh.rotation, /* smooth */ false); - } + /// + /// Recalculates the reverse table - a table that maps vertices to faces. + /// Until all geometry modification has been converted to GeometryOperation, calling this is the only way to + /// ensure that the reverse table is accurate. After the GeometryOperation transition is done, the reverse table + /// should always be valid and this can likely be deprecated as an external method. + /// + public void RecalcReverseTable() + { + reverseTable.Clear(); + foreach (Face face in facesById.Values) + { + for (int i = 0; i < face.vertexIds.Count; i++) + { + int curVert = face.vertexIds[i]; + HashSet faceIds; + if (!reverseTable.TryGetValue(curVert, out faceIds)) + { + faceIds = new HashSet(); + reverseTable[curVert] = faceIds; + } + faceIds.Add(face.id); + } + } + } - public static void DetachMeshFromGameObject(WorldSpace worldSpace, GameObject gameObject) { - MeshWithMaterialRenderer renderer = gameObject.GetComponent(); - if (renderer == null) { - renderer = gameObject.AddComponent(); - renderer.Init(worldSpace); - } - renderer.meshes = new List(); - } + /// + /// Debug utility that prints the reverse table to console. + /// Don't commit code that calls this. + /// + public void PrintReverseTable() + { + Debug.Log("Mesh " + id + " Reverse Table:"); + foreach (int vertId in reverseTable.Keys) + { + String line = vertId + ": ["; + foreach (int faceId in reverseTable[vertId]) + { + line += faceId + ", "; + } + line += "]"; + Debug.Log(line); + } + } - /// - /// Moves an MMesh by specific parameters. - /// - /// The Mesh. - /// The positional move delta. - /// The rotational move delta. - public static void MoveMMesh(MMesh mesh, Vector3 positionDelta, Quaternion rotDelta) { - mesh._offset += positionDelta; - mesh._rotation = Math3d.Normalize(mesh._rotation * rotDelta); - mesh.RecalcBounds(); - } + /// + /// Debug utility that prints all vertices to console. + /// Don't commit code that calls this. + /// + public void PrintVerts() + { + Debug.Log("Mesh " + id + " Vertices"); + foreach (int vertId in verticesById.Keys) + { + Debug.Log(" " + vertId + ": " + MeshUtil.Vector3ToString(verticesById[vertId].loc)); + } + } - public Vector3 offset { - get { - return _offset; - } - set { - Vector3 old = _offset; - _offset = value; - bounds = new Bounds(bounds.center + (_offset - old), bounds.size); - } - } + /// + /// Debug utility that prints all faces vertex ids to console. + /// Don't commit code that calls this. + /// + public void PrintFaces() + { + Debug.Log("Mesh " + id + " Faces"); + foreach (int faceId in facesById.Keys) + { + String faceString = " " + faceId + ": ["; + for (int i = 0; i < facesById[faceId].vertexIds.Count; i++) + { + faceString += facesById[faceId].vertexIds[i] + ", "; + } + faceString += "]"; + Debug.Log(faceString); + } + } - public Quaternion rotation { - get { - return _rotation; - } - set { - _rotation = Math3d.Normalize(value); - RecalcBounds(); - } - } + /// + /// Generates a new ID that does not refer to any existing face. + /// + /// A new face id. + public int GenerateFaceId() + { + int faceId; + do + { + faceId = rand.Next(); + } while (facesById.ContainsKey(faceId)); + return faceId; + } - public Matrix4x4 GetTransform() { - return Matrix4x4.TRS(_offset, _rotation, Vector3.one); - } + /// + /// Generates a new ID that does not refer to any existing face. + /// + /// A new face id. + public int GenerateFaceId(HashSet excludedIds) + { + int faceId; + do + { + faceId = rand.Next(); + } while (facesById.ContainsKey(faceId) || excludedIds.Contains(faceId)); + return faceId; + } - public Matrix4x4 GetJitteredTransform() { - return Matrix4x4.TRS(_offset + _offsetJitter, _rotation, Vector3.one); - } - - // Serialize - public void GetObjectData(SerializationInfo info, StreamingContext context) { - info.AddValue("meshId", _id); - info.AddValue("offset", new SerializableVector3(offset)); - info.AddValue("rotation", new SerializableQuaternion(rotation)); - info.AddValue("verticesById", verticesById); - info.AddValue("facesById", facesById); - info.AddValue("groupId", groupId); - } + /// + /// Generates a new ID that does not refer to any existing vertex. + /// + /// A new vertex id. + public int GenerateVertexId() + { + int vertexId; + do + { + vertexId = rand.Next(); + } while (verticesById.ContainsKey(vertexId)); + return vertexId; + } - // Deserialize - private SerializableVector3 serializedOffset; - private SerializableQuaternion serializedRotation; + /// + /// Generates a new ID that does not refer to any existing vertex. + /// + /// A new vertex id. + public int GenerateVertexId(HashSet excludedIds) + { + int vertexId; + do + { + vertexId = rand.Next(); + } while (verticesById.ContainsKey(vertexId) || excludedIds.Contains(vertexId)); + return vertexId; + } - /// - /// Writes to PolySerializer. - /// - public void Serialize(PolySerializer serializer) { - serializer.StartWritingChunk(SerializationConsts.CHUNK_MMESH); - serializer.WriteInt(_id); - PolySerializationUtils.WriteVector3(serializer, offset); - PolySerializationUtils.WriteQuaternion(serializer, rotation); - serializer.WriteInt(groupId); - - // Write vertices. - serializer.WriteCount(verticesById.Count); - foreach (Vertex v in verticesById.Values) { - serializer.WriteInt(v.id); - PolySerializationUtils.WriteVector3(serializer, v.loc); - } + // Override for the below. + public static void AttachMeshToGameObject( + WorldSpace worldSpace, GameObject gameObject, MMesh mesh, + bool updateOnly = false, MaterialAndColor materialOverride = null) + { + Dictionary components; + AttachMeshToGameObject(worldSpace, gameObject, mesh, out components, updateOnly, materialOverride); + } - // Write faces. - serializer.WriteCount(facesById.Count); - foreach (Face face in facesById.Values) { - serializer.WriteInt(face.id); - serializer.WriteInt(face.properties.materialId); - PolySerializationUtils.WriteIntList(serializer, face.vertexIds); - // Repeat the face normal for backwards compatability. - PolySerializationUtils.WriteVector3List(serializer, - Enumerable.Repeat(face.normal, face.vertexIds.Count).ToList()); - - // DEPRECATED: Write holes. - serializer.WriteCount(0); - } - serializer.FinishWritingChunk(SerializationConsts.CHUNK_MMESH); - - // If we have any remix IDs, also write a remix info chunk. - // As per the design of the file format, this chunk will be automatically skipped by older versions - // that don't expect remix IDs in the file. - if (remixIds != null) { - serializer.StartWritingChunk(SerializationConsts.CHUNK_MMESH_EXT_REMIX_IDS); - PolySerializationUtils.WriteStringSet(serializer, remixIds); - serializer.FinishWritingChunk(SerializationConsts.CHUNK_MMESH_EXT_REMIX_IDS); - } - } + /// + /// Creates, or updates, the Mesh representation of an MMesh, implemented through our MeshWithMaterialRenderer. + /// The rotation and position/positionModelSpace of the incoming gameObject are irrelevant, as they will be + /// set at the end of this method. + /// + /// The final product will be a Mesh with vertex co-ordinates in 'model space' (set around a centroid of Vector3.0) + /// attached to a GameObject with its positionModelSpace set to the mesh's offset, and its rotation + /// set to the mesh's rotation. + /// + /// Thrown if the GameObject does + /// not have the required Components. + public static void AttachMeshToGameObject( + WorldSpace worldSpace, GameObject gameObject, MMesh mesh, + out Dictionary components, + bool updateOnly = false, MaterialAndColor materialOverride = null) + { + // Get or add renderer to GameObject. + MeshWithMaterialRenderer renderer = gameObject.GetComponent(); + if (renderer == null) + { + renderer = gameObject.AddComponent(); + renderer.Init(worldSpace); + } + + // Try to use the cached mesh and just update positions/normals. + if (updateOnly) + { + components = null; + if (materialOverride != null) + { + renderer.OverrideWithNewMaterial(materialOverride); + } + MeshHelper.UpdateMeshes(mesh, renderer.meshes); + } + else + { + // We get vert positions in model space, not mesh space. + renderer.meshes = MeshHelper.MeshFromMMesh(mesh, /* useModelSpace */ false, out components, materialOverride); + } + + // Position & rotate the gameObject to match the incoming mesh's position and rotation. + renderer.SetPositionModelSpace(mesh.offset); + renderer.SetOrientationModelSpace(mesh.rotation, /* smooth */ false); + } - public int GetSerializedSizeEstimate() { - int estimate = 256; // Headers, offset, rotation, group ID, overhead. - estimate += 8 + verticesById.Count * 16; // count + (1 int + 3 floats) per vertex. - foreach (Face face in facesById.Values) { - estimate += 32; // ID, material ID, headers. - estimate += 8 + face.vertexIds.Count * 4; // count + 1 int per vertex ID - estimate += 8 + face.vertexIds.Count * 12; // count + 3 floats per normal - } - if (remixIds != null) { - estimate += 32; // list header overhead - foreach (string remixId in remixIds) { - estimate += 4 + remixId.Length; + public static void DetachMeshFromGameObject(WorldSpace worldSpace, GameObject gameObject) + { + MeshWithMaterialRenderer renderer = gameObject.GetComponent(); + if (renderer == null) + { + renderer = gameObject.AddComponent(); + renderer.Init(worldSpace); + } + renderer.meshes = new List(); } - } - return estimate; - } - private void CheckReverseTableIntegrity() { - foreach (int vertId in reverseTable.Keys) { - if (!HasVertex(vertId)) { - throw new Exception("Vert id " + vertId + " in reverse table does not exist in the mesh"); + /// + /// Moves an MMesh by specific parameters. + /// + /// The Mesh. + /// The positional move delta. + /// The rotational move delta. + public static void MoveMMesh(MMesh mesh, Vector3 positionDelta, Quaternion rotDelta) + { + mesh._offset += positionDelta; + mesh._rotation = Math3d.Normalize(mesh._rotation * rotDelta); + mesh.RecalcBounds(); } - foreach (int faceId in reverseTable[vertId]) { - if (!HasFace(faceId)) { - throw new Exception("Face id " + faceId + " in reverse table does not exist in the mesh"); - } + + public Vector3 offset + { + get + { + return _offset; + } + set + { + Vector3 old = _offset; + _offset = value; + bounds = new Bounds(bounds.center + (_offset - old), bounds.size); + } } - } - foreach (int vertId in verticesById.Keys) { - if (!reverseTable.ContainsKey(vertId)) { - throw new Exception("Vert id " + vertId + " in mesh is not in reverse table"); + + public Quaternion rotation + { + get + { + return _rotation; + } + set + { + _rotation = Math3d.Normalize(value); + RecalcBounds(); + } } - } - foreach (Face face in facesById.Values) { - foreach (int vertId in face.vertexIds) { - if (!reverseTable[vertId].Contains(face.id)) { - throw new Exception("Face id " + face.id + " in mesh is not in reverse table for vert " + vertId); - } + + public Matrix4x4 GetTransform() + { + return Matrix4x4.TRS(_offset, _rotation, Vector3.one); } - } - } -// Reads from PolySerializer. - public MMesh(PolySerializer serializer) { - serializer.StartReadingChunk(SerializationConsts.CHUNK_MMESH); - _id = serializer.ReadInt(); - _offset = PolySerializationUtils.ReadVector3(serializer); - _rotation = PolySerializationUtils.ReadQuaternion(serializer); - groupId = serializer.ReadInt(); - - verticesById = new Dictionary(); - facesById = new Dictionary(); - reverseTable = new Dictionary>(); - - // Read vertices. - int vertexCount = serializer.ReadCount(0, SerializationConsts.MAX_VERTICES_PER_MESH, "vertexCount"); - for (int i = 0; i < vertexCount; i++) { - int vertexId = serializer.ReadInt(); - Vector3 vertexLoc = PolySerializationUtils.ReadVector3(serializer); - verticesById[vertexId] = new Vertex(vertexId, vertexLoc); - } + public Matrix4x4 GetJitteredTransform() + { + return Matrix4x4.TRS(_offset + _offsetJitter, _rotation, Vector3.one); + } - // Read faces. - int faceCount = serializer.ReadCount(0, SerializationConsts.MAX_FACES_PER_MESH, "faceCount"); - for (int i = 0; i < faceCount; i++) { - int faceId = serializer.ReadInt(); - int materialId = serializer.ReadInt(); - List vertexIds = - PolySerializationUtils.ReadIntList(serializer, 0, SerializationConsts.MAX_VERTICES_PER_FACE, "vertexIds"); - - List normals = - PolySerializationUtils.ReadVector3List(serializer, 0, SerializationConsts.MAX_VERTICES_PER_FACE, "normals"); - - // Holes are deprecated. We read their data but don't do anything with it. - int holeCount = serializer.ReadCount(0, SerializationConsts.MAX_HOLES_PER_FACE, "holes"); - for (int j = 0; j < holeCount; j++) { - PolySerializationUtils.ReadIntList(serializer, 0, - SerializationConsts.MAX_VERTICES_PER_HOLE, "hole vertexIds"); - PolySerializationUtils.ReadVector3List(serializer, 0, - SerializationConsts.MAX_VERTICES_PER_HOLE, "hole normals"); - } - - // Once normal fixes are backfilled after http://bug we can use the deserialized normals directly. - facesById[faceId] = new Face(faceId, vertexIds.AsReadOnly(), verticesById, new FaceProperties(materialId)); - } + // Serialize + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("meshId", _id); + info.AddValue("offset", new SerializableVector3(offset)); + info.AddValue("rotation", new SerializableQuaternion(rotation)); + info.AddValue("verticesById", verticesById); + info.AddValue("facesById", facesById); + info.AddValue("groupId", groupId); + } - serializer.FinishReadingChunk(SerializationConsts.CHUNK_MMESH); - - // If the remix IDs chunk is present (it's optional), read it. - if (serializer.GetNextChunkLabel() == SerializationConsts.CHUNK_MMESH_EXT_REMIX_IDS) { - serializer.StartReadingChunk(SerializationConsts.CHUNK_MMESH_EXT_REMIX_IDS); - remixIds = PolySerializationUtils.ReadStringSet(serializer, 0, SerializationConsts.MAX_REMIX_IDS_PER_MMESH, - "remixIds"); - serializer.FinishReadingChunk(SerializationConsts.CHUNK_MMESH_EXT_REMIX_IDS); - } else { - // No remix IDs present in file. - remixIds = null; - } + // Deserialize + private SerializableVector3 serializedOffset; + private SerializableQuaternion serializedRotation; + + /// + /// Writes to PolySerializer. + /// + public void Serialize(PolySerializer serializer) + { + serializer.StartWritingChunk(SerializationConsts.CHUNK_MMESH); + serializer.WriteInt(_id); + PolySerializationUtils.WriteVector3(serializer, offset); + PolySerializationUtils.WriteQuaternion(serializer, rotation); + serializer.WriteInt(groupId); + + // Write vertices. + serializer.WriteCount(verticesById.Count); + foreach (Vertex v in verticesById.Values) + { + serializer.WriteInt(v.id); + PolySerializationUtils.WriteVector3(serializer, v.loc); + } + + // Write faces. + serializer.WriteCount(facesById.Count); + foreach (Face face in facesById.Values) + { + serializer.WriteInt(face.id); + serializer.WriteInt(face.properties.materialId); + PolySerializationUtils.WriteIntList(serializer, face.vertexIds); + // Repeat the face normal for backwards compatability. + PolySerializationUtils.WriteVector3List(serializer, + Enumerable.Repeat(face.normal, face.vertexIds.Count).ToList()); + + // DEPRECATED: Write holes. + serializer.WriteCount(0); + } + serializer.FinishWritingChunk(SerializationConsts.CHUNK_MMESH); + + // If we have any remix IDs, also write a remix info chunk. + // As per the design of the file format, this chunk will be automatically skipped by older versions + // that don't expect remix IDs in the file. + if (remixIds != null) + { + serializer.StartWritingChunk(SerializationConsts.CHUNK_MMESH_EXT_REMIX_IDS); + PolySerializationUtils.WriteStringSet(serializer, remixIds); + serializer.FinishWritingChunk(SerializationConsts.CHUNK_MMESH_EXT_REMIX_IDS); + } + } - RecalcBounds(); - RecalcReverseTable(); - /// bug - orphan vertices from Zandria models were causing reverseTable lookup failures. Fixing - /// by cleaning up orphan vertices on import. - HashSet orphanVerts = new HashSet(); - foreach (Vertex vert in verticesById.Values) { - if (!reverseTable.ContainsKey(vert.id)) { - orphanVerts.Add(vert.id); + public int GetSerializedSizeEstimate() + { + int estimate = 256; // Headers, offset, rotation, group ID, overhead. + estimate += 8 + verticesById.Count * 16; // count + (1 int + 3 floats) per vertex. + foreach (Face face in facesById.Values) + { + estimate += 32; // ID, material ID, headers. + estimate += 8 + face.vertexIds.Count * 4; // count + 1 int per vertex ID + estimate += 8 + face.vertexIds.Count * 12; // count + 3 floats per normal + } + if (remixIds != null) + { + estimate += 32; // list header overhead + foreach (string remixId in remixIds) + { + estimate += 4 + remixId.Length; + } + } + return estimate; + } + + private void CheckReverseTableIntegrity() + { + foreach (int vertId in reverseTable.Keys) + { + if (!HasVertex(vertId)) + { + throw new Exception("Vert id " + vertId + " in reverse table does not exist in the mesh"); + } + foreach (int faceId in reverseTable[vertId]) + { + if (!HasFace(faceId)) + { + throw new Exception("Face id " + faceId + " in reverse table does not exist in the mesh"); + } + } + } + foreach (int vertId in verticesById.Keys) + { + if (!reverseTable.ContainsKey(vertId)) + { + throw new Exception("Vert id " + vertId + " in mesh is not in reverse table"); + } + } + foreach (Face face in facesById.Values) + { + foreach (int vertId in face.vertexIds) + { + if (!reverseTable[vertId].Contains(face.id)) + { + throw new Exception("Face id " + face.id + " in mesh is not in reverse table for vert " + vertId); + } + } + } } - } - foreach (int vertId in orphanVerts) { - verticesById.Remove(vertId); - } - } - // Test method. - public void SetBoundsForTest(Bounds bounds) { - this.bounds = bounds; + // Reads from PolySerializer. + public MMesh(PolySerializer serializer) + { + serializer.StartReadingChunk(SerializationConsts.CHUNK_MMESH); + _id = serializer.ReadInt(); + _offset = PolySerializationUtils.ReadVector3(serializer); + _rotation = PolySerializationUtils.ReadQuaternion(serializer); + groupId = serializer.ReadInt(); + + verticesById = new Dictionary(); + facesById = new Dictionary(); + reverseTable = new Dictionary>(); + + // Read vertices. + int vertexCount = serializer.ReadCount(0, SerializationConsts.MAX_VERTICES_PER_MESH, "vertexCount"); + for (int i = 0; i < vertexCount; i++) + { + int vertexId = serializer.ReadInt(); + Vector3 vertexLoc = PolySerializationUtils.ReadVector3(serializer); + verticesById[vertexId] = new Vertex(vertexId, vertexLoc); + } + + // Read faces. + int faceCount = serializer.ReadCount(0, SerializationConsts.MAX_FACES_PER_MESH, "faceCount"); + for (int i = 0; i < faceCount; i++) + { + int faceId = serializer.ReadInt(); + int materialId = serializer.ReadInt(); + List vertexIds = + PolySerializationUtils.ReadIntList(serializer, 0, SerializationConsts.MAX_VERTICES_PER_FACE, "vertexIds"); + + List normals = + PolySerializationUtils.ReadVector3List(serializer, 0, SerializationConsts.MAX_VERTICES_PER_FACE, "normals"); + + // Holes are deprecated. We read their data but don't do anything with it. + int holeCount = serializer.ReadCount(0, SerializationConsts.MAX_HOLES_PER_FACE, "holes"); + for (int j = 0; j < holeCount; j++) + { + PolySerializationUtils.ReadIntList(serializer, 0, + SerializationConsts.MAX_VERTICES_PER_HOLE, "hole vertexIds"); + PolySerializationUtils.ReadVector3List(serializer, 0, + SerializationConsts.MAX_VERTICES_PER_HOLE, "hole normals"); + } + + // Once normal fixes are backfilled after http://bug we can use the deserialized normals directly. + facesById[faceId] = new Face(faceId, vertexIds.AsReadOnly(), verticesById, new FaceProperties(materialId)); + } + + serializer.FinishReadingChunk(SerializationConsts.CHUNK_MMESH); + + // If the remix IDs chunk is present (it's optional), read it. + if (serializer.GetNextChunkLabel() == SerializationConsts.CHUNK_MMESH_EXT_REMIX_IDS) + { + serializer.StartReadingChunk(SerializationConsts.CHUNK_MMESH_EXT_REMIX_IDS); + remixIds = PolySerializationUtils.ReadStringSet(serializer, 0, SerializationConsts.MAX_REMIX_IDS_PER_MMESH, + "remixIds"); + serializer.FinishReadingChunk(SerializationConsts.CHUNK_MMESH_EXT_REMIX_IDS); + } + else + { + // No remix IDs present in file. + remixIds = null; + } + + RecalcBounds(); + RecalcReverseTable(); + /// bug - orphan vertices from Zandria models were causing reverseTable lookup failures. Fixing + /// by cleaning up orphan vertices on import. + HashSet orphanVerts = new HashSet(); + foreach (Vertex vert in verticesById.Values) + { + if (!reverseTable.ContainsKey(vert.id)) + { + orphanVerts.Add(vert.id); + } + } + foreach (int vertId in orphanVerts) + { + verticesById.Remove(vertId); + } + } + + // Test method. + public void SetBoundsForTest(Bounds bounds) + { + this.bounds = bounds; + } } - } } diff --git a/Assets/Scripts/model/core/MeshFixer.cs b/Assets/Scripts/model/core/MeshFixer.cs index 7986f7e3..d15dcad0 100644 --- a/Assets/Scripts/model/core/MeshFixer.cs +++ b/Assets/Scripts/model/core/MeshFixer.cs @@ -18,317 +18,360 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.model.core { - - public class MeshFixer { - /// - /// Modify a mesh by moving the vertices as supplied, then tries to fix it as much as is possible. - /// - /// A copy of the original mesh. - /// The mesh to update. - /// The new vertex positions. - /// Whether this is for a final command, or just for an in-progress preview. - /// Whether mesh geometry changed. - public static bool MoveVerticesAndMutateMeshAndFix( - MMesh originalMesh, MMesh meshCopy, IEnumerable updatedVerts, bool forPreview) { - HashSet updatedVertIds = new HashSet(); - MMesh.GeometryOperation mutateOperation = meshCopy.StartOperation(); - foreach (Vertex vert in updatedVerts) { - updatedVertIds.Add(vert.id); - mutateOperation.ModifyVertex(vert); - } - mutateOperation.Commit(); - if (forPreview) { - return SplitNonCoplanarFaces(originalMesh, meshCopy, updatedVertIds); - } else { - meshCopy.RecalcBounds(); - return FixMutatedMesh(originalMesh, meshCopy, updatedVertIds, /* splitNonCoplanarFaces */ true, - /* mergeAdjacentCoplanarFaces */ false); - } - } +namespace com.google.apps.peltzer.client.model.core +{ - /// - /// Fixes a mesh which has been mutated. Tries to "fix" the mesh as much as possible - /// to reflect the user's intentions. It will join vertices that are at the same location. It - /// will remove faces and edges that no longer make sense. And it will split faces in cases - /// where the move would cause them to be non-coplanar. - /// - /// The method refines the mesh through a set of steps: - /// - /// 1) Join vertices that are in the same place. - /// 2) Remove zero-length segments (that may have been caused by joined vertices). - /// 3) Remove zero-area segments (segments that are really a line). - /// 4) Remove faces that are no longer valid (due to above). - /// 5) Split non-coplanar faces -- as much as possible, at the vertices that moved. - /// - /// - /// The mesh prior to any alterations. - /// The altered mesh. - /// The vertices that have been updated. - /// Whether to split faces that are non coplanar. - /// Whether to coalesce adjacent coplanar faces. - /// Whether mesh geometry changed. - public static bool FixMutatedMesh(MMesh originalMesh, MMesh alteredMesh, HashSet updatedVertIds, - bool splitNonCoplanarFaces, bool mergeAdjacentCoplanarFaces) { - bool mutated = false; - - mutated |= JoinDuplicateVertices(alteredMesh, updatedVertIds); - - if (splitNonCoplanarFaces && !Features.allowNoncoplanarFaces) { - // Face split needs to be first since it results in duplicate vertices when patching holes. - mutated |= SplitNonCoplanarFaces(originalMesh, alteredMesh, updatedVertIds); - } - - // The next three operations are all scoped to the set of faces that may have been changed. - // Generate this once here to avoid duplicate work. - HashSet potentiallyChangedFaces = new HashSet(); - foreach (int vertId in updatedVertIds) { - potentiallyChangedFaces.UnionWith(alteredMesh.reverseTable[vertId]); - } - - // Similarly, these methods rely on duplicate vertices being merged. - mutated |= RemoveZeroLengthSegments(alteredMesh, potentiallyChangedFaces); - mutated |= RemoveZeroAreaSegments(alteredMesh, potentiallyChangedFaces); - mutated |= RemoveInvalidFacesAndHoles(alteredMesh, potentiallyChangedFaces); - - return mutated; - } + public class MeshFixer + { + /// + /// Modify a mesh by moving the vertices as supplied, then tries to fix it as much as is possible. + /// + /// A copy of the original mesh. + /// The mesh to update. + /// The new vertex positions. + /// Whether this is for a final command, or just for an in-progress preview. + /// Whether mesh geometry changed. + public static bool MoveVerticesAndMutateMeshAndFix( + MMesh originalMesh, MMesh meshCopy, IEnumerable updatedVerts, bool forPreview) + { + HashSet updatedVertIds = new HashSet(); + MMesh.GeometryOperation mutateOperation = meshCopy.StartOperation(); + foreach (Vertex vert in updatedVerts) + { + updatedVertIds.Add(vert.id); + mutateOperation.ModifyVertex(vert); + } + mutateOperation.Commit(); + if (forPreview) + { + return SplitNonCoplanarFaces(originalMesh, meshCopy, updatedVertIds); + } + else + { + meshCopy.RecalcBounds(); + return FixMutatedMesh(originalMesh, meshCopy, updatedVertIds, /* splitNonCoplanarFaces */ true, + /* mergeAdjacentCoplanarFaces */ false); + } + } + + /// + /// Fixes a mesh which has been mutated. Tries to "fix" the mesh as much as possible + /// to reflect the user's intentions. It will join vertices that are at the same location. It + /// will remove faces and edges that no longer make sense. And it will split faces in cases + /// where the move would cause them to be non-coplanar. + /// + /// The method refines the mesh through a set of steps: + /// + /// 1) Join vertices that are in the same place. + /// 2) Remove zero-length segments (that may have been caused by joined vertices). + /// 3) Remove zero-area segments (segments that are really a line). + /// 4) Remove faces that are no longer valid (due to above). + /// 5) Split non-coplanar faces -- as much as possible, at the vertices that moved. + /// + /// + /// The mesh prior to any alterations. + /// The altered mesh. + /// The vertices that have been updated. + /// Whether to split faces that are non coplanar. + /// Whether to coalesce adjacent coplanar faces. + /// Whether mesh geometry changed. + public static bool FixMutatedMesh(MMesh originalMesh, MMesh alteredMesh, HashSet updatedVertIds, + bool splitNonCoplanarFaces, bool mergeAdjacentCoplanarFaces) + { + bool mutated = false; + + mutated |= JoinDuplicateVertices(alteredMesh, updatedVertIds); + + if (splitNonCoplanarFaces && !Features.allowNoncoplanarFaces) + { + // Face split needs to be first since it results in duplicate vertices when patching holes. + mutated |= SplitNonCoplanarFaces(originalMesh, alteredMesh, updatedVertIds); + } + + // The next three operations are all scoped to the set of faces that may have been changed. + // Generate this once here to avoid duplicate work. + HashSet potentiallyChangedFaces = new HashSet(); + foreach (int vertId in updatedVertIds) + { + potentiallyChangedFaces.UnionWith(alteredMesh.reverseTable[vertId]); + } - /// - /// Given a set of vertices that have been moved, check to see if they are - /// at the same location as another existing vertex with which they share a face. If so, join the two - /// vertices by replacing all of the ids of the second one with the first. - /// - /// This method can leave faces in an invalid state. The faces should be - /// fixed up afterwards. - /// - /// If duplicate vertices are found where BOTH are in updatedVertIds (the vertices being changed), - /// then they will not be joined, as that would break the universe (a lot of logic that uses this method - /// can't deal with us deleting vertices that are being moved). In that case, the fact that they are - /// duplicates will be reported via the 'unresolvedDuplicateVertices' out param. - /// - /// - /// - /// Whether mesh geometry changed. - public static bool JoinDuplicateVertices(MMesh mesh, HashSet updatedVertIds) { - - bool mutated = false; - - List newlyUpdatedVertexIds = new List(); - List deletedVertexIds = new List(); - - // Iterates through updatedVertIds to find duplicates. If a duplicate is found, then - // all references to vert.id are moved to updatedVertId, and then vert.id is - // removed from the list of the mesh's vertices. - // - // Merging a vert with a vertex it doesn't share a face with can potentially create bad geometry - so we only - // check the vertex against vertices it shares a face with. - foreach (int updatedVertId in updatedVertIds) { - Vector3 vertLoc = mesh.VertexPositionInMeshCoords(updatedVertId); - HashSet vertsToCheck = new HashSet(); - HashSet facesForVert = mesh.reverseTable[updatedVertId]; - - foreach (int faceId in facesForVert) { - Face face; - if (!mesh.TryGetFace(faceId, out face)) { - continue; - }; - vertsToCheck.UnionWith(face.vertexIds); + // Similarly, these methods rely on duplicate vertices being merged. + mutated |= RemoveZeroLengthSegments(alteredMesh, potentiallyChangedFaces); + mutated |= RemoveZeroAreaSegments(alteredMesh, potentiallyChangedFaces); + mutated |= RemoveInvalidFacesAndHoles(alteredMesh, potentiallyChangedFaces); + + return mutated; } - - foreach (int vertIndex in vertsToCheck) { - Vector3 vertexToCheckPositionMeshCoords = mesh.VertexPositionInMeshCoords(vertIndex); - bool areCloseEnough = (Vector3.Distance(vertLoc, vertexToCheckPositionMeshCoords) < Math3d.MERGE_DISTANCE); - if (!areCloseEnough) { - continue; - } - - // Only attempt to merge vertices that are not being moved by the user (merging vertices that are being - // actively moved would break things) - if (!updatedVertIds.Contains(vertIndex)) { - MMesh.GeometryOperation joinOperation = mesh.StartOperation(); - JoinVerts(joinOperation, vertIndex, updatedVertId); - joinOperation.DeleteVertex(vertIndex); - deletedVertexIds.Add(vertIndex); - joinOperation.Commit(); - mutated = true; - } + + /// + /// Given a set of vertices that have been moved, check to see if they are + /// at the same location as another existing vertex with which they share a face. If so, join the two + /// vertices by replacing all of the ids of the second one with the first. + /// + /// This method can leave faces in an invalid state. The faces should be + /// fixed up afterwards. + /// + /// If duplicate vertices are found where BOTH are in updatedVertIds (the vertices being changed), + /// then they will not be joined, as that would break the universe (a lot of logic that uses this method + /// can't deal with us deleting vertices that are being moved). In that case, the fact that they are + /// duplicates will be reported via the 'unresolvedDuplicateVertices' out param. + /// + /// + /// + /// Whether mesh geometry changed. + public static bool JoinDuplicateVertices(MMesh mesh, HashSet updatedVertIds) + { + + bool mutated = false; + + List newlyUpdatedVertexIds = new List(); + List deletedVertexIds = new List(); + + // Iterates through updatedVertIds to find duplicates. If a duplicate is found, then + // all references to vert.id are moved to updatedVertId, and then vert.id is + // removed from the list of the mesh's vertices. + // + // Merging a vert with a vertex it doesn't share a face with can potentially create bad geometry - so we only + // check the vertex against vertices it shares a face with. + foreach (int updatedVertId in updatedVertIds) + { + Vector3 vertLoc = mesh.VertexPositionInMeshCoords(updatedVertId); + HashSet vertsToCheck = new HashSet(); + HashSet facesForVert = mesh.reverseTable[updatedVertId]; + + foreach (int faceId in facesForVert) + { + Face face; + if (!mesh.TryGetFace(faceId, out face)) + { + continue; + }; + vertsToCheck.UnionWith(face.vertexIds); + } + + foreach (int vertIndex in vertsToCheck) + { + Vector3 vertexToCheckPositionMeshCoords = mesh.VertexPositionInMeshCoords(vertIndex); + bool areCloseEnough = (Vector3.Distance(vertLoc, vertexToCheckPositionMeshCoords) < Math3d.MERGE_DISTANCE); + if (!areCloseEnough) + { + continue; + } + + // Only attempt to merge vertices that are not being moved by the user (merging vertices that are being + // actively moved would break things) + if (!updatedVertIds.Contains(vertIndex)) + { + MMesh.GeometryOperation joinOperation = mesh.StartOperation(); + JoinVerts(joinOperation, vertIndex, updatedVertId); + joinOperation.DeleteVertex(vertIndex); + deletedVertexIds.Add(vertIndex); + joinOperation.Commit(); + mutated = true; + } + } + } + + foreach (int newlyUpdatedVertexId in newlyUpdatedVertexIds) + { + updatedVertIds.Add(newlyUpdatedVertexId); + } + + return mutated; } - } - foreach (int newlyUpdatedVertexId in newlyUpdatedVertexIds) { - updatedVertIds.Add(newlyUpdatedVertexId); - } - - return mutated; - } - - /// - /// Replace all instances of a given vertex id with another. - /// - /// The vertex id to replace - /// The vertex id to replace it with - private static void JoinVerts(MMesh.GeometryOperation operation, int id, int replaceId) { - HashSet facesWithVert = operation.GetMesh().reverseTable[id]; - foreach(int faceId in facesWithVert) { - Face face = operation.GetCurrentFace(faceId); - int idx = face.vertexIds.IndexOf(id); - - // Still not sure how this case comes up, but it's possible to generate it via actions like spamming - // the extrude tool. - if (idx != -1) { - // Replace the vertex id. Normals should be the same. - Face updatedFace = new Face( - face.id, ReplaceAt(face.vertexIds, idx, replaceId), face.normal, face.properties); - operation.ModifyFace(updatedFace); + /// + /// Replace all instances of a given vertex id with another. + /// + /// The vertex id to replace + /// The vertex id to replace it with + private static void JoinVerts(MMesh.GeometryOperation operation, int id, int replaceId) + { + HashSet facesWithVert = operation.GetMesh().reverseTable[id]; + foreach (int faceId in facesWithVert) + { + Face face = operation.GetCurrentFace(faceId); + int idx = face.vertexIds.IndexOf(id); + + // Still not sure how this case comes up, but it's possible to generate it via actions like spamming + // the extrude tool. + if (idx != -1) + { + // Replace the vertex id. Normals should be the same. + Face updatedFace = new Face( + face.id, ReplaceAt(face.vertexIds, idx, replaceId), face.normal, face.properties); + operation.ModifyFace(updatedFace); + } + + } + operation.DeleteVertex(id); } - } - operation.DeleteVertex(id); - } + private static ReadOnlyCollection ReplaceAt(ReadOnlyCollection collection, int idx, T newVal) + { + List copy = new List(collection); + copy[idx] = newVal; + return copy.AsReadOnly(); + } - private static ReadOnlyCollection ReplaceAt(ReadOnlyCollection collection, int idx, T newVal) { - List copy = new List(collection); - copy[idx] = newVal; - return copy.AsReadOnly(); - } + /// + /// For all faces in the mesh, check to see if any segments are zero-length. We assume any vertices that + /// occupy the same place have the same id. So it boils down to replacing segments where both endpoints + /// are the same vertex. + /// + /// Whether mesh geometry changed. + public static bool RemoveZeroLengthSegments(MMesh mesh, HashSet potentiallyChangedFaces) + { + MMesh.GeometryOperation segmentReplaceOperation = mesh.StartOperation(); + bool mutated = false; + foreach (int faceId in potentiallyChangedFaces) + { + Face face = segmentReplaceOperation.GetCurrentFace(faceId); + bool changed; + do + { + changed = false; + for (int i = 0; i < face.vertexIds.Count; i++) + { + if (face.vertexIds[i] == face.vertexIds[(i + 1) % face.vertexIds.Count]) + { + face = new Face( + face.id, RemoveAt(face.vertexIds, i), face.normal, face.properties); + changed = true; + break; + } + } - /// - /// For all faces in the mesh, check to see if any segments are zero-length. We assume any vertices that - /// occupy the same place have the same id. So it boils down to replacing segments where both endpoints - /// are the same vertex. - /// - /// Whether mesh geometry changed. - public static bool RemoveZeroLengthSegments(MMesh mesh, HashSet potentiallyChangedFaces) { - MMesh.GeometryOperation segmentReplaceOperation = mesh.StartOperation(); - bool mutated = false; - foreach (int faceId in potentiallyChangedFaces) { - Face face = segmentReplaceOperation.GetCurrentFace(faceId); - bool changed; - do { - changed = false; - for (int i = 0; i < face.vertexIds.Count; i++) { - if (face.vertexIds[i] == face.vertexIds[(i + 1) % face.vertexIds.Count]) { - face = new Face( - face.id, RemoveAt(face.vertexIds, i), face.normal, face.properties); - changed = true; - break; + } while (changed); + segmentReplaceOperation.ModifyFace(face); + mutated = true; } - } - - } while (changed); - segmentReplaceOperation.ModifyFace(face); - mutated = true; - } - // As long as the face was coplanar, removing a segment won't change the normal - segmentReplaceOperation.CommitWithoutRecalculation(); - return mutated; - } + // As long as the face was coplanar, removing a segment won't change the normal + segmentReplaceOperation.CommitWithoutRecalculation(); + return mutated; + } + + /// + /// Remove segments that have zero-area. In this case we look for segments that have the same vertex + /// with one vertex in between -- which is a line. In this case, collapse the three segments down + /// into one (the one that is duplicated). + /// + /// Whether mesh geometry changed. + public static bool RemoveZeroAreaSegments(MMesh mesh, HashSet potentiallyChangedFaces) + { + MMesh.GeometryOperation zeroAreaOperation = mesh.StartOperation(); + bool mutated = false; + foreach (int faceId in potentiallyChangedFaces) + { + Face face = zeroAreaOperation.GetCurrentFace(faceId); + bool changed; + do + { + changed = false; + if (face.vertexIds.Count < 3) + { + // Need at least three vertices. + continue; + } - /// - /// Remove segments that have zero-area. In this case we look for segments that have the same vertex - /// with one vertex in between -- which is a line. In this case, collapse the three segments down - /// into one (the one that is duplicated). - /// - /// Whether mesh geometry changed. - public static bool RemoveZeroAreaSegments(MMesh mesh, HashSet potentiallyChangedFaces) { - MMesh.GeometryOperation zeroAreaOperation = mesh.StartOperation(); - bool mutated = false; - foreach (int faceId in potentiallyChangedFaces) { - Face face = zeroAreaOperation.GetCurrentFace(faceId); - bool changed; - do { - changed = false; - if (face.vertexIds.Count < 3) { - // Need at least three vertices. - continue; - } - - for (int i = 0; i < face.vertexIds.Count; i++) { - if (face.vertexIds[i] == face.vertexIds[(i + 2) % face.vertexIds.Count]) { - face = new Face(face.id, RemoveTwoCyclicallyAfter(face.vertexIds, i), - face.normal, face.properties); - changed = true; - break; + for (int i = 0; i < face.vertexIds.Count; i++) + { + if (face.vertexIds[i] == face.vertexIds[(i + 2) % face.vertexIds.Count]) + { + face = new Face(face.id, RemoveTwoCyclicallyAfter(face.vertexIds, i), + face.normal, face.properties); + changed = true; + break; + } + } + } while (changed); + zeroAreaOperation.ModifyFace(face); + mutated = true; } - } - } while (changed); - zeroAreaOperation.ModifyFace(face); - mutated = true; - } - // As long as the face was already coplanar, removing a segment won't change the normal - zeroAreaOperation.CommitWithoutRecalculation(); - return mutated; - } + // As long as the face was already coplanar, removing a segment won't change the normal + zeroAreaOperation.CommitWithoutRecalculation(); + return mutated; + } - private static ReadOnlyCollection RemoveAt(ReadOnlyCollection collection, int idx) { - List copy = new List(collection); - copy.RemoveAt(idx); - return copy.AsReadOnly(); - } + private static ReadOnlyCollection RemoveAt(ReadOnlyCollection collection, int idx) + { + List copy = new List(collection); + copy.RemoveAt(idx); + return copy.AsReadOnly(); + } - private static ReadOnlyCollection RemoveTwoCyclicallyAfter(ReadOnlyCollection collection, int idx) { - List copy = new List(collection); - int idx1 = (idx + 1) % collection.Count; - int idx2 = (idx + 2) % collection.Count; + private static ReadOnlyCollection RemoveTwoCyclicallyAfter(ReadOnlyCollection collection, int idx) + { + List copy = new List(collection); + int idx1 = (idx + 1) % collection.Count; + int idx2 = (idx + 2) % collection.Count; - // If we didn't loop around between 1 and 2, decrement idx2 to account for idx1 being removed. - if (idx2 > idx1) { - idx2--; - } + // If we didn't loop around between 1 and 2, decrement idx2 to account for idx1 being removed. + if (idx2 > idx1) + { + idx2--; + } - copy.RemoveAt(idx1); - copy.RemoveAt(idx2); - return copy.AsReadOnly(); - } + copy.RemoveAt(idx1); + copy.RemoveAt(idx2); + return copy.AsReadOnly(); + } - /// - /// Remove any invalid faces. This assumes duplicate and zero-area segments have been removed. So it - /// looks for any faces with less than three vertices and removes them. - /// - /// - /// Whether mesh geometry changed. - public static bool RemoveInvalidFacesAndHoles(MMesh mesh, HashSet potentiallyChangedFaces) { - bool mutated = false; - MMesh.GeometryOperation removeInvalidOperation = mesh.StartOperation(); - foreach (int faceId in potentiallyChangedFaces) { - Face face = removeInvalidOperation.GetCurrentFace(faceId); - if (face.vertexIds.Count < 3) { - removeInvalidOperation.DeleteFace(face.id); - mutated = true; + /// + /// Remove any invalid faces. This assumes duplicate and zero-area segments have been removed. So it + /// looks for any faces with less than three vertices and removes them. + /// + /// + /// Whether mesh geometry changed. + public static bool RemoveInvalidFacesAndHoles(MMesh mesh, HashSet potentiallyChangedFaces) + { + bool mutated = false; + MMesh.GeometryOperation removeInvalidOperation = mesh.StartOperation(); + foreach (int faceId in potentiallyChangedFaces) + { + Face face = removeInvalidOperation.GetCurrentFace(faceId); + if (face.vertexIds.Count < 3) + { + removeInvalidOperation.DeleteFace(face.id); + mutated = true; + } + } + removeInvalidOperation.CommitWithoutRecalculation(); + return mutated; } - } - removeInvalidOperation.CommitWithoutRecalculation(); - return mutated; - } - /// - /// Split faces that are not coplanar. Ideally, we only want to split at places where the vertices have been - /// moved. And we should split into as few faces as possible. Right now, we split at the point of any moved - /// vertex wich means we might insert more faces than needed (but won't update parts of faces that weren't - /// moved.) - /// - /// The original mesh, needed for hole patching. - /// The new mesh, which will be mutated. - /// The set of vertices moved. - /// Whether mesh geometry changed. - public static bool SplitNonCoplanarFaces(MMesh originalMesh, MMesh newMesh, HashSet updatedVertIds) { - // Generate a map of vertex ids to list of ids of faces that contain them. - - bool mutated = false; - MMesh.GeometryOperation splitOperation = newMesh.StartOperation(); - foreach (int vertId in updatedVertIds) { - foreach (int faceId in newMesh.reverseTable[vertId]) { - Face face; - // Note: some faces may not exist in the new mesh because they were deleted as a result of merges. - if (splitOperation.TryGetCurrentFace(faceId, out face)) { - mutated |= MeshUtil.SplitFaceIfNeeded(splitOperation, face, vertId); - } + /// + /// Split faces that are not coplanar. Ideally, we only want to split at places where the vertices have been + /// moved. And we should split into as few faces as possible. Right now, we split at the point of any moved + /// vertex wich means we might insert more faces than needed (but won't update parts of faces that weren't + /// moved.) + /// + /// The original mesh, needed for hole patching. + /// The new mesh, which will be mutated. + /// The set of vertices moved. + /// Whether mesh geometry changed. + public static bool SplitNonCoplanarFaces(MMesh originalMesh, MMesh newMesh, HashSet updatedVertIds) + { + // Generate a map of vertex ids to list of ids of faces that contain them. + + bool mutated = false; + MMesh.GeometryOperation splitOperation = newMesh.StartOperation(); + foreach (int vertId in updatedVertIds) + { + foreach (int faceId in newMesh.reverseTable[vertId]) + { + Face face; + // Note: some faces may not exist in the new mesh because they were deleted as a result of merges. + if (splitOperation.TryGetCurrentFace(faceId, out face)) + { + mutated |= MeshUtil.SplitFaceIfNeeded(splitOperation, face, vertId); + } + } + } + splitOperation.Commit(); + + return mutated; } - } - splitOperation.Commit(); - - return mutated; } - } } diff --git a/Assets/Scripts/model/core/MeshMath.cs b/Assets/Scripts/model/core/MeshMath.cs index 6bde2ce0..7b36831c 100644 --- a/Assets/Scripts/model/core/MeshMath.cs +++ b/Assets/Scripts/model/core/MeshMath.cs @@ -19,1173 +19,1294 @@ using System.Linq; using UnityEngine; -namespace com.google.apps.peltzer.client.model.core { - /// - /// Structure for holding a pair of edges and the separation between the edges. - /// - public struct EdgePair { - internal float separation; - internal EdgeInfo fromEdge; - internal EdgeInfo toEdge; - } - - /// - /// Structure for holding a pair of faces and the separation between the faces. - /// - public struct FacePair { - internal float separation; - internal float angle; - internal FaceKey fromFaceKey; - internal FaceKey toFaceKey; - internal Vector3 fromFaceModelSpaceCenter; - internal Vector3 toFaceModelSpaceCenter; - } - - /// - /// Structure for holding a vertex and a face and the separation between them. - /// - public struct FaceVertexPair { - internal float separation; - internal VertexKey vertexKey; - internal FaceKey faceKey; - } - - /// - /// Math associated with meshes, faces and vertices. - /// - public class MeshMath { +namespace com.google.apps.peltzer.client.model.core +{ /// - /// Calculates a normal for a clockwise list of coplanar vertices. - /// This code is directly copied beneath to avoid an expensive Select. + /// Structure for holding a pair of edges and the separation between the edges. /// - /// A clockwise list of Vertex objects. - /// The normal for this list of vertices. - public static Vector3 CalculateNormal(List vertices) { - if (vertices.Count == 0) return Vector3.zero; - // This uses Newell's method, which is proven to generate correct normal for any polygon. - Vector3 normal = Vector3.zero; - int count = vertices.Count; - Vector3 thisPos = vertices[0].loc; - Vector3 nextPos; - for (int i = 0, next = 1; i < count; i++, next++) { - // Note: this is cheaper than computing "next % count" at each iteration. - next = (next == count) ? 0 : next; - nextPos = vertices[next].loc; - normal.x += (thisPos.y - nextPos.y) * (thisPos.z + nextPos.z); - normal.y += (thisPos.z - nextPos.z) * (thisPos.x + nextPos.x); - normal.z += (thisPos.x - nextPos.x) * (thisPos.y + nextPos.y); - thisPos = nextPos; - } - return Math3d.Normalize(normal); + public struct EdgePair + { + internal float separation; + internal EdgeInfo fromEdge; + internal EdgeInfo toEdge; } /// - /// Calculates a normal for a clockwise list of coplanar vertices. - /// This code is directly copied from above to avoid an expensive Select. + /// Structure for holding a pair of faces and the separation between the faces. /// - /// A clockwise list of Vertex objects. - /// The normal for this list of vertices. - public static Vector3 CalculateNormal(List vertices) { - if (vertices.Count == 0) return Vector3.zero; - // This uses Newell's method, which is proven to generate correct normal for any polygon. - Vector3 normal = Vector3.zero; - int count = vertices.Count; - Vector3 thisPos = vertices[0]; - Vector3 nextPos; - for (int i = 0, next = 1; i < count; i++, next++) { - // Note: this is cheaper than computing "next % count" at each iteration. - next = (next == count) ? 0 : next; - nextPos = vertices[next]; - normal.x += (thisPos.y - nextPos.y) * (thisPos.z + nextPos.z); - normal.y += (thisPos.z - nextPos.z) * (thisPos.x + nextPos.x); - normal.z += (thisPos.x - nextPos.x) * (thisPos.y + nextPos.y); - thisPos = nextPos; - } - return Math3d.Normalize(normal); + public struct FacePair + { + internal float separation; + internal float angle; + internal FaceKey fromFaceKey; + internal FaceKey toFaceKey; + internal Vector3 fromFaceModelSpaceCenter; + internal Vector3 toFaceModelSpaceCenter; } - - /// - /// Calculates a normal from a clockwise wound array of vertices, - /// - /// - /// - /// - public static Vector3 CalculateNormal(ReadOnlyCollection vertices, Dictionary verticesById) { - if (vertices.Count == 0) return Vector3.zero; - // This uses Newell's method, which is proven to generate correct normal for any polygon. - Vector3 normal = Vector3.zero; - int count = vertices.Count; - Vector3 thisPos = verticesById[vertices[0]].loc; - Vector3 nextPos; - for (int i = 0, next = 1; i < count; i++, next++) { - // Note: this is cheaper than computing "next % count" at each iteration. - next = (next == count) ? 0 : next; - nextPos = verticesById[vertices[next]].loc; - normal.x += (thisPos.y - nextPos.y) * (thisPos.z + nextPos.z); - normal.y += (thisPos.z - nextPos.z) * (thisPos.x + nextPos.x); - normal.z += (thisPos.x - nextPos.x) * (thisPos.y + nextPos.y); - thisPos = nextPos; - } - return Math3d.Normalize(normal); - } - + /// - /// Calculates a normal from a clockwise wound array of vertices in an ongoing GeometryOperation, + /// Structure for holding a vertex and a face and the separation between them. /// - /// The face to calculate the normal for. - /// The GeometryOperation to use as the source of vertex locations. - /// - public static Vector3 CalculateNormal(Face face, MMesh.GeometryOperation operation) { - if (face.vertexIds.Count == 0) return Vector3.zero; - // This uses Newell's method, which is proven to generate correct normal for any polygon. - Vector3 normal = Vector3.zero; - int count = face.vertexIds.Count; - Vector3 thisPos = operation.GetCurrentVertexPositionMeshSpace(face.vertexIds[0]); - Vector3 nextPos; - for (int i = 0, next = 1; i < count; i++, next++) { - // Note: this is cheaper than computing "next % count" at each iteration. - next = (next == count) ? 0 : next; - nextPos = operation.GetCurrentVertexPositionMeshSpace(face.vertexIds[next]); - normal.x += (thisPos.y - nextPos.y) * (thisPos.z + nextPos.z); - normal.y += (thisPos.z - nextPos.z) * (thisPos.x + nextPos.x); - normal.z += (thisPos.x - nextPos.x) * (thisPos.y + nextPos.y); - thisPos = nextPos; - } - return Math3d.Normalize(normal); + public struct FaceVertexPair + { + internal float separation; + internal VertexKey vertexKey; + internal FaceKey faceKey; } - + /// - /// Calculates a normal for a clockwise list of coplanar vertices. - /// This code is directly copied from above to avoid an expensive Select. + /// Math associated with meshes, faces and vertices. /// - /// A clockwise list of Vertex objects. - /// The normal for this list of vertices. - public static List CalculateNormals(List vertices, List> indices) { - List outList = new List(); - if (vertices.Count == 0) return outList; - - for (int faceIndex = 0; faceIndex < indices.Count; faceIndex++) { - // This uses Newell's method, which is proven to generate correct normal for any polygon. - Vector3 normal = Vector3.zero; - int count = indices[faceIndex].Count; - Vector3 thisPos = vertices[indices[faceIndex][0]]; - Vector3 nextPos; - for (int i = 0, next = 1; i < count; i++, next++) { - // Note: this is cheaper than computing "next % count" at each iteration. - next = (next == count) ? 0 : next; - nextPos = vertices[indices[faceIndex][next]]; - normal.x += (thisPos.y - nextPos.y) * (thisPos.z + nextPos.z); - normal.y += (thisPos.z - nextPos.z) * (thisPos.x + nextPos.x); - normal.z += (thisPos.x - nextPos.x) * (thisPos.y + nextPos.y); - thisPos = nextPos; + public class MeshMath + { + /// + /// Calculates a normal for a clockwise list of coplanar vertices. + /// This code is directly copied beneath to avoid an expensive Select. + /// + /// A clockwise list of Vertex objects. + /// The normal for this list of vertices. + public static Vector3 CalculateNormal(List vertices) + { + if (vertices.Count == 0) return Vector3.zero; + // This uses Newell's method, which is proven to generate correct normal for any polygon. + Vector3 normal = Vector3.zero; + int count = vertices.Count; + Vector3 thisPos = vertices[0].loc; + Vector3 nextPos; + for (int i = 0, next = 1; i < count; i++, next++) + { + // Note: this is cheaper than computing "next % count" at each iteration. + next = (next == count) ? 0 : next; + nextPos = vertices[next].loc; + normal.x += (thisPos.y - nextPos.y) * (thisPos.z + nextPos.z); + normal.y += (thisPos.z - nextPos.z) * (thisPos.x + nextPos.x); + normal.z += (thisPos.x - nextPos.x) * (thisPos.y + nextPos.y); + thisPos = nextPos; + } + return Math3d.Normalize(normal); } - outList.Add(Math3d.Normalize(normal)); - } - return outList; - } - - /// - /// Calculates a normal for a clockwise list of coplanar vertices. - /// This code is directly copied from above to avoid an expensive Select. - /// - /// A clockwise list of Vertex objects. - /// The normal for this list of vertices. - public static List CalculateNormals(List vertices, List> indices) { - List outList = new List(); - if (vertices.Count == 0) return outList; - - for (int faceIndex = 0; faceIndex < indices.Count; faceIndex++) { - // This uses Newell's method, which is proven to generate correct normal for any polygon. - Vector3 normal = Vector3.zero; - int count = indices[faceIndex].Count; - Vector3 thisPos = vertices[indices[faceIndex][0]].loc; - Vector3 nextPos; - for (int i = 0, next = 1; i < count; i++, next++) { - // Note: this is cheaper than computing "next % count" at each iteration. - next = (next == count) ? 0 : next; - nextPos = vertices[indices[faceIndex][next]].loc; - normal.x += (thisPos.y - nextPos.y) * (thisPos.z + nextPos.z); - normal.y += (thisPos.z - nextPos.z) * (thisPos.x + nextPos.x); - normal.z += (thisPos.x - nextPos.x) * (thisPos.y + nextPos.y); - thisPos = nextPos; + + /// + /// Calculates a normal for a clockwise list of coplanar vertices. + /// This code is directly copied from above to avoid an expensive Select. + /// + /// A clockwise list of Vertex objects. + /// The normal for this list of vertices. + public static Vector3 CalculateNormal(List vertices) + { + if (vertices.Count == 0) return Vector3.zero; + // This uses Newell's method, which is proven to generate correct normal for any polygon. + Vector3 normal = Vector3.zero; + int count = vertices.Count; + Vector3 thisPos = vertices[0]; + Vector3 nextPos; + for (int i = 0, next = 1; i < count; i++, next++) + { + // Note: this is cheaper than computing "next % count" at each iteration. + next = (next == count) ? 0 : next; + nextPos = vertices[next]; + normal.x += (thisPos.y - nextPos.y) * (thisPos.z + nextPos.z); + normal.y += (thisPos.z - nextPos.z) * (thisPos.x + nextPos.x); + normal.z += (thisPos.x - nextPos.x) * (thisPos.y + nextPos.y); + thisPos = nextPos; + } + return Math3d.Normalize(normal); } - outList.Add(Math3d.Normalize(normal)); - } - return outList; - } - - /// - /// Calculates a normal for a clockwise list of coplanar vertices. - /// This code is directly copied from above to avoid an expensive Select. - /// - /// A clockwise list of Vertex objects. - /// The normal for this list of vertices. - public static List CalculateNormals(Dictionary vertices, List> indices) { - List outList = new List(); - if (vertices.Count == 0) return outList; - - for (int faceIndex = 0; faceIndex < indices.Count; faceIndex++) { - // This uses Newell's method, which is proven to generate correct normal for any polygon. - Vector3 normal = Vector3.zero; - int count = indices[faceIndex].Count; - Vector3 thisPos = vertices[indices[faceIndex][0]].loc; - Vector3 nextPos; - for (int i = 0, next = 1; i < count; i++, next++) { - // Note: this is cheaper than computing "next % count" at each iteration. - next = (next == count) ? 0 : next; - nextPos = vertices[indices[faceIndex][next]].loc; - normal.x += (thisPos.y - nextPos.y) * (thisPos.z + nextPos.z); - normal.y += (thisPos.z - nextPos.z) * (thisPos.x + nextPos.x); - normal.z += (thisPos.x - nextPos.x) * (thisPos.y + nextPos.y); - thisPos = nextPos; + + /// + /// Calculates a normal from a clockwise wound array of vertices, + /// + /// + /// + /// + public static Vector3 CalculateNormal(ReadOnlyCollection vertices, Dictionary verticesById) + { + if (vertices.Count == 0) return Vector3.zero; + // This uses Newell's method, which is proven to generate correct normal for any polygon. + Vector3 normal = Vector3.zero; + int count = vertices.Count; + Vector3 thisPos = verticesById[vertices[0]].loc; + Vector3 nextPos; + for (int i = 0, next = 1; i < count; i++, next++) + { + // Note: this is cheaper than computing "next % count" at each iteration. + next = (next == count) ? 0 : next; + nextPos = verticesById[vertices[next]].loc; + normal.x += (thisPos.y - nextPos.y) * (thisPos.z + nextPos.z); + normal.y += (thisPos.z - nextPos.z) * (thisPos.x + nextPos.x); + normal.z += (thisPos.x - nextPos.x) * (thisPos.y + nextPos.y); + thisPos = nextPos; + } + return Math3d.Normalize(normal); } - outList.Add(normal.normalized); - } - return outList; - } - /// - /// Calculates the normal of a face given the face and the mesh it belongs to. - /// - /// The face whose normal is being calculated. - /// The mesh the face belongs to. - /// The normal of the face. - public static Vector3 CalculateMeshSpaceNormal(Face face, MMesh mesh) { - List vertices = new List(face.vertexIds.Count); - for (int i = 0; i < face.vertexIds.Count; i++) { - vertices.Add(mesh.VertexPositionInMeshCoords(face.vertexIds[i])); - } - return CalculateNormal(vertices); - } + /// + /// Calculates a normal from a clockwise wound array of vertices in an ongoing GeometryOperation, + /// + /// The face to calculate the normal for. + /// The GeometryOperation to use as the source of vertex locations. + /// + public static Vector3 CalculateNormal(Face face, MMesh.GeometryOperation operation) + { + if (face.vertexIds.Count == 0) return Vector3.zero; + // This uses Newell's method, which is proven to generate correct normal for any polygon. + Vector3 normal = Vector3.zero; + int count = face.vertexIds.Count; + Vector3 thisPos = operation.GetCurrentVertexPositionMeshSpace(face.vertexIds[0]); + Vector3 nextPos; + for (int i = 0, next = 1; i < count; i++, next++) + { + // Note: this is cheaper than computing "next % count" at each iteration. + next = (next == count) ? 0 : next; + nextPos = operation.GetCurrentVertexPositionMeshSpace(face.vertexIds[next]); + normal.x += (thisPos.y - nextPos.y) * (thisPos.z + nextPos.z); + normal.y += (thisPos.z - nextPos.z) * (thisPos.x + nextPos.x); + normal.z += (thisPos.x - nextPos.x) * (thisPos.y + nextPos.y); + thisPos = nextPos; + } + return Math3d.Normalize(normal); + } - /// - /// Calculates the normal of a face given the face and the mesh it belongs to in model space. - /// - /// The face whose normal is being calculated. - /// The mesh the face belongs to. - /// The normal of the face. - public static Vector3 CalculateModelSpaceNormal(Face face, MMesh mesh) { - List vertices = new List(face.vertexIds.Count); - for (int i = 0; i < face.vertexIds.Count; i++) { - vertices.Add(mesh.VertexPositionInModelCoords(face.vertexIds[i])); - } - return CalculateNormal(vertices); - } + /// + /// Calculates a normal for a clockwise list of coplanar vertices. + /// This code is directly copied from above to avoid an expensive Select. + /// + /// A clockwise list of Vertex objects. + /// The normal for this list of vertices. + public static List CalculateNormals(List vertices, List> indices) + { + List outList = new List(); + if (vertices.Count == 0) return outList; + + for (int faceIndex = 0; faceIndex < indices.Count; faceIndex++) + { + // This uses Newell's method, which is proven to generate correct normal for any polygon. + Vector3 normal = Vector3.zero; + int count = indices[faceIndex].Count; + Vector3 thisPos = vertices[indices[faceIndex][0]]; + Vector3 nextPos; + for (int i = 0, next = 1; i < count; i++, next++) + { + // Note: this is cheaper than computing "next % count" at each iteration. + next = (next == count) ? 0 : next; + nextPos = vertices[indices[faceIndex][next]]; + normal.x += (thisPos.y - nextPos.y) * (thisPos.z + nextPos.z); + normal.y += (thisPos.z - nextPos.z) * (thisPos.x + nextPos.x); + normal.z += (thisPos.x - nextPos.x) * (thisPos.y + nextPos.y); + thisPos = nextPos; + } + outList.Add(Math3d.Normalize(normal)); + } + return outList; + } - /// - /// Calculates a normal for three vertices given in clockwise order. - /// - /// First vertex. - /// Second vertex. - /// Third vertex. - /// The normal for the given vertices. - public static Vector3 CalculateNormal(Vector3 v1, Vector3 v2, Vector3 v3) { - // Note: we scale the vectors by 1000 before calculating the cross product because the vectors might be really - // tiny, so the cross product and normalization might run into floating point errors causing the result - // to be zero (bug). Pre-scaling the vectors by 1000 is mathematically equivalent, as we're normalizing - // anyway. - return Vector3.Cross((v1 - v2) * 1000f, (v1 - v3) * 1000f).normalized; - } + /// + /// Calculates a normal for a clockwise list of coplanar vertices. + /// This code is directly copied from above to avoid an expensive Select. + /// + /// A clockwise list of Vertex objects. + /// The normal for this list of vertices. + public static List CalculateNormals(List vertices, List> indices) + { + List outList = new List(); + if (vertices.Count == 0) return outList; + + for (int faceIndex = 0; faceIndex < indices.Count; faceIndex++) + { + // This uses Newell's method, which is proven to generate correct normal for any polygon. + Vector3 normal = Vector3.zero; + int count = indices[faceIndex].Count; + Vector3 thisPos = vertices[indices[faceIndex][0]].loc; + Vector3 nextPos; + for (int i = 0, next = 1; i < count; i++, next++) + { + // Note: this is cheaper than computing "next % count" at each iteration. + next = (next == count) ? 0 : next; + nextPos = vertices[indices[faceIndex][next]].loc; + normal.x += (thisPos.y - nextPos.y) * (thisPos.z + nextPos.z); + normal.y += (thisPos.z - nextPos.z) * (thisPos.x + nextPos.x); + normal.z += (thisPos.x - nextPos.x) * (thisPos.y + nextPos.y); + thisPos = nextPos; + } + outList.Add(Math3d.Normalize(normal)); + } + return outList; + } - /// - /// Checks if a point is close to a face. - /// - /// Convenience wrapper for calling function with our model objects. - /// - /// The point to test. - /// The normal to the face (this is a flat face, so one normal) - /// The coplanar vertices of the face. - /// How close we must be to the face. - /// How far we must be from the vertex. - /// True if we are close, false if we're not close. - public static bool IsCloseToFaceInterior(Vector3 point, MMesh mesh, Face face, float faceClosenessThreshold, - float vertexDistanceThreshold) { - List faceVertices = face.vertexIds.Select( - vertexId => mesh.VertexPositionInModelCoords(vertexId)).ToList(); - return IsCloseToFaceInterior(point, face.normal, faceVertices, faceClosenessThreshold, vertexDistanceThreshold); - } + /// + /// Calculates a normal for a clockwise list of coplanar vertices. + /// This code is directly copied from above to avoid an expensive Select. + /// + /// A clockwise list of Vertex objects. + /// The normal for this list of vertices. + public static List CalculateNormals(Dictionary vertices, List> indices) + { + List outList = new List(); + if (vertices.Count == 0) return outList; + + for (int faceIndex = 0; faceIndex < indices.Count; faceIndex++) + { + // This uses Newell's method, which is proven to generate correct normal for any polygon. + Vector3 normal = Vector3.zero; + int count = indices[faceIndex].Count; + Vector3 thisPos = vertices[indices[faceIndex][0]].loc; + Vector3 nextPos; + for (int i = 0, next = 1; i < count; i++, next++) + { + // Note: this is cheaper than computing "next % count" at each iteration. + next = (next == count) ? 0 : next; + nextPos = vertices[indices[faceIndex][next]].loc; + normal.x += (thisPos.y - nextPos.y) * (thisPos.z + nextPos.z); + normal.y += (thisPos.z - nextPos.z) * (thisPos.x + nextPos.x); + normal.z += (thisPos.x - nextPos.x) * (thisPos.y + nextPos.y); + thisPos = nextPos; + } + outList.Add(normal.normalized); + } + return outList; + } - /// - /// Tests whether a point p is on a coplanar convex face. - /// - /// The point to test. - /// The normal to the face (this is a flat face, so one normal) - /// The coplanar vertices of the face. - /// How close we must be to the face. - /// How far we must be from the vertex. - /// True if we are close, false if we're not close. - public static bool IsCloseToFaceInterior(Vector3 point, Vector3 faceNormal, - List faceVertices, float faceClosenessThreshold, float vertexDistanceThreshold) { - // Don't accept points that are too close to vertices as being 'close to a face'. - foreach (Vector3 vertex in faceVertices) { - if (Vector3.Distance(vertex, point) < vertexDistanceThreshold) { - return false; + /// + /// Calculates the normal of a face given the face and the mesh it belongs to. + /// + /// The face whose normal is being calculated. + /// The mesh the face belongs to. + /// The normal of the face. + public static Vector3 CalculateMeshSpaceNormal(Face face, MMesh mesh) + { + List vertices = new List(face.vertexIds.Count); + for (int i = 0; i < face.vertexIds.Count; i++) + { + vertices.Add(mesh.VertexPositionInMeshCoords(face.vertexIds[i])); + } + return CalculateNormal(vertices); } - } - - // Short-circuit where we know a point is too far from a face. - if (Vector3.Distance(Vector3.Project(point, faceNormal), Vector3.Project(faceVertices[0], faceNormal)) - > faceClosenessThreshold) { - return false; - } - - Vector3 prev = faceVertices[faceVertices.Count - 1]; - for (int i = 0; i < faceVertices.Count; i++) { - Vector3 vertex = faceVertices[i]; - Vector3 edge = vertex - prev; - Vector3 normal = Vector3.Cross(faceNormal, edge); - float min = float.MaxValue; - float max = float.MinValue; - foreach (Vector3 faceVertex in faceVertices) { - float dot = Vector3.Dot(faceVertex, normal); - if (dot < min) { - min = dot; - } - if (dot > max) { - max = dot; - } + + /// + /// Calculates the normal of a face given the face and the mesh it belongs to in model space. + /// + /// The face whose normal is being calculated. + /// The mesh the face belongs to. + /// The normal of the face. + public static Vector3 CalculateModelSpaceNormal(Face face, MMesh mesh) + { + List vertices = new List(face.vertexIds.Count); + for (int i = 0; i < face.vertexIds.Count; i++) + { + vertices.Add(mesh.VertexPositionInModelCoords(face.vertexIds[i])); + } + return CalculateNormal(vertices); } - float pointDot = Vector3.Dot(point, normal); - if (pointDot < min || pointDot > max) { - return false; + + /// + /// Calculates a normal for three vertices given in clockwise order. + /// + /// First vertex. + /// Second vertex. + /// Third vertex. + /// The normal for the given vertices. + public static Vector3 CalculateNormal(Vector3 v1, Vector3 v2, Vector3 v3) + { + // Note: we scale the vectors by 1000 before calculating the cross product because the vectors might be really + // tiny, so the cross product and normalization might run into floating point errors causing the result + // to be zero (bug). Pre-scaling the vectors by 1000 is mathematically equivalent, as we're normalizing + // anyway. + return Vector3.Cross((v1 - v2) * 1000f, (v1 - v3) * 1000f).normalized; } - prev = vertex; - } - return true; - } - public static Vector3 CalculateGeometricCenter(Face face, MMesh mesh) { - List coplanarVertices = new List(face.vertexIds.Count); - foreach (int vertexId in face.vertexIds) { - coplanarVertices.Add(mesh.VertexPositionInModelCoords(vertexId)); - } - return CalculateGeometricCenter(coplanarVertices); - } + /// + /// Checks if a point is close to a face. + /// + /// Convenience wrapper for calling function with our model objects. + /// + /// The point to test. + /// The normal to the face (this is a flat face, so one normal) + /// The coplanar vertices of the face. + /// How close we must be to the face. + /// How far we must be from the vertex. + /// True if we are close, false if we're not close. + public static bool IsCloseToFaceInterior(Vector3 point, MMesh mesh, Face face, float faceClosenessThreshold, + float vertexDistanceThreshold) + { + List faceVertices = face.vertexIds.Select( + vertexId => mesh.VertexPositionInModelCoords(vertexId)).ToList(); + return IsCloseToFaceInterior(point, face.normal, faceVertices, faceClosenessThreshold, vertexDistanceThreshold); + } - public static Vector3 CalculateGeometricCenter(List coplanarVertices) { - List cornerVertices = FindCornerVertices(coplanarVertices); - return cornerVertices.Aggregate(Vector3.zero, (sum, vec) => sum + vec) / cornerVertices.Count; - } + /// + /// Tests whether a point p is on a coplanar convex face. + /// + /// The point to test. + /// The normal to the face (this is a flat face, so one normal) + /// The coplanar vertices of the face. + /// How close we must be to the face. + /// How far we must be from the vertex. + /// True if we are close, false if we're not close. + public static bool IsCloseToFaceInterior(Vector3 point, Vector3 faceNormal, + List faceVertices, float faceClosenessThreshold, float vertexDistanceThreshold) + { + // Don't accept points that are too close to vertices as being 'close to a face'. + foreach (Vector3 vertex in faceVertices) + { + if (Vector3.Distance(vertex, point) < vertexDistanceThreshold) + { + return false; + } + } - /// - /// Takes a set of bounds and returns a bounds that encapsulates all of them. - /// - /// The bounds to be encapsulated. - /// A bounds that encapsulates all the passed bounds. - public static Bounds FindEncapsulatingBounds(IEnumerable bounds) { - // You have to start off with a bounds to encapsulate other bounds together. The first iteration of the for loop - // will encapsulate the first bounds again but it won't change the outcome of encapsulating bounds. - Bounds encapsulatingBounds = bounds.First(); - - foreach (Bounds bound in bounds) { - encapsulatingBounds.Encapsulate(bound); - } - - return encapsulatingBounds; - } + // Short-circuit where we know a point is too far from a face. + if (Vector3.Distance(Vector3.Project(point, faceNormal), Vector3.Project(faceVertices[0], faceNormal)) + > faceClosenessThreshold) + { + return false; + } - /// - /// Finds the edge bisectors in clockwise order around a face. - /// - /// Clockwise vertices representing a face. - /// Clockwise positions of edge bisector points. - public static List CalculateEdgeBisectors(IEnumerable coplanarVertices) { - List edgeBisectors = new List(); - - // Find all the edge centers by iterating through the vertices which are stored clockwise. - for (int index = 0; index < coplanarVertices.Count(); index++) { - Vector3 v1 = coplanarVertices.ElementAt(index); - Vector3 v2 = (index + 1 == coplanarVertices.Count()) ? coplanarVertices.ElementAt(0) : - coplanarVertices.ElementAt(index + 1); - - // Add the edge bisectors to verticesAndEdgeBisectors. - edgeBisectors.Add((v1 + v2) / 2.0f); - } - - return edgeBisectors; - } + Vector3 prev = faceVertices[faceVertices.Count - 1]; + for (int i = 0; i < faceVertices.Count; i++) + { + Vector3 vertex = faceVertices[i]; + Vector3 edge = vertex - prev; + Vector3 normal = Vector3.Cross(faceNormal, edge); + float min = float.MaxValue; + float max = float.MinValue; + foreach (Vector3 faceVertex in faceVertices) + { + float dot = Vector3.Dot(faceVertex, normal); + if (dot < min) + { + min = dot; + } + if (dot > max) + { + max = dot; + } + } + float pointDot = Vector3.Dot(point, normal); + if (pointDot < min || pointDot > max) + { + return false; + } + prev = vertex; + } + return true; + } - /// - /// Takes a list of coplanarVertices representing a face and removes any extraneous colinear vertices along the - /// edges returning only corner vertices. - /// - /// The list of clockwise coplanar vertices representing the face. - /// A list of clockwise coplanar vertices where no 3 vertices are colinear. - public static List FindCornerVertices(List coplanarVertices) { - // Start by populating the cornerVertices with all coplanarVertices. We will remove colinear ones as we iterate. - int numElements = coplanarVertices.Count; - - if (numElements < 3) { - // Nothing to do here, return a copy. - return new List(coplanarVertices); - } - - // Iterate through the coplanarVertices checking every Every vertex needs to be the middle vertex once. If the - // colinear check returns true we remove the middle vertex from the set of cornerVertices. - // Given that we are building a list, we need to ensure we preserve its order. - List cornerVertices = new List(numElements); - Vector3 previous = coplanarVertices[numElements - 1]; - Vector3 current = coplanarVertices[0]; - Vector3 next; - for (int i = 0; i < numElements; i++) { - next = coplanarVertices[(i + 1) % numElements]; - - if (!Math3d.AreColinear(previous, current, next)) { - cornerVertices.Add(current); + public static Vector3 CalculateGeometricCenter(Face face, MMesh mesh) + { + List coplanarVertices = new List(face.vertexIds.Count); + foreach (int vertexId in face.vertexIds) + { + coplanarVertices.Add(mesh.VertexPositionInModelCoords(vertexId)); + } + return CalculateGeometricCenter(coplanarVertices); } - previous = current; - current = next; - } + public static Vector3 CalculateGeometricCenter(List coplanarVertices) + { + List cornerVertices = FindCornerVertices(coplanarVertices); + return cornerVertices.Aggregate(Vector3.zero, (sum, vec) => sum + vec) / cornerVertices.Count; + } - return cornerVertices; - } + /// + /// Takes a set of bounds and returns a bounds that encapsulates all of them. + /// + /// The bounds to be encapsulated. + /// A bounds that encapsulates all the passed bounds. + public static Bounds FindEncapsulatingBounds(IEnumerable bounds) + { + // You have to start off with a bounds to encapsulate other bounds together. The first iteration of the for loop + // will encapsulate the first bounds again but it won't change the outcome of encapsulating bounds. + Bounds encapsulatingBounds = bounds.First(); + + foreach (Bounds bound in bounds) + { + encapsulatingBounds.Encapsulate(bound); + } - /// - /// Finds which edge in a face a position is closest to by checking how far the position is from every edge in - /// the face. - /// - /// The position we want to find the closest edge to. - /// The clockwise vertices representing the face. - /// The two vertexIds that make the closest edge. - public static KeyValuePair FindClosestEdgeInFace(Vector3 position, - IEnumerable coplanarVertices) { - float closestDistance = Mathf.Infinity; - KeyValuePair closestEdge = new KeyValuePair(); - - // Check each edge. Every vertex in vertexIds should be the first vertex on one edge to iterate through every - // edge we can iterate through the vertices which are stored in Face.vertexId in clockwise order and make this - // vertex the first and the subsequent vertex the second one for the edge. When we hit the end of the list the - // second vertex is the first one in the list. - for (int index = 0; index < coplanarVertices.Count(); index++) { - // Find the vertices. - Vector3 v1 = coplanarVertices.ElementAt(index); - Vector3 v2 = (index + 1 == coplanarVertices.Count()) ? - coplanarVertices.ElementAt(0) : coplanarVertices.ElementAt(index + 1); - - // Find the distance from the position to the edge. - float currentDistance = DistanceFromEdge(position, v1, v2); - - if (currentDistance < closestDistance) { - closestDistance = currentDistance; - closestEdge = new KeyValuePair(v1, v2); + return encapsulatingBounds; } - } - return closestEdge; - } + /// + /// Finds the edge bisectors in clockwise order around a face. + /// + /// Clockwise vertices representing a face. + /// Clockwise positions of edge bisector points. + public static List CalculateEdgeBisectors(IEnumerable coplanarVertices) + { + List edgeBisectors = new List(); + + // Find all the edge centers by iterating through the vertices which are stored clockwise. + for (int index = 0; index < coplanarVertices.Count(); index++) + { + Vector3 v1 = coplanarVertices.ElementAt(index); + Vector3 v2 = (index + 1 == coplanarVertices.Count()) ? coplanarVertices.ElementAt(0) : + coplanarVertices.ElementAt(index + 1); + + // Add the edge bisectors to verticesAndEdgeBisectors. + edgeBisectors.Add((v1 + v2) / 2.0f); + } + return edgeBisectors; + } - /// - /// Sorts all pairs of edges by increasing separation, where each edge belongs to a different face represented - /// by a list of clockwise coplanar vertices. - /// - /// The coplanar vertices of the first face. - /// The coplanar vertices of the second face. - /// - /// A sorted list of edgePair structs which contains each edge and the average distance between them. - /// - public static IEnumerable FindClosestEdgePairs(IEnumerable fromFaceVertices, - IEnumerable toFaceVertices) { - List edgePairs = new List(); - - // Check each edge in fromFaceVertices against each edge in toFaceVertices. - for (int toIndex = 0; toIndex < toFaceVertices.Count(); toIndex++) { - Vector3 toV1 = toFaceVertices.ElementAt(toIndex); - Vector3 toV2 = toFaceVertices.ElementAt((toIndex + 1) % toFaceVertices.Count()); - - for (int fromIndex = 0; fromIndex < fromFaceVertices.Count(); fromIndex++) { - Vector3 fromV1 = fromFaceVertices.ElementAt(fromIndex); - Vector3 fromV2 = fromFaceVertices.ElementAt((fromIndex + 1) % fromFaceVertices.Count()); - - float separation; - bool edgesOverlap = CompareEdges(fromV1, fromV2, toV1, toV2, out separation); - - if (edgesOverlap) { - // Create the EdgePair and add it to the list. - EdgeInfo fromEdge = new EdgeInfo(); - fromEdge.edgeStart = fromV1; - fromEdge.edgeVector = fromV2 - fromV1; - - EdgeInfo toEdge = new EdgeInfo(); - toEdge.edgeStart = toV1; - toEdge.edgeVector = toV2 - toV1; - - EdgePair edgePair = new EdgePair(); - edgePair.separation = separation; - edgePair.fromEdge = fromEdge; - edgePair.toEdge = toEdge; - - edgePairs.Add(edgePair); - } + /// + /// Takes a list of coplanarVertices representing a face and removes any extraneous colinear vertices along the + /// edges returning only corner vertices. + /// + /// The list of clockwise coplanar vertices representing the face. + /// A list of clockwise coplanar vertices where no 3 vertices are colinear. + public static List FindCornerVertices(List coplanarVertices) + { + // Start by populating the cornerVertices with all coplanarVertices. We will remove colinear ones as we iterate. + int numElements = coplanarVertices.Count; + + if (numElements < 3) + { + // Nothing to do here, return a copy. + return new List(coplanarVertices); + } + + // Iterate through the coplanarVertices checking every Every vertex needs to be the middle vertex once. If the + // colinear check returns true we remove the middle vertex from the set of cornerVertices. + // Given that we are building a list, we need to ensure we preserve its order. + List cornerVertices = new List(numElements); + Vector3 previous = coplanarVertices[numElements - 1]; + Vector3 current = coplanarVertices[0]; + Vector3 next; + for (int i = 0; i < numElements; i++) + { + next = coplanarVertices[(i + 1) % numElements]; + + if (!Math3d.AreColinear(previous, current, next)) + { + cornerVertices.Add(current); + } + + previous = current; + current = next; + } + + return cornerVertices; } - } - // Return edgePairs sorted in ascending order of separation. - return edgePairs.OrderBy(pair => pair.separation); - } + /// + /// Finds which edge in a face a position is closest to by checking how far the position is from every edge in + /// the face. + /// + /// The position we want to find the closest edge to. + /// The clockwise vertices representing the face. + /// The two vertexIds that make the closest edge. + public static KeyValuePair FindClosestEdgeInFace(Vector3 position, + IEnumerable coplanarVertices) + { + float closestDistance = Mathf.Infinity; + KeyValuePair closestEdge = new KeyValuePair(); + + // Check each edge. Every vertex in vertexIds should be the first vertex on one edge to iterate through every + // edge we can iterate through the vertices which are stored in Face.vertexId in clockwise order and make this + // vertex the first and the subsequent vertex the second one for the edge. When we hit the end of the list the + // second vertex is the first one in the list. + for (int index = 0; index < coplanarVertices.Count(); index++) + { + // Find the vertices. + Vector3 v1 = coplanarVertices.ElementAt(index); + Vector3 v2 = (index + 1 == coplanarVertices.Count()) ? + coplanarVertices.ElementAt(0) : coplanarVertices.ElementAt(index + 1); + + // Find the distance from the position to the edge. + float currentDistance = DistanceFromEdge(position, v1, v2); + + if (currentDistance < closestDistance) + { + closestDistance = currentDistance; + closestEdge = new KeyValuePair(v1, v2); + } + } - /// - /// Finds the pair of edges with the smallest separation, where each edge belongs to a different face - /// represented by a list of clockwise coplanar vertices. - /// - /// The coplanar vertices of the first face. - /// The coplanar vertices of the second face. - /// The closest edge pair. - /// - /// Whether a closest edge pair was found. - /// - public static bool MaybeFindClosestEdgePair(IEnumerable fromFaceVertices, - IEnumerable toFaceVertices, out EdgePair closestEdgePair) { - closestEdgePair = new EdgePair(); - float closestSeparation = Mathf.Infinity; - - // Check each edge in fromFaceVertices against each edge in toFaceVertices. - for (int toIndex = 0; toIndex < toFaceVertices.Count(); toIndex++) { - Vector3 toV1 = toFaceVertices.ElementAt(toIndex); - Vector3 toV2 = toFaceVertices.ElementAt((toIndex + 1) % toFaceVertices.Count()); - - for (int fromIndex = 0; fromIndex < fromFaceVertices.Count(); fromIndex++) { - Vector3 fromV1 = fromFaceVertices.ElementAt(fromIndex); - Vector3 fromV2 = fromFaceVertices.ElementAt((fromIndex + 1) % fromFaceVertices.Count()); - - float separation; - bool edgesOverlap = CompareEdges(fromV1, fromV2, toV1, toV2, out separation); - - if (edgesOverlap && separation < closestSeparation) { - closestSeparation = separation; - // Create the EdgePair and add it to the list. - EdgeInfo fromEdge = new EdgeInfo(); - fromEdge.edgeStart = fromV1; - fromEdge.edgeVector = fromV2 - fromV1; - - EdgeInfo toEdge = new EdgeInfo(); - toEdge.edgeStart = toV1; - toEdge.edgeVector = toV2 - toV1; - - closestEdgePair.separation = separation; - closestEdgePair.fromEdge = fromEdge; - closestEdgePair.toEdge = toEdge;; - } + return closestEdge; } - } - // Return edgePairs sorted in ascending order of separation. - return closestSeparation != Mathf.Infinity; - } - /// - /// Compares two edges and determines how far apart they are. The separation between edges is calculated by - /// finding the average distance from each vertex in the first edge at a right angle to the second edge. We only - /// compare edges if they overlap or if at least one vertex of either edge is inside the other edge. - /// - /// See bug for a diagram. - /// - /// First vertex of an edge a. - /// Second vertex of an edge a. - /// First vertex of an edge b. - /// Second vertex of an edge b. - /// How far apart the edges are. - /// Whether the edges overlapped. - public static bool CompareEdges(Vector3 a1, Vector3 a2, Vector3 b1, Vector3 b2, out float separation) { - float distanceA2 = DistanceFromEdge(a2, b1, b2); - bool a2InsideB = InsideEdge(a2, b1, b2); - - float distanceA1 = DistanceFromEdge(a1, b1, b2); - bool a1InsideB = InsideEdge(a1, b1, b2); - - // Find the separation which is the sum of the projections of a1 and a2 at 90 degree angles onto edge b. - separation = (distanceA1 + distanceA2) / 2.0f; - - // Check if either a1 or a2 was inside edge b. If they were we already know the edges are comparable. - if (a1InsideB || a2InsideB) { - return true; - } - - // We already know the separation but we still don't know if the edges are actually comparable because a1 and a2 - // were not inside edge b. But we can still compare the edges and use the separation we already calculated if - // b1 or b2 are inside edge a. - float distanceB1 = DistanceFromEdge(b1, a1, a2); - bool b1InsideEdge = InsideEdge(b1, a1, a2); - float distanceB2 = DistanceFromEdge(b2, a1, a2); - bool b2InsideEdge = InsideEdge(b2, a1, a2); - - return b1InsideEdge || b2InsideEdge; - } + /// + /// Sorts all pairs of edges by increasing separation, where each edge belongs to a different face represented + /// by a list of clockwise coplanar vertices. + /// + /// The coplanar vertices of the first face. + /// The coplanar vertices of the second face. + /// + /// A sorted list of edgePair structs which contains each edge and the average distance between them. + /// + public static IEnumerable FindClosestEdgePairs(IEnumerable fromFaceVertices, + IEnumerable toFaceVertices) + { + List edgePairs = new List(); + + // Check each edge in fromFaceVertices against each edge in toFaceVertices. + for (int toIndex = 0; toIndex < toFaceVertices.Count(); toIndex++) + { + Vector3 toV1 = toFaceVertices.ElementAt(toIndex); + Vector3 toV2 = toFaceVertices.ElementAt((toIndex + 1) % toFaceVertices.Count()); + + for (int fromIndex = 0; fromIndex < fromFaceVertices.Count(); fromIndex++) + { + Vector3 fromV1 = fromFaceVertices.ElementAt(fromIndex); + Vector3 fromV2 = fromFaceVertices.ElementAt((fromIndex + 1) % fromFaceVertices.Count()); + + float separation; + bool edgesOverlap = CompareEdges(fromV1, fromV2, toV1, toV2, out separation); + + if (edgesOverlap) + { + // Create the EdgePair and add it to the list. + EdgeInfo fromEdge = new EdgeInfo(); + fromEdge.edgeStart = fromV1; + fromEdge.edgeVector = fromV2 - fromV1; + + EdgeInfo toEdge = new EdgeInfo(); + toEdge.edgeStart = toV1; + toEdge.edgeVector = toV2 - toV1; + + EdgePair edgePair = new EdgePair(); + edgePair.separation = separation; + edgePair.fromEdge = fromEdge; + edgePair.toEdge = toEdge; + + edgePairs.Add(edgePair); + } + } + } - /// - /// Finds the perpendicular distance from a position/vertex a to an edge b represented by two vertices b1 and b2. - /// Determines whether a is inside edge b. A vertex is inside an edge if the triangle formed by all three vertices - /// doesn't have obtuse angles at the corners defined by the edge's vertices. - /// - /// The position we are trying to find the distance to the edge for. - /// First vertex for the edge. - /// Second vertex for the edge. - /// Perpendicular distance from the position to the edge. - /// Whether a is inside edge b. - public static float DistanceFromEdge(Vector3 a, Vector3 b1, Vector3 b2) { - // Find the angles of the corners of b2 in triangle ab2b1. - float thetaAB2B1 = Vector3.Angle(a - b2, b1 - b2) * Mathf.Deg2Rad; - - // Calculate the distance between a and edge b such that the projection of a onto edge b forms a right angle - // with edge b. - return Vector3.Distance(a, b2) * Mathf.Sin(Mathf.Min(Mathf.PI - thetaAB2B1, thetaAB2B1));; - } + // Return edgePairs sorted in ascending order of separation. + return edgePairs.OrderBy(pair => pair.separation); + } - /// - /// Checks if a given vertex a is inside an edge b. A vertex is inside an edge if the triangle formed by all three - /// vertices doesn't have obtuse angles at the corners defined by the edge's vertices. - /// - /// The position we are trying to check is inside an edge. - /// First vertex for the edge. - /// Second vertex for the edge. - /// Whether a is inside edge b. - public static bool InsideEdge(Vector3 a, Vector3 b1, Vector3 b2) { - // Find the angles of the corners of b1 and b2 in triangle ab2b1. - float thetaAB1B2 = Vector3.Angle(a - b1, b2 - b1) * Mathf.Deg2Rad; - float thetaAB2B1 = Vector3.Angle(a - b2, b1 - b2) * Mathf.Deg2Rad; - - // Check that b1 and b2 aren't obtuse angles in triangle a2b2b1. - return thetaAB1B2 < (Mathf.PI / 2.0f) && thetaAB2B1 < (Mathf.PI / 2.0f); - } + /// + /// Finds the pair of edges with the smallest separation, where each edge belongs to a different face + /// represented by a list of clockwise coplanar vertices. + /// + /// The coplanar vertices of the first face. + /// The coplanar vertices of the second face. + /// The closest edge pair. + /// + /// Whether a closest edge pair was found. + /// + public static bool MaybeFindClosestEdgePair(IEnumerable fromFaceVertices, + IEnumerable toFaceVertices, out EdgePair closestEdgePair) + { + closestEdgePair = new EdgePair(); + float closestSeparation = Mathf.Infinity; + + // Check each edge in fromFaceVertices against each edge in toFaceVertices. + for (int toIndex = 0; toIndex < toFaceVertices.Count(); toIndex++) + { + Vector3 toV1 = toFaceVertices.ElementAt(toIndex); + Vector3 toV2 = toFaceVertices.ElementAt((toIndex + 1) % toFaceVertices.Count()); + + for (int fromIndex = 0; fromIndex < fromFaceVertices.Count(); fromIndex++) + { + Vector3 fromV1 = fromFaceVertices.ElementAt(fromIndex); + Vector3 fromV2 = fromFaceVertices.ElementAt((fromIndex + 1) % fromFaceVertices.Count()); + + float separation; + bool edgesOverlap = CompareEdges(fromV1, fromV2, toV1, toV2, out separation); + + if (edgesOverlap && separation < closestSeparation) + { + closestSeparation = separation; + // Create the EdgePair and add it to the list. + EdgeInfo fromEdge = new EdgeInfo(); + fromEdge.edgeStart = fromV1; + fromEdge.edgeVector = fromV2 - fromV1; + + EdgeInfo toEdge = new EdgeInfo(); + toEdge.edgeStart = toV1; + toEdge.edgeVector = toV2 - toV1; + + closestEdgePair.separation = separation; + closestEdgePair.fromEdge = fromEdge; + closestEdgePair.toEdge = toEdge; ; + } + } + } - /// - /// Finds which edge from a set of coplanarVertices is closest to a given edge. - /// - /// A set of vertices representing edges in clockwise order. - /// The edge being compared to. - /// A vector representation of the closestEdge. - public static Vector3 ClosestEdgeToEdge(IEnumerable coplanarVertices, EdgeInfo toEdge) { - // We just want the edge endpoints. - Vector3 eV1 = toEdge.edgeStart; - Vector3 eV2 = eV1 + toEdge.edgeVector; - - float closestDistance = Mathf.Infinity; - Vector3 closestEdge = Vector3.zero; - - // Check each edge. To iterate through the edges we can iterate through the clockwise vertices in coplanar - // vertices allowing each vertex to be the first vertex in an edge. - for (int index = 0; index < coplanarVertices.Count(); index++) { - // Find the vertices. - Vector3 v1 = coplanarVertices.ElementAt(index); - Vector3 v2 = coplanarVertices.ElementAt((index + 1) % coplanarVertices.Count()); - - Vector3 edge = v2 - v1; - - float d1 = DistanceFromEdge(v1, eV1, eV2); - float d2 = DistanceFromEdge(v2, eV1, eV2); - - float currentDistance = (d1 + d2) / 2.0f; - - if (currentDistance < closestDistance) { - closestDistance = currentDistance; - closestEdge = edge; + // Return edgePairs sorted in ascending order of separation. + return closestSeparation != Mathf.Infinity; } - } - return closestEdge; - } + /// + /// Compares two edges and determines how far apart they are. The separation between edges is calculated by + /// finding the average distance from each vertex in the first edge at a right angle to the second edge. We only + /// compare edges if they overlap or if at least one vertex of either edge is inside the other edge. + /// + /// See bug for a diagram. + /// + /// First vertex of an edge a. + /// Second vertex of an edge a. + /// First vertex of an edge b. + /// Second vertex of an edge b. + /// How far apart the edges are. + /// Whether the edges overlapped. + public static bool CompareEdges(Vector3 a1, Vector3 a2, Vector3 b1, Vector3 b2, out float separation) + { + float distanceA2 = DistanceFromEdge(a2, b1, b2); + bool a2InsideB = InsideEdge(a2, b1, b2); + + float distanceA1 = DistanceFromEdge(a1, b1, b2); + bool a1InsideB = InsideEdge(a1, b1, b2); + + // Find the separation which is the sum of the projections of a1 and a2 at 90 degree angles onto edge b. + separation = (distanceA1 + distanceA2) / 2.0f; + + // Check if either a1 or a2 was inside edge b. If they were we already know the edges are comparable. + if (a1InsideB || a2InsideB) + { + return true; + } - /// - /// Checks to see if N vertices from a set of vertices are on a given mesh. - /// - /// The id for the mesh. - /// The set of vertices. - /// The minimum number of vertices on the same mesh required to return true. - /// True if N vertices from the set are on the mesh. - public static bool MultipleNearbyVerticesOnSameMesh(int meshId, IEnumerable vertexKeys, - int minSetSize) { - ushort vertexCountOnSameMesh = 0; - - foreach (VertexKey vertexKey in vertexKeys) { - if (meshId == vertexKey.meshId) { - vertexCountOnSameMesh++; - if (vertexCountOnSameMesh >= minSetSize) - return true; + // We already know the separation but we still don't know if the edges are actually comparable because a1 and a2 + // were not inside edge b. But we can still compare the edges and use the separation we already calculated if + // b1 or b2 are inside edge a. + float distanceB1 = DistanceFromEdge(b1, a1, a2); + bool b1InsideEdge = InsideEdge(b1, a1, a2); + float distanceB2 = DistanceFromEdge(b2, a1, a2); + bool b2InsideEdge = InsideEdge(b2, a1, a2); + + return b1InsideEdge || b2InsideEdge; } - } - return false; - } + /// + /// Finds the perpendicular distance from a position/vertex a to an edge b represented by two vertices b1 and b2. + /// Determines whether a is inside edge b. A vertex is inside an edge if the triangle formed by all three vertices + /// doesn't have obtuse angles at the corners defined by the edge's vertices. + /// + /// The position we are trying to find the distance to the edge for. + /// First vertex for the edge. + /// Second vertex for the edge. + /// Perpendicular distance from the position to the edge. + /// Whether a is inside edge b. + public static float DistanceFromEdge(Vector3 a, Vector3 b1, Vector3 b2) + { + // Find the angles of the corners of b2 in triangle ab2b1. + float thetaAB2B1 = Vector3.Angle(a - b2, b1 - b2) * Mathf.Deg2Rad; + + // Calculate the distance between a and edge b such that the projection of a onto edge b forms a right angle + // with edge b. + return Vector3.Distance(a, b2) * Mathf.Sin(Mathf.Min(Mathf.PI - thetaAB2B1, thetaAB2B1)); ; + } - /// - /// Checks to see if N faces from a set of faces are on the same mesh. - /// - /// The faces, as a List for efficiency. - /// The minimum number of faces on the same mesh required to return true. - /// The mesh with the most nearby faces on it. - /// True if N faces from the set are on a mesh. - public static bool TryFindingNearestMeshGivenNearbyFaces(List> faces, int minSetSize, - out int nearestMeshId) { - Dictionary faceCountByMeshId = new Dictionary(); - int currentMaxCount = 0; - nearestMeshId = -1; - - foreach (DistancePair faceKeyPair in faces) { - int meshId = faceKeyPair.value.meshId; - - // Set the current face count for this mesh to 1, or increment it. - int currentFaceCount = 0; - faceCountByMeshId.TryGetValue(meshId, out currentFaceCount); - currentFaceCount++; - faceCountByMeshId[meshId] = currentFaceCount; - - // Update the current max count if the current mesh has more references. - if (currentFaceCount > currentMaxCount) { - currentMaxCount = currentFaceCount; - nearestMeshId = meshId; + /// + /// Checks if a given vertex a is inside an edge b. A vertex is inside an edge if the triangle formed by all three + /// vertices doesn't have obtuse angles at the corners defined by the edge's vertices. + /// + /// The position we are trying to check is inside an edge. + /// First vertex for the edge. + /// Second vertex for the edge. + /// Whether a is inside edge b. + public static bool InsideEdge(Vector3 a, Vector3 b1, Vector3 b2) + { + // Find the angles of the corners of b1 and b2 in triangle ab2b1. + float thetaAB1B2 = Vector3.Angle(a - b1, b2 - b1) * Mathf.Deg2Rad; + float thetaAB2B1 = Vector3.Angle(a - b2, b1 - b2) * Mathf.Deg2Rad; + + // Check that b1 and b2 aren't obtuse angles in triangle a2b2b1. + return thetaAB1B2 < (Mathf.PI / 2.0f) && thetaAB2B1 < (Mathf.PI / 2.0f); } - } - return currentMaxCount >= minSetSize; - } + /// + /// Finds which edge from a set of coplanarVertices is closest to a given edge. + /// + /// A set of vertices representing edges in clockwise order. + /// The edge being compared to. + /// A vector representation of the closestEdge. + public static Vector3 ClosestEdgeToEdge(IEnumerable coplanarVertices, EdgeInfo toEdge) + { + // We just want the edge endpoints. + Vector3 eV1 = toEdge.edgeStart; + Vector3 eV2 = eV1 + toEdge.edgeVector; + + float closestDistance = Mathf.Infinity; + Vector3 closestEdge = Vector3.zero; + + // Check each edge. To iterate through the edges we can iterate through the clockwise vertices in coplanar + // vertices allowing each vertex to be the first vertex in an edge. + for (int index = 0; index < coplanarVertices.Count(); index++) + { + // Find the vertices. + Vector3 v1 = coplanarVertices.ElementAt(index); + Vector3 v2 = coplanarVertices.ElementAt((index + 1) % coplanarVertices.Count()); + + Vector3 edge = v2 - v1; + + float d1 = DistanceFromEdge(v1, eV1, eV2); + float d2 = DistanceFromEdge(v2, eV1, eV2); + + float currentDistance = (d1 + d2) / 2.0f; + + if (currentDistance < closestDistance) + { + closestDistance = currentDistance; + closestEdge = edge; + } + } - /// - /// Finds the closest face in a mesh to a list of faces. Done by comparing the separation between the faces, - /// defined as the average distance of each vertex on the mesh face to the plane created by the other face. It - /// also uses the angle between the faces normals as a measure for "flushness". - /// - /// - /// The faces near enough to the mesh for comparison, as a List for efficiency. - /// - /// The unrotated, unoffset mesh being compared to. - /// The offset of the mesh. - /// The rotation of the mesh. - /// The model the faces belong to. - /// The degree at which two faces are too "unflush" to be close. - /// The closest pair of faces. - /// - /// Whether there were any comparable faces. Faces are only comparable if Plane.Raycast() returns true. This - /// happens when the face normals have an angle > 90f. Or when they face each other. - /// - public static bool FindClosestFace(List> nearbyFaces, MMesh passedMesh, Vector3 meshOffset, - Quaternion meshRotation, Model model, float angleThreshold, out FacePair closestFace) { - List closestFaces = new List(); - Dictionary nearbyFacesInfo = new Dictionary(); - - foreach (DistancePair nearbyFaceKeyPair in nearbyFaces) { - FaceKey nearbyFaceKey = nearbyFaceKeyPair.value; - MMesh nearbyMesh = model.GetMesh(nearbyFaceKey.meshId); - Face nearbyFace = nearbyMesh.GetFace(nearbyFaceKey.faceId); - FaceInfo nearbyFaceInfo = new FaceInfo(); - List nearbyFaceVertices = new List(); - - for (int i = 0; i < nearbyFace.vertexIds.Count(); i++) { - nearbyFaceVertices.Add(nearbyMesh.VertexPositionInModelCoords(nearbyFace.vertexIds[i])); + return closestEdge; } - nearbyFaceInfo.baryCenter = MeshMath.CalculateGeometricCenter(nearbyFaceVertices); - nearbyFaceInfo.plane = new Plane( - CalculateModelSpaceNormal(nearbyFace, nearbyMesh), - nearbyMesh.VertexPositionInModelCoords(nearbyFace.vertexIds.First())); - - nearbyFacesInfo[nearbyFaceKey] = nearbyFaceInfo; - } - - // Compare each pair of faces. - foreach (Face meshFace in passedMesh.GetFaces()) { - List verticesInModelSpace = new List(meshFace.vertexIds.Count); - for (int i = 0; i < meshFace.vertexIds.Count; i++) { - Vector3 positionMeshSpace = passedMesh.VertexPositionInMeshCoords(meshFace.vertexIds[i]); - Vector3 positionModelSpace = (meshRotation * positionMeshSpace) + meshOffset; - verticesInModelSpace.Add(positionModelSpace); + /// + /// Checks to see if N vertices from a set of vertices are on a given mesh. + /// + /// The id for the mesh. + /// The set of vertices. + /// The minimum number of vertices on the same mesh required to return true. + /// True if N vertices from the set are on the mesh. + public static bool MultipleNearbyVerticesOnSameMesh(int meshId, IEnumerable vertexKeys, + int minSetSize) + { + ushort vertexCountOnSameMesh = 0; + + foreach (VertexKey vertexKey in vertexKeys) + { + if (meshId == vertexKey.meshId) + { + vertexCountOnSameMesh++; + if (vertexCountOnSameMesh >= minSetSize) + return true; + } + } + + return false; } - Vector3 meshFaceNormal = CalculateNormal(verticesInModelSpace); - FaceKey meshFaceKey = new FaceKey(passedMesh.id, meshFace.id); - foreach (KeyValuePair pair in nearbyFacesInfo) { - FacePair facePair = new FacePair(); - // If it was possible to compare the faces add them to the closestFaces list. Faces aren't comparable if the - // angle between their normals is >= 90f. Or the faces don't face each other as defined by Plane.Raycast. - if (CompareFaces(meshFaceKey, meshFaceNormal, verticesInModelSpace, pair.Key, pair.Value, out facePair)) { - closestFaces.Add(facePair); - } + + /// + /// Checks to see if N faces from a set of faces are on the same mesh. + /// + /// The faces, as a List for efficiency. + /// The minimum number of faces on the same mesh required to return true. + /// The mesh with the most nearby faces on it. + /// True if N faces from the set are on a mesh. + public static bool TryFindingNearestMeshGivenNearbyFaces(List> faces, int minSetSize, + out int nearestMeshId) + { + Dictionary faceCountByMeshId = new Dictionary(); + int currentMaxCount = 0; + nearestMeshId = -1; + + foreach (DistancePair faceKeyPair in faces) + { + int meshId = faceKeyPair.value.meshId; + + // Set the current face count for this mesh to 1, or increment it. + int currentFaceCount = 0; + faceCountByMeshId.TryGetValue(meshId, out currentFaceCount); + currentFaceCount++; + faceCountByMeshId[meshId] = currentFaceCount; + + // Update the current max count if the current mesh has more references. + if (currentFaceCount > currentMaxCount) + { + currentMaxCount = currentFaceCount; + nearestMeshId = meshId; + } + } + + return currentMaxCount >= minSetSize; } - } - - if (closestFaces.Count() > 0) { - // Sort in ascending order of separation. - IEnumerable sortedClosestFaces = closestFaces.OrderBy(pair => pair.separation); - closestFace = sortedClosestFaces.First(); - return true; - } - - // If no faces were comparable return nothing. - closestFace = new FacePair(); - return false; - } - /// - /// Compares two faces. Finds the physical separation between the faces as the average distance of each vertex - /// in the fromFace to the plane created by the toFace. Also finds the angle between the normals of the face - /// which defines flushness. Two faces are defined as flush if they have an angle of 180 degrees between their - /// normals. - /// - /// The key of the face we are comparing the difference from, to the other face. - /// The normal of the fromFace. - /// The coplanar vertices that make up the fromFace. - /// The key of the face we are comparing the difference to, from the other face. - /// The plane defined by the toFace. - /// - /// A FairPair containing both faces, their separation and their angle from being flush. - /// - /// - /// Whether the faces were comparable. Faces are only comparable if Plane.Raycast() returns true. This happens - /// when the face normals have an angle > 90f. Or when they face each other. - /// - public static bool CompareFaces(FaceKey fromFaceKey, Vector3 fromFaceNormal, - List fromFaceVertices, FaceKey toFaceKey, FaceInfo toFaceInfo, out FacePair facePair) { - Vector3 fromFaceCenter = MeshMath.CalculateGeometricCenter(fromFaceVertices); - - Ray normalRay = new Ray(fromFaceCenter, fromFaceNormal); - // The length of the Raycast before it enters the plane. - float intersectionLength; - - // The angle is calculated as the degrees from flush the normals are. Essentially the number of degrees the faces - // would have to be rotated to be flush. Faces are flush if there is 180 degrees between their normals. - facePair.angle = Vector3.Angle(fromFaceNormal, toFaceInfo.plane.normal); - - bool normalsPointTowardEachOther = toFaceInfo.plane.Raycast(normalRay, out intersectionLength); - float estimatedFaceSeparation = Vector3.Distance(fromFaceCenter, toFaceInfo.baryCenter); - - facePair.separation = (intersectionLength + estimatedFaceSeparation) / 2.0f; - facePair.fromFaceKey = fromFaceKey; - facePair.toFaceKey = toFaceKey; - facePair.toFaceModelSpaceCenter = toFaceInfo.baryCenter; - facePair.fromFaceModelSpaceCenter = fromFaceCenter; - - return normalsPointTowardEachOther; - } + /// + /// Finds the closest face in a mesh to a list of faces. Done by comparing the separation between the faces, + /// defined as the average distance of each vertex on the mesh face to the plane created by the other face. It + /// also uses the angle between the faces normals as a measure for "flushness". + /// + /// + /// The faces near enough to the mesh for comparison, as a List for efficiency. + /// + /// The unrotated, unoffset mesh being compared to. + /// The offset of the mesh. + /// The rotation of the mesh. + /// The model the faces belong to. + /// The degree at which two faces are too "unflush" to be close. + /// The closest pair of faces. + /// + /// Whether there were any comparable faces. Faces are only comparable if Plane.Raycast() returns true. This + /// happens when the face normals have an angle > 90f. Or when they face each other. + /// + public static bool FindClosestFace(List> nearbyFaces, MMesh passedMesh, Vector3 meshOffset, + Quaternion meshRotation, Model model, float angleThreshold, out FacePair closestFace) + { + List closestFaces = new List(); + Dictionary nearbyFacesInfo = new Dictionary(); + + foreach (DistancePair nearbyFaceKeyPair in nearbyFaces) + { + FaceKey nearbyFaceKey = nearbyFaceKeyPair.value; + MMesh nearbyMesh = model.GetMesh(nearbyFaceKey.meshId); + Face nearbyFace = nearbyMesh.GetFace(nearbyFaceKey.faceId); + FaceInfo nearbyFaceInfo = new FaceInfo(); + List nearbyFaceVertices = new List(); + + for (int i = 0; i < nearbyFace.vertexIds.Count(); i++) + { + nearbyFaceVertices.Add(nearbyMesh.VertexPositionInModelCoords(nearbyFace.vertexIds[i])); + } + + nearbyFaceInfo.baryCenter = MeshMath.CalculateGeometricCenter(nearbyFaceVertices); + nearbyFaceInfo.plane = new Plane( + CalculateModelSpaceNormal(nearbyFace, nearbyMesh), + nearbyMesh.VertexPositionInModelCoords(nearbyFace.vertexIds.First())); + + nearbyFacesInfo[nearbyFaceKey] = nearbyFaceInfo; + } - /// - /// Finds the average distance of a set of vertices from a plane. We use the signed difference from point to - /// plane so that faces that intersect each other are considered closest. - /// - /// The plane the vertices are separated from. - /// The vertices whose distance from the plane is being calculated. - /// The average distance for all vertices. - public static float AverageDistanceFromPlane(Plane plane, IEnumerable vertices) { - // Avoid a divide by zero error. - if (vertices.Count() == 0) - return 0; - - float sum = 0; - foreach (Vector3 vertex in vertices) { - // We want the distance from point to plane so we take the inverse. - sum += -plane.GetDistanceToPoint(vertex); - } - - return (sum / vertices.Count()); - } + // Compare each pair of faces. + foreach (Face meshFace in passedMesh.GetFaces()) + { + List verticesInModelSpace = new List(meshFace.vertexIds.Count); + for (int i = 0; i < meshFace.vertexIds.Count; i++) + { + Vector3 positionMeshSpace = passedMesh.VertexPositionInMeshCoords(meshFace.vertexIds[i]); + Vector3 positionModelSpace = (meshRotation * positionMeshSpace) + meshOffset; + verticesInModelSpace.Add(positionModelSpace); + } + Vector3 meshFaceNormal = CalculateNormal(verticesInModelSpace); + FaceKey meshFaceKey = new FaceKey(passedMesh.id, meshFace.id); + foreach (KeyValuePair pair in nearbyFacesInfo) + { + FacePair facePair = new FacePair(); + // If it was possible to compare the faces add them to the closestFaces list. Faces aren't comparable if the + // angle between their normals is >= 90f. Or the faces don't face each other as defined by Plane.Raycast. + if (CompareFaces(meshFaceKey, meshFaceNormal, verticesInModelSpace, pair.Key, pair.Value, out facePair)) + { + closestFaces.Add(facePair); + } + } + } - /// - /// Finds the closest vertex from the faces of a mesh. Given a set of nearby faces, finds which vertex is closest - /// to any face in the mesh. The closeness is determined by the distance between the vertex and the center of the - /// face. We choose the center of the face because the center will be snapped to the vertex and that is the - /// distance we want to minimize. - /// - /// - /// The list of possible nearbyVertices, as DistancePair for efficiency. - /// - /// The unrotated, unoffset mesh whose faces are being compared to the vertices. - /// The offset of the mesh. - /// The rotation of the mesh. - /// The model the vertices belongs to. - /// - /// A FaceVertexPair with the vertex and the face that are least separated plus the distance between them. - /// - public static FaceVertexPair FindClosestVertex(List> nearbyVertices, MMesh passedMesh, Vector3 meshOffset, - Quaternion meshRotation, Model model) { - List closestVertices = new List(); - - // Create a version of the mesh that is positioned and rotated correctly without altering the true mesh. - MMesh mesh = passedMesh.Clone(); - mesh.rotation = meshRotation; - mesh.offset = meshOffset; - - Dictionary faceCenters = new Dictionary(); - - foreach (Face face in mesh.GetFaces()) { - List faceVertices = new List(face.vertexIds.Count); - foreach (int vertexId in face.vertexIds) { - faceVertices.Add(mesh.VertexPositionInModelCoords(vertexId)); + if (closestFaces.Count() > 0) + { + // Sort in ascending order of separation. + IEnumerable sortedClosestFaces = closestFaces.OrderBy(pair => pair.separation); + closestFace = sortedClosestFaces.First(); + return true; + } + + // If no faces were comparable return nothing. + closestFace = new FacePair(); + return false; } - faceCenters[new FaceKey(mesh.id, face.id)] = - CalculateGeometricCenter(faceVertices); - } - - // Compare each face-vertex pair. - foreach (DistancePair nearbyVertexKeyPair in nearbyVertices) { - VertexKey nearbyVertexKey = nearbyVertexKeyPair.value; - MMesh nearbyMesh = model.GetMesh(nearbyVertexKey.meshId); - Vector3 vertex = nearbyMesh.VertexPositionInModelCoords(nearbyVertexKey.vertexId); - foreach (KeyValuePair pair in faceCenters) { - closestVertices.Add(CompareFaceAndVertex(pair.Key, pair.Value, nearbyVertexKey, vertex)); + + /// + /// Compares two faces. Finds the physical separation between the faces as the average distance of each vertex + /// in the fromFace to the plane created by the toFace. Also finds the angle between the normals of the face + /// which defines flushness. Two faces are defined as flush if they have an angle of 180 degrees between their + /// normals. + /// + /// The key of the face we are comparing the difference from, to the other face. + /// The normal of the fromFace. + /// The coplanar vertices that make up the fromFace. + /// The key of the face we are comparing the difference to, from the other face. + /// The plane defined by the toFace. + /// + /// A FairPair containing both faces, their separation and their angle from being flush. + /// + /// + /// Whether the faces were comparable. Faces are only comparable if Plane.Raycast() returns true. This happens + /// when the face normals have an angle > 90f. Or when they face each other. + /// + public static bool CompareFaces(FaceKey fromFaceKey, Vector3 fromFaceNormal, + List fromFaceVertices, FaceKey toFaceKey, FaceInfo toFaceInfo, out FacePair facePair) + { + Vector3 fromFaceCenter = MeshMath.CalculateGeometricCenter(fromFaceVertices); + + Ray normalRay = new Ray(fromFaceCenter, fromFaceNormal); + // The length of the Raycast before it enters the plane. + float intersectionLength; + + // The angle is calculated as the degrees from flush the normals are. Essentially the number of degrees the faces + // would have to be rotated to be flush. Faces are flush if there is 180 degrees between their normals. + facePair.angle = Vector3.Angle(fromFaceNormal, toFaceInfo.plane.normal); + + bool normalsPointTowardEachOther = toFaceInfo.plane.Raycast(normalRay, out intersectionLength); + float estimatedFaceSeparation = Vector3.Distance(fromFaceCenter, toFaceInfo.baryCenter); + + facePair.separation = (intersectionLength + estimatedFaceSeparation) / 2.0f; + facePair.fromFaceKey = fromFaceKey; + facePair.toFaceKey = toFaceKey; + facePair.toFaceModelSpaceCenter = toFaceInfo.baryCenter; + facePair.fromFaceModelSpaceCenter = fromFaceCenter; + + return normalsPointTowardEachOther; } - } - // Order them by ascending separation and return the least separated. - return closestVertices.OrderBy(pair => pair.separation).First(); - } + /// + /// Finds the average distance of a set of vertices from a plane. We use the signed difference from point to + /// plane so that faces that intersect each other are considered closest. + /// + /// The plane the vertices are separated from. + /// The vertices whose distance from the plane is being calculated. + /// The average distance for all vertices. + public static float AverageDistanceFromPlane(Plane plane, IEnumerable vertices) + { + // Avoid a divide by zero error. + if (vertices.Count() == 0) + return 0; + + float sum = 0; + foreach (Vector3 vertex in vertices) + { + // We want the distance from point to plane so we take the inverse. + sum += -plane.GetDistanceToPoint(vertex); + } - /// - /// Finds the nearest vertex to a position from a list of nearbyVertices. - /// - /// The vertices to choose the nearest from. - /// The position being compared to the vertices. - /// The model the vertices belong to. - /// The position of the nearest vertex. - public static Vector3 FindClosestVertex(List nearbyVertices, Vector3 position, Model model) { - Vector3 nearestVertex = Vector3.zero; - float distance = Mathf.Infinity; - - foreach (VertexKey nearbyVertexKey in nearbyVertices) { - MMesh nearbyMesh = model.GetMesh(nearbyVertexKey.meshId); - Vector3 vertex = nearbyMesh.VertexPositionInModelCoords(nearbyVertexKey.vertexId); - float currentDistance = Mathf.Abs(vertex.sqrMagnitude - position.sqrMagnitude); - - if (currentDistance < distance) { - distance = currentDistance; - nearestVertex = vertex; + return (sum / vertices.Count()); } - } - return nearestVertex; - } + /// + /// Finds the closest vertex from the faces of a mesh. Given a set of nearby faces, finds which vertex is closest + /// to any face in the mesh. The closeness is determined by the distance between the vertex and the center of the + /// face. We choose the center of the face because the center will be snapped to the vertex and that is the + /// distance we want to minimize. + /// + /// + /// The list of possible nearbyVertices, as DistancePair for efficiency. + /// + /// The unrotated, unoffset mesh whose faces are being compared to the vertices. + /// The offset of the mesh. + /// The rotation of the mesh. + /// The model the vertices belongs to. + /// + /// A FaceVertexPair with the vertex and the face that are least separated plus the distance between them. + /// + public static FaceVertexPair FindClosestVertex(List> nearbyVertices, MMesh passedMesh, Vector3 meshOffset, + Quaternion meshRotation, Model model) + { + List closestVertices = new List(); + + // Create a version of the mesh that is positioned and rotated correctly without altering the true mesh. + MMesh mesh = passedMesh.Clone(); + mesh.rotation = meshRotation; + mesh.offset = meshOffset; + + Dictionary faceCenters = new Dictionary(); + + foreach (Face face in mesh.GetFaces()) + { + List faceVertices = new List(face.vertexIds.Count); + foreach (int vertexId in face.vertexIds) + { + faceVertices.Add(mesh.VertexPositionInModelCoords(vertexId)); + } + faceCenters[new FaceKey(mesh.id, face.id)] = + CalculateGeometricCenter(faceVertices); + } - /// - /// Compares a face and vertex. Calculates the distance from the faces center to the vertex. - /// - /// The key of the face being compared. - /// The center of the face being compared. - /// The key of the vertex being compared. - /// The position of the vertex being compared. - /// A FaceVertexPair with the face, the vertex and their separation. - public static FaceVertexPair CompareFaceAndVertex(FaceKey faceKey, Vector3 faceCenter, VertexKey vertexKey, - Vector3 vertex) { - FaceVertexPair faceVertexPair = new FaceVertexPair(); - // Find the separation between the two as the distance from the vertex to the faces center. - faceVertexPair.separation = Vector3.Distance(vertex, faceCenter); - faceVertexPair.vertexKey = vertexKey; - faceVertexPair.faceKey = faceKey; - - return faceVertexPair; - } + // Compare each face-vertex pair. + foreach (DistancePair nearbyVertexKeyPair in nearbyVertices) + { + VertexKey nearbyVertexKey = nearbyVertexKeyPair.value; + MMesh nearbyMesh = model.GetMesh(nearbyVertexKey.meshId); + Vector3 vertex = nearbyMesh.VertexPositionInModelCoords(nearbyVertexKey.vertexId); + foreach (KeyValuePair pair in faceCenters) + { + closestVertices.Add(CompareFaceAndVertex(pair.Key, pair.Value, nearbyVertexKey, vertex)); + } + } - /// - /// Finds which edge in a face most represents the other edges. It does this by choosing an edge that is - /// perpendicular to the greatest number of other edges. - /// - /// The coplanar vertices representing the face. - /// The most representative edge. - public static EdgeInfo FindMostRepresentativeEdge(List coplanarFaceVertices) { - // Start by cleaning out any colinear vertices (subedges). - IEnumerable cornerVertices = FindCornerVertices(coplanarFaceVertices); - // Create a dictionary that will hold an edge and the number of edges perpendicular to this edge. - Dictionary similarEdges = new Dictionary(); - - // Iterate through every edge. - for (int index = 0; index < cornerVertices.Count(); index++) { - // Find the vertices. - Vector3 v1 = cornerVertices.ElementAt(index); - Vector3 v2 = cornerVertices.ElementAt((index + 1) % cornerVertices.Count()); ; - - // Find the current edge. - Vector3 currentEdge = (v2 - v1); - - bool foundSimilarEdge = false; - - // Check if the current edge is perpendicular to one of the edges in similarEdges. Two vectors are - // perpendicular if their dot product is 0. - foreach (KeyValuePair edges in similarEdges) { - if (Mathf.Abs(Vector3.Dot(edges.Key.edgeVector, currentEdge.normalized) - 0) < Math3d.EPSILON) { - // If they are similar, update the edgeCount and break to avoid iterating through the dictionary. - similarEdges[edges.Key] = similarEdges[edges.Key] + 1; - foundSimilarEdge = true; - break; - } + // Order them by ascending separation and return the least separated. + return closestVertices.OrderBy(pair => pair.separation).First(); } - // If we there was no similar edge, create a new edge entry in similarEdges. - if (!foundSimilarEdge) { - EdgeInfo currentEdgeInfo = new EdgeInfo(); - currentEdgeInfo.edgeVector = currentEdge; - currentEdgeInfo.edgeStart = v1; - similarEdges[currentEdgeInfo] = 1; + /// + /// Finds the nearest vertex to a position from a list of nearbyVertices. + /// + /// The vertices to choose the nearest from. + /// The position being compared to the vertices. + /// The model the vertices belong to. + /// The position of the nearest vertex. + public static Vector3 FindClosestVertex(List nearbyVertices, Vector3 position, Model model) + { + Vector3 nearestVertex = Vector3.zero; + float distance = Mathf.Infinity; + + foreach (VertexKey nearbyVertexKey in nearbyVertices) + { + MMesh nearbyMesh = model.GetMesh(nearbyVertexKey.meshId); + Vector3 vertex = nearbyMesh.VertexPositionInModelCoords(nearbyVertexKey.vertexId); + float currentDistance = Mathf.Abs(vertex.sqrMagnitude - position.sqrMagnitude); + + if (currentDistance < distance) + { + distance = currentDistance; + nearestVertex = vertex; + } + } + + return nearestVertex; } - } - return similarEdges.OrderByDescending(pair => pair.Value).First().Key; - } + /// + /// Compares a face and vertex. Calculates the distance from the faces center to the vertex. + /// + /// The key of the face being compared. + /// The center of the face being compared. + /// The key of the vertex being compared. + /// The position of the vertex being compared. + /// A FaceVertexPair with the face, the vertex and their separation. + public static FaceVertexPair CompareFaceAndVertex(FaceKey faceKey, Vector3 faceCenter, VertexKey vertexKey, + Vector3 vertex) + { + FaceVertexPair faceVertexPair = new FaceVertexPair(); + // Find the separation between the two as the distance from the vertex to the faces center. + faceVertexPair.separation = Vector3.Distance(vertex, faceCenter); + faceVertexPair.vertexKey = vertexKey; + faceVertexPair.faceKey = faceKey; + + return faceVertexPair; + } - /// - /// Checks to see if a given vertex is common amongst at least two nearby edges. - /// - /// This function takes in DistancePairs to avoid parsing through the lists returned by the SpatialIndex - /// before it is necessary. - /// - /// The edges the vertex could belong to. - /// The given vertex. - /// The model the vertex and edges belong to. - /// True if the vertex is common between two edges. - public static bool FindCommonVertex(List> edges, DistancePair vertex, - Model model) { - int meshId = vertex.value.meshId; - int vertexId = vertex.value.vertexId; - int edgeCount = 0; - - foreach (DistancePair edgePair in edges) { - MMesh mesh = model.GetMesh(edgePair.value.meshId); - if (vertexId == edgePair.value.vertexId1 || vertexId == edgePair.value.vertexId2) { - if (++edgeCount >= 2) { - return true; - } + /// + /// Finds which edge in a face most represents the other edges. It does this by choosing an edge that is + /// perpendicular to the greatest number of other edges. + /// + /// The coplanar vertices representing the face. + /// The most representative edge. + public static EdgeInfo FindMostRepresentativeEdge(List coplanarFaceVertices) + { + // Start by cleaning out any colinear vertices (subedges). + IEnumerable cornerVertices = FindCornerVertices(coplanarFaceVertices); + // Create a dictionary that will hold an edge and the number of edges perpendicular to this edge. + Dictionary similarEdges = new Dictionary(); + + // Iterate through every edge. + for (int index = 0; index < cornerVertices.Count(); index++) + { + // Find the vertices. + Vector3 v1 = cornerVertices.ElementAt(index); + Vector3 v2 = cornerVertices.ElementAt((index + 1) % cornerVertices.Count()); ; + + // Find the current edge. + Vector3 currentEdge = (v2 - v1); + + bool foundSimilarEdge = false; + + // Check if the current edge is perpendicular to one of the edges in similarEdges. Two vectors are + // perpendicular if their dot product is 0. + foreach (KeyValuePair edges in similarEdges) + { + if (Mathf.Abs(Vector3.Dot(edges.Key.edgeVector, currentEdge.normalized) - 0) < Math3d.EPSILON) + { + // If they are similar, update the edgeCount and break to avoid iterating through the dictionary. + similarEdges[edges.Key] = similarEdges[edges.Key] + 1; + foundSimilarEdge = true; + break; + } + } + + // If we there was no similar edge, create a new edge entry in similarEdges. + if (!foundSimilarEdge) + { + EdgeInfo currentEdgeInfo = new EdgeInfo(); + currentEdgeInfo.edgeVector = currentEdge; + currentEdgeInfo.edgeStart = v1; + similarEdges[currentEdgeInfo] = 1; + } + } + + return similarEdges.OrderByDescending(pair => pair.Value).First().Key; } - } - return false; - } - /// - /// Checks to see if a given edge is common amongst at least two nearby faces. - /// - /// This function takes in DistancePairs to avoid parsing through the lists returned by the SpatialIndex - /// before it is necessary. - /// - /// The faces the edge could belong to. - /// The given edge. - /// The model the edge and faces belong to. - /// True if the edge is common between two faces. - public static bool FindCommonEdge(List> faces, DistancePair edge, Model model) { - int meshId = edge.value.meshId; - int vertexId1 = edge.value.vertexId1; - int vertexId2 = edge.value.vertexId2; - int faceCount = 0; - - foreach (DistancePair facePair in faces) { - if (meshId == facePair.value.meshId) { - Face face = model.GetMesh(facePair.value.meshId).GetFace(facePair.value.faceId); - // .Contains() is slow to call. There is no work around currently but we should revisit this if it's - // causing problems. - if (face.vertexIds.Contains(vertexId1) && face.vertexIds.Contains(vertexId2)) { - if (++faceCount >= 2) { - return true; + /// + /// Checks to see if a given vertex is common amongst at least two nearby edges. + /// + /// This function takes in DistancePairs to avoid parsing through the lists returned by the SpatialIndex + /// before it is necessary. + /// + /// The edges the vertex could belong to. + /// The given vertex. + /// The model the vertex and edges belong to. + /// True if the vertex is common between two edges. + public static bool FindCommonVertex(List> edges, DistancePair vertex, + Model model) + { + int meshId = vertex.value.meshId; + int vertexId = vertex.value.vertexId; + int edgeCount = 0; + + foreach (DistancePair edgePair in edges) + { + MMesh mesh = model.GetMesh(edgePair.value.meshId); + if (vertexId == edgePair.value.vertexId1 || vertexId == edgePair.value.vertexId2) + { + if (++edgeCount >= 2) + { + return true; + } + } } - } + return false; } - } - return false; - } + /// + /// Checks to see if a given edge is common amongst at least two nearby faces. + /// + /// This function takes in DistancePairs to avoid parsing through the lists returned by the SpatialIndex + /// before it is necessary. + /// + /// The faces the edge could belong to. + /// The given edge. + /// The model the edge and faces belong to. + /// True if the edge is common between two faces. + public static bool FindCommonEdge(List> faces, DistancePair edge, Model model) + { + int meshId = edge.value.meshId; + int vertexId1 = edge.value.vertexId1; + int vertexId2 = edge.value.vertexId2; + int faceCount = 0; + + foreach (DistancePair facePair in faces) + { + if (meshId == facePair.value.meshId) + { + Face face = model.GetMesh(facePair.value.meshId).GetFace(facePair.value.faceId); + // .Contains() is slow to call. There is no work around currently but we should revisit this if it's + // causing problems. + if (face.vertexIds.Contains(vertexId1) && face.vertexIds.Contains(vertexId2)) + { + if (++faceCount >= 2) + { + return true; + } + } + } + } - /// - /// Checks to see if 75% of given vertices belong to a given face. - /// - /// This function takes in DistancePairs to avoid parsing through the lists returned by the SpatialIndex - /// before it is necessary. - /// - /// The nearest vertices that could belong to the face. - /// The given face. - /// The model the vertices and face belong to. - /// True if 75% of the given vertices belong to the given face. - public static bool FindCommonFace(List> vertices, DistancePair face, - Model model) { - int meshId = face.value.meshId; - int faceId = face.value.faceId; - Face f = model.GetMesh(meshId).GetFace(faceId); - // Precalculate 75% of the vertices. - float minCount = f.vertexIds.Count() * 0.75f; - int vertexCount = 0; - - foreach (DistancePair vertexPair in vertices) { - // .Contains() is slow. We should investigate work arounds if possible. - if (meshId == vertexPair.value.meshId && f.vertexIds.Contains(vertexPair.value.vertexId)) { - - if (++vertexCount > minCount) { - return true; - } + return false; } - } - return false; - } - /// - /// Checks to see if 75% of given edges belong to a given face. - /// - /// This function takes in DistancePairs to avoid parsing through the lists returned by the SpatialIndex - /// before it is necessary. - /// - /// The nearest edges that could belong to the face. - /// The given face. - /// The model the vertices and face belong to. - /// True if 75% of the given edges belong to the given face. - public static bool FindCommonFace(List> edges, DistancePair face, Model model) { - int meshId = face.value.meshId; - int faceId = face.value.faceId; - Face f = model.GetMesh(meshId).GetFace(faceId); - // Precalculate 75% of the edges. - float minCount = (f.vertexIds.Count() - 2) * 0.75f; - int edgeCount = 0; - - foreach (DistancePair edgePair in edges) { - // .Contains() is slow. We should investigate work arounds if possible. - if (meshId == edgePair.value.meshId && - f.vertexIds.Contains(edgePair.value.vertexId1) && - f.vertexIds.Contains(edgePair.value.vertexId2)) { - if (++edgeCount > minCount) { - return true; - } + /// + /// Checks to see if 75% of given vertices belong to a given face. + /// + /// This function takes in DistancePairs to avoid parsing through the lists returned by the SpatialIndex + /// before it is necessary. + /// + /// The nearest vertices that could belong to the face. + /// The given face. + /// The model the vertices and face belong to. + /// True if 75% of the given vertices belong to the given face. + public static bool FindCommonFace(List> vertices, DistancePair face, + Model model) + { + int meshId = face.value.meshId; + int faceId = face.value.faceId; + Face f = model.GetMesh(meshId).GetFace(faceId); + // Precalculate 75% of the vertices. + float minCount = f.vertexIds.Count() * 0.75f; + int vertexCount = 0; + + foreach (DistancePair vertexPair in vertices) + { + // .Contains() is slow. We should investigate work arounds if possible. + if (meshId == vertexPair.value.meshId && f.vertexIds.Contains(vertexPair.value.vertexId)) + { + + if (++vertexCount > minCount) + { + return true; + } + } + } + return false; } - } - return false; - } - - /// - /// Finds the height of a regular polygon. Which is either twice the apothem if the polygon has an even - /// number of vertices or the apothem + radius if the polygon has an odd number of vertices. - /// - /// The height. - public static float FindHeightOfARegularPolygonalFace(List vertices) { - float apothem = (Vector3.Distance(vertices[0], vertices[1])) / (2 * Mathf.Tan(Mathf.PI / vertices.Count)); - - // If the number of vertices is even the height is twice the apothem. - if (vertices.Count() % 2 == 0) { - return apothem * 2; - } else { - // If there is an odd number of vertices the height is the apothem plus the radius. - float radius = apothem / (Mathf.Cos(Mathf.PI / vertices.Count)); - return radius + apothem; - } - } - /// - /// Finds the radius of a regular polygon. - /// - /// The radius. - public static float FindRadiusOfARegularPolygonalFace(List vertices) { - float sideLength = Vector3.Distance(vertices[0], vertices[1]); + /// + /// Checks to see if 75% of given edges belong to a given face. + /// + /// This function takes in DistancePairs to avoid parsing through the lists returned by the SpatialIndex + /// before it is necessary. + /// + /// The nearest edges that could belong to the face. + /// The given face. + /// The model the vertices and face belong to. + /// True if 75% of the given edges belong to the given face. + public static bool FindCommonFace(List> edges, DistancePair face, Model model) + { + int meshId = face.value.meshId; + int faceId = face.value.faceId; + Face f = model.GetMesh(meshId).GetFace(faceId); + // Precalculate 75% of the edges. + float minCount = (f.vertexIds.Count() - 2) * 0.75f; + int edgeCount = 0; + + foreach (DistancePair edgePair in edges) + { + // .Contains() is slow. We should investigate work arounds if possible. + if (meshId == edgePair.value.meshId && + f.vertexIds.Contains(edgePair.value.vertexId1) && + f.vertexIds.Contains(edgePair.value.vertexId2)) + { + if (++edgeCount > minCount) + { + return true; + } + } + } + return false; + } - return sideLength / (2.0f * Mathf.Sin(Mathf.PI / vertices.Count)); - } + /// + /// Finds the height of a regular polygon. Which is either twice the apothem if the polygon has an even + /// number of vertices or the apothem + radius if the polygon has an odd number of vertices. + /// + /// The height. + public static float FindHeightOfARegularPolygonalFace(List vertices) + { + float apothem = (Vector3.Distance(vertices[0], vertices[1])) / (2 * Mathf.Tan(Mathf.PI / vertices.Count)); + + // If the number of vertices is even the height is twice the apothem. + if (vertices.Count() % 2 == 0) + { + return apothem * 2; + } + else + { + // If there is an odd number of vertices the height is the apothem plus the radius. + float radius = apothem / (Mathf.Cos(Mathf.PI / vertices.Count)); + return radius + apothem; + } + } - /// - /// Calculates the positions of vertices belonging to an MMesh for a given mesh position and rotation. - /// Extremely useful for doing geometry math on the MMesh at a different position or rotation then recorded - /// in MMesh. - /// - /// The ids of the vertices whose positions are being calculated. - /// The mesh the vertices belong to. - /// The position the calculations are being done at. - /// The rotation the calculations are being done at. - /// The positions of the vertices for the given mesh position and rotation. - public static List CalculateVertexPositions(ReadOnlyCollection vertexIds, MMesh mesh, - Vector3 meshPosition, Quaternion meshRotation) { - List vertexPositions = new List(vertexIds.Count()); - - for (int i = 0; i < vertexIds.Count(); i++) { - vertexPositions.Add( - (meshRotation * mesh.VertexPositionInMeshCoords(vertexIds[i])) + meshPosition); - } - - return vertexPositions; + /// + /// Finds the radius of a regular polygon. + /// + /// The radius. + public static float FindRadiusOfARegularPolygonalFace(List vertices) + { + float sideLength = Vector3.Distance(vertices[0], vertices[1]); + + return sideLength / (2.0f * Mathf.Sin(Mathf.PI / vertices.Count)); + } + + /// + /// Calculates the positions of vertices belonging to an MMesh for a given mesh position and rotation. + /// Extremely useful for doing geometry math on the MMesh at a different position or rotation then recorded + /// in MMesh. + /// + /// The ids of the vertices whose positions are being calculated. + /// The mesh the vertices belong to. + /// The position the calculations are being done at. + /// The rotation the calculations are being done at. + /// The positions of the vertices for the given mesh position and rotation. + public static List CalculateVertexPositions(ReadOnlyCollection vertexIds, MMesh mesh, + Vector3 meshPosition, Quaternion meshRotation) + { + List vertexPositions = new List(vertexIds.Count()); + + for (int i = 0; i < vertexIds.Count(); i++) + { + vertexPositions.Add( + (meshRotation * mesh.VertexPositionInMeshCoords(vertexIds[i])) + meshPosition); + } + + return vertexPositions; + } } - } } diff --git a/Assets/Scripts/model/core/MeshRenderOwner.cs b/Assets/Scripts/model/core/MeshRenderOwner.cs index 3b6e216d..caf11000 100644 --- a/Assets/Scripts/model/core/MeshRenderOwner.cs +++ b/Assets/Scripts/model/core/MeshRenderOwner.cs @@ -12,39 +12,42 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace com.google.apps.peltzer.client.model.core { - /// - /// Class that owns responsibility for rendering a particular mesh. CLASSES OTHER THAN MODEL SHOULD ONLY CALL THIS - /// ON MODEL. - /// /// - public interface IMeshRenderOwner { +namespace com.google.apps.peltzer.client.model.core +{ /// - /// Claim responsibility for rendering a mesh from this class. - /// - /// The id of the mesh being claimed - /// The id of the mesh that was claimed, or -1 for failure. - int ClaimMesh(int meshId, IMeshRenderOwner fosterRenderer); - } + /// Class that owns responsibility for rendering a particular mesh. CLASSES OTHER THAN MODEL SHOULD ONLY CALL THIS + /// ON MODEL. + /// /// + public interface IMeshRenderOwner + { + /// + /// Claim responsibility for rendering a mesh from this class. + /// + /// The id of the mesh being claimed + /// The id of the mesh that was claimed, or -1 for failure. + int ClaimMesh(int meshId, IMeshRenderOwner fosterRenderer); + } - // Interface naming courtesy of Java. - // This interface marks a class as the owner of the MeshRenderOwner ownership list - ie, Model. This is the only - // class that mesh ownership can be relinquished to. - public interface IMeshRenderOwnerOwner { - /// - /// Gives responsibility for rendering a mesh to this class. Generally, this should only be done to Model - the - /// general dynamic being that tool classes attempt to claim ownership whenever they need a - /// preview (which will in turn cause model to call Claim on the current owner), and then bequeath it back to Model - /// when they are done (provided they still own the mesh.) - /// - /// The id of the mesh being bequeathed - /// The id of the mesh that is being bequeathed, or -1 for failure. - void RelinquishMesh(int meshId, IMeshRenderOwner fosterRenderer); + // Interface naming courtesy of Java. + // This interface marks a class as the owner of the MeshRenderOwner ownership list - ie, Model. This is the only + // class that mesh ownership can be relinquished to. + public interface IMeshRenderOwnerOwner + { + /// + /// Gives responsibility for rendering a mesh to this class. Generally, this should only be done to Model - the + /// general dynamic being that tool classes attempt to claim ownership whenever they need a + /// preview (which will in turn cause model to call Claim on the current owner), and then bequeath it back to Model + /// when they are done (provided they still own the mesh.) + /// + /// The id of the mesh being bequeathed + /// The id of the mesh that is being bequeathed, or -1 for failure. + void RelinquishMesh(int meshId, IMeshRenderOwner fosterRenderer); - /// - /// Claim responsibility for rendering a mesh from this class if and only if it is unowned by another renderer. - /// - /// The id of the mesh being claimed - /// The id of the mesh that was claimed, or -1 for failure. - int ClaimMeshIfUnowned(int meshId, IMeshRenderOwner fosterRenderer); - } + /// + /// Claim responsibility for rendering a mesh from this class if and only if it is unowned by another renderer. + /// + /// The id of the mesh being claimed + /// The id of the mesh that was claimed, or -1 for failure. + int ClaimMeshIfUnowned(int meshId, IMeshRenderOwner fosterRenderer); + } } \ No newline at end of file diff --git a/Assets/Scripts/model/core/MeshRepresentationCache.cs b/Assets/Scripts/model/core/MeshRepresentationCache.cs index 5526278a..b65aed16 100644 --- a/Assets/Scripts/model/core/MeshRepresentationCache.cs +++ b/Assets/Scripts/model/core/MeshRepresentationCache.cs @@ -19,291 +19,337 @@ using com.google.apps.peltzer.client.model.render; using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.model.core { - /// - /// Cache of useful representations of Meshes. - /// - public class MeshRepresentationCache : MonoBehaviour { - // Stagger out the cost of triangulation over many frames. The smaller this number is, the longer it'll take - // operations to 'fade in'. - private const int MAX_CACHE_MISSES_PER_FRAME = 20; - private int cacheMissesThisFrame = 0; - - // Dictionary of mesh ID to the corresponding standard preview. - private Dictionary normalTemplateForMeshId = new Dictionary(); - // Dictionary of mesh ID to the corresponding preview, by material. - private Dictionary> highlightedTemplatesForMeshIdsByMaterial = - new Dictionary>(); - // Dictionary of mesh ID to: - // Dictionaries of material IDs to the components (tris, verts) of that mesh with the given material. - private Dictionary> componentsByMaterialForMeshId = - new Dictionary>(); - - // Dictionary of mesh ID to: - // Dictionaries of material IDs to the components (tris, verts) of that mesh with the given material. - private Dictionary> meshSpaceComponentsByMaterialForMeshId = - new Dictionary>(); - - private Model model; - private WorldSpace worldSpace; - +namespace com.google.apps.peltzer.client.model.core +{ /// - /// Sets up the cache. + /// Cache of useful representations of Meshes. /// - /// The model we are working on. - /// The world space. - public void Setup(Model model, WorldSpace worldSpace) { - this.model = model; - this.worldSpace = worldSpace; + public class MeshRepresentationCache : MonoBehaviour + { + // Stagger out the cost of triangulation over many frames. The smaller this number is, the longer it'll take + // operations to 'fade in'. + private const int MAX_CACHE_MISSES_PER_FRAME = 20; + private int cacheMissesThisFrame = 0; - // When meshes get modified or deleted in the model, we have to invalidate the corresponding previews. - model.OnMeshChanged += (MMesh mesh, bool materialsChanged, bool geometryChanged, bool facesOrVertsChanged) => { - // Only invalidate if the mesh mutated (but not if it was just transformed). - if (materialsChanged || geometryChanged) { - InvalidatePreviews(mesh.id); - } - componentsByMaterialForMeshId.Remove(mesh.id); - if (facesOrVertsChanged) { - meshSpaceComponentsByMaterialForMeshId.Remove(mesh.id); - } - }; - model.OnMeshDeleted += (MMesh mesh) => { - InvalidatePreviews(mesh.id); - componentsByMaterialForMeshId.Remove(mesh.id); - meshSpaceComponentsByMaterialForMeshId.Remove(mesh.id); - }; - } + // Dictionary of mesh ID to the corresponding standard preview. + private Dictionary normalTemplateForMeshId = new Dictionary(); + // Dictionary of mesh ID to the corresponding preview, by material. + private Dictionary> highlightedTemplatesForMeshIdsByMaterial = + new Dictionary>(); + // Dictionary of mesh ID to: + // Dictionaries of material IDs to the components (tris, verts) of that mesh with the given material. + private Dictionary> componentsByMaterialForMeshId = + new Dictionary>(); - public void Update() { - // This is something of a trick: it is known that code depending on our cache miss logic - // only runs in LateUpdate, thus we reset this counter on Update. - cacheMissesThisFrame = 0; - } + // Dictionary of mesh ID to: + // Dictionaries of material IDs to the components (tris, verts) of that mesh with the given material. + private Dictionary> meshSpaceComponentsByMaterialForMeshId = + new Dictionary>(); - /// - /// Generates a preview for the given mesh. If we have a cached GameObject, we just clone it; if not, - /// we construct it from the MMesh (and cache it for next time). - /// - /// The mesh for which to create a preview. - /// - /// A material with which to colour the entire mesh, or null if the preview should use the same materials - /// as exist on the given mesh. - /// - /// The preview GameObject. - public GameObject GeneratePreview(MMesh mesh, MaterialAndColor highlightMaterial = null) { - // Find the right cache. - Dictionary dict; - if (highlightMaterial == null) { - dict = normalTemplateForMeshId; - } else if (!highlightedTemplatesForMeshIdsByMaterial.TryGetValue(highlightMaterial, out dict)) { - dict = new Dictionary(); - highlightedTemplatesForMeshIdsByMaterial[highlightMaterial] = dict; - } + private Model model; + private WorldSpace worldSpace; + + /// + /// Sets up the cache. + /// + /// The model we are working on. + /// The world space. + public void Setup(Model model, WorldSpace worldSpace) + { + this.model = model; + this.worldSpace = worldSpace; - // See if we have a cache hit. - GameObject template; - bool isCacheHit = dict.ContainsKey(mesh.id); - if (isCacheHit) { - // Yes! - template = dict[mesh.id]; - } else { - // Cache miss. Disappointing. Create it now, using the highlight material if requested. - template = new GameObject(); - Dictionary components; - bool componentsAreCached = componentsByMaterialForMeshId.TryGetValue(mesh.id, out components); - if (componentsAreCached) { - // If we have cached components, just use those to make the GameObject. - MeshWithMaterialRenderer renderer = template.AddComponent(); - renderer.Init(worldSpace); - renderer.meshes = MeshHelper.ToMeshes(components, highlightMaterial, mesh.rotation, mesh.offset); - renderer.SetPositionModelSpace(mesh.offset); - renderer.SetOrientationModelSpace(mesh.rotation, /* smooth */ false); - } else { - // Else, fetch the components of the mesh, make a GameObject, and cache the components if this mesh exists - // in the model. - MMesh.AttachMeshToGameObject(worldSpace, template, mesh, out components, - /* updateOnly */ false, highlightMaterial); - if (model.HasMesh(mesh.id)) { - componentsByMaterialForMeshId.Add(mesh.id, components); - } + // When meshes get modified or deleted in the model, we have to invalidate the corresponding previews. + model.OnMeshChanged += (MMesh mesh, bool materialsChanged, bool geometryChanged, bool facesOrVertsChanged) => + { + // Only invalidate if the mesh mutated (but not if it was just transformed). + if (materialsChanged || geometryChanged) + { + InvalidatePreviews(mesh.id); + } + componentsByMaterialForMeshId.Remove(mesh.id); + if (facesOrVertsChanged) + { + meshSpaceComponentsByMaterialForMeshId.Remove(mesh.id); + } + }; + model.OnMeshDeleted += (MMesh mesh) => + { + InvalidatePreviews(mesh.id); + componentsByMaterialForMeshId.Remove(mesh.id); + meshSpaceComponentsByMaterialForMeshId.Remove(mesh.id); + }; } - } - // Templates are inactive (we don't want them to appear on the scene!). - template.SetActive(false); + public void Update() + { + // This is something of a trick: it is known that code depending on our cache miss logic + // only runs in LateUpdate, thus we reset this counter on Update. + cacheMissesThisFrame = 0; + } - // Only store the preview in the cache if the mesh is in the model (otherwise it could be a temporary - // mesh that will later be discarded and we won't be warned about it). - if (!isCacheHit && model.HasMesh(mesh.id)) { - // Cache it. - dict[mesh.id] = template; - isCacheHit = true; - } + /// + /// Generates a preview for the given mesh. If we have a cached GameObject, we just clone it; if not, + /// we construct it from the MMesh (and cache it for next time). + /// + /// The mesh for which to create a preview. + /// + /// A material with which to colour the entire mesh, or null if the preview should use the same materials + /// as exist on the given mesh. + /// + /// The preview GameObject. + public GameObject GeneratePreview(MMesh mesh, MaterialAndColor highlightMaterial = null) + { + // Find the right cache. + Dictionary dict; + if (highlightMaterial == null) + { + dict = normalTemplateForMeshId; + } + else if (!highlightedTemplatesForMeshIdsByMaterial.TryGetValue(highlightMaterial, out dict)) + { + dict = new Dictionary(); + highlightedTemplatesForMeshIdsByMaterial[highlightMaterial] = dict; + } - // If we are not caching the template, we can return the template directly; if we are caching, - // we have to clone it. - GameObject result; - if (isCacheHit) { - // The template belongs to our cache, so we want to return a copy, not our precious cached template. - result = GameObject.Instantiate(template); - result.GetComponent().SetupAsCopyOf(template.GetComponent()); - } else { - // We're not caching this one, so just return the template. - result = template; - } + // See if we have a cache hit. + GameObject template; + bool isCacheHit = dict.ContainsKey(mesh.id); + if (isCacheHit) + { + // Yes! + template = dict[mesh.id]; + } + else + { + // Cache miss. Disappointing. Create it now, using the highlight material if requested. + template = new GameObject(); + Dictionary components; + bool componentsAreCached = componentsByMaterialForMeshId.TryGetValue(mesh.id, out components); + if (componentsAreCached) + { + // If we have cached components, just use those to make the GameObject. + MeshWithMaterialRenderer renderer = template.AddComponent(); + renderer.Init(worldSpace); + renderer.meshes = MeshHelper.ToMeshes(components, highlightMaterial, mesh.rotation, mesh.offset); + renderer.SetPositionModelSpace(mesh.offset); + renderer.SetOrientationModelSpace(mesh.rotation, /* smooth */ false); + } + else + { + // Else, fetch the components of the mesh, make a GameObject, and cache the components if this mesh exists + // in the model. + MMesh.AttachMeshToGameObject(worldSpace, template, mesh, out components, + /* updateOnly */ false, highlightMaterial); + if (model.HasMesh(mesh.id)) + { + componentsByMaterialForMeshId.Add(mesh.id, components); + } + } + } - // Position/rotate the gameObject so that the preview has the correct position. - result.GetComponent().SetPositionModelSpace(mesh.offset); - result.GetComponent().SetOrientationModelSpace(Math3d.Normalize(mesh.rotation), - /* smooth */ false); + // Templates are inactive (we don't want them to appear on the scene!). + template.SetActive(false); - result.SetActive(true); - return result; - } + // Only store the preview in the cache if the mesh is in the model (otherwise it could be a temporary + // mesh that will later be discarded and we won't be warned about it). + if (!isCacheHit && model.HasMesh(mesh.id)) + { + // Cache it. + dict[mesh.id] = template; + isCacheHit = true; + } - /// - /// Get the components, keyed by material ID, of a given mesh, fetching from the cache if possible, - /// else triangulating on-the-fly and then adding to the cache and returning. - /// If the mesh does not exist in the model, nothing will be added to the cache. - /// - /// The mesh for which to fetch the previews. - /// - /// A dictionary keyed by material ID, with values containing the vertices and triangles of the triangulated - /// mesh. - /// - public Dictionary FetchComponentsForMesh(MMesh mesh) { - Dictionary output; + // If we are not caching the template, we can return the template directly; if we are caching, + // we have to clone it. + GameObject result; + if (isCacheHit) + { + // The template belongs to our cache, so we want to return a copy, not our precious cached template. + result = GameObject.Instantiate(template); + result.GetComponent().SetupAsCopyOf(template.GetComponent()); + } + else + { + // We're not caching this one, so just return the template. + result = template; + } - // Look for a hit in the cache, and return it if found. - bool isCacheHit = componentsByMaterialForMeshId.TryGetValue(mesh.id, out output); - if (isCacheHit) { - return output; - } + // Position/rotate the gameObject so that the preview has the correct position. + result.GetComponent().SetPositionModelSpace(mesh.offset); + result.GetComponent().SetOrientationModelSpace(Math3d.Normalize(mesh.rotation), + /* smooth */ false); - // Else create the required information. - output = MeshHelper.MeshComponentsFromMMesh(mesh, /* useModelSpace */ true); - if (model.HasMesh(mesh.id)) { - // If a mesh with this ID exists in the model, we add an entry to the cache. - componentsByMaterialForMeshId.Add(mesh.id, output); - } - return output; - } + result.SetActive(true); + return result; + } - /// - /// Get the components, keyed by material ID, of a given mesh that is expected to exist in the model, - /// fetching from the cache if possible, else triangulating on-the-fly and then adding to the cache - /// and returning. - /// - /// The mesh id for which to fetch the previews. - /// - /// If true, and the mesh is not found in the cache, and there have been too many - /// cache misses already this frame, this method will return null. - /// - /// A dictionary keyed by material ID, with values containing the vertices and triangles of the triangulated - /// mesh. - /// - /// - /// If the given mesh id does not exist in the model. - /// - public Dictionary FetchComponentsForMesh(int meshId, bool abortOnTooManyCacheMisses) { - Dictionary output; - // Look for a hit in the cache, and return it if found. - bool foundInCache = componentsByMaterialForMeshId.TryGetValue(meshId, out output); - if (foundInCache) { - return output; - } else { - cacheMissesThisFrame++; - if (abortOnTooManyCacheMisses && cacheMissesThisFrame >= MAX_CACHE_MISSES_PER_FRAME) { - return null; + /// + /// Get the components, keyed by material ID, of a given mesh, fetching from the cache if possible, + /// else triangulating on-the-fly and then adding to the cache and returning. + /// If the mesh does not exist in the model, nothing will be added to the cache. + /// + /// The mesh for which to fetch the previews. + /// + /// A dictionary keyed by material ID, with values containing the vertices and triangles of the triangulated + /// mesh. + /// + public Dictionary FetchComponentsForMesh(MMesh mesh) + { + Dictionary output; + + // Look for a hit in the cache, and return it if found. + bool isCacheHit = componentsByMaterialForMeshId.TryGetValue(mesh.id, out output); + if (isCacheHit) + { + return output; + } + + // Else create the required information. + output = MeshHelper.MeshComponentsFromMMesh(mesh, /* useModelSpace */ true); + if (model.HasMesh(mesh.id)) + { + // If a mesh with this ID exists in the model, we add an entry to the cache. + componentsByMaterialForMeshId.Add(mesh.id, output); + } + return output; } - } - AssertOrThrow.True(model.HasMesh(meshId), - "Attempted to get components for a mesh that does not exist in the model"); + /// + /// Get the components, keyed by material ID, of a given mesh that is expected to exist in the model, + /// fetching from the cache if possible, else triangulating on-the-fly and then adding to the cache + /// and returning. + /// + /// The mesh id for which to fetch the previews. + /// + /// If true, and the mesh is not found in the cache, and there have been too many + /// cache misses already this frame, this method will return null. + /// + /// A dictionary keyed by material ID, with values containing the vertices and triangles of the triangulated + /// mesh. + /// + /// + /// If the given mesh id does not exist in the model. + /// + public Dictionary FetchComponentsForMesh(int meshId, bool abortOnTooManyCacheMisses) + { + Dictionary output; + // Look for a hit in the cache, and return it if found. + bool foundInCache = componentsByMaterialForMeshId.TryGetValue(meshId, out output); + if (foundInCache) + { + return output; + } + else + { + cacheMissesThisFrame++; + if (abortOnTooManyCacheMisses && cacheMissesThisFrame >= MAX_CACHE_MISSES_PER_FRAME) + { + return null; + } + } - // Else create the required information. - output = MeshHelper.MeshComponentsFromMMesh(model.GetMesh(meshId), /* useModelSpace */ true); - if (model.HasMesh(meshId)) { - // If a mesh with this ID exists in the model, we add an entry to the cache. - componentsByMaterialForMeshId.Add(meshId, output); - } - return output; - } - - /// - /// Get the components, keyed by material ID, of a given mesh that is expected to exist in the model, - /// fetching from the cache if possible, else triangulating on-the-fly and then adding to the cache - /// and returning. - /// - /// The mesh id for which to fetch the previews. - /// - /// If true, and the mesh is not found in the cache, and there have been too many - /// cache misses already this frame, this method will return null. - /// - /// A dictionary keyed by material ID, with values containing the vertices and triangles of the triangulated - /// mesh. - /// - /// - /// If the given mesh id does not exist in the model. - /// - public Dictionary FetchMeshSpaceComponentsForMesh(int meshId, bool abortOnTooManyCacheMisses) { - Dictionary output; - // Look for a hit in the cache, and return it if found. - bool foundInCache = meshSpaceComponentsByMaterialForMeshId.TryGetValue(meshId, out output); - if (foundInCache) { - return output; - } else { - cacheMissesThisFrame++; - if (abortOnTooManyCacheMisses && cacheMissesThisFrame >= MAX_CACHE_MISSES_PER_FRAME) { - return null; + AssertOrThrow.True(model.HasMesh(meshId), + "Attempted to get components for a mesh that does not exist in the model"); + + // Else create the required information. + output = MeshHelper.MeshComponentsFromMMesh(model.GetMesh(meshId), /* useModelSpace */ true); + if (model.HasMesh(meshId)) + { + // If a mesh with this ID exists in the model, we add an entry to the cache. + componentsByMaterialForMeshId.Add(meshId, output); + } + return output; } - } - AssertOrThrow.True(model.HasMesh(meshId), - "Attempted to get components for a mesh that does not exist in the model"); + /// + /// Get the components, keyed by material ID, of a given mesh that is expected to exist in the model, + /// fetching from the cache if possible, else triangulating on-the-fly and then adding to the cache + /// and returning. + /// + /// The mesh id for which to fetch the previews. + /// + /// If true, and the mesh is not found in the cache, and there have been too many + /// cache misses already this frame, this method will return null. + /// + /// A dictionary keyed by material ID, with values containing the vertices and triangles of the triangulated + /// mesh. + /// + /// + /// If the given mesh id does not exist in the model. + /// + public Dictionary FetchMeshSpaceComponentsForMesh(int meshId, bool abortOnTooManyCacheMisses) + { + Dictionary output; + // Look for a hit in the cache, and return it if found. + bool foundInCache = meshSpaceComponentsByMaterialForMeshId.TryGetValue(meshId, out output); + if (foundInCache) + { + return output; + } + else + { + cacheMissesThisFrame++; + if (abortOnTooManyCacheMisses && cacheMissesThisFrame >= MAX_CACHE_MISSES_PER_FRAME) + { + return null; + } + } - // Else create the required information. - output = MeshHelper.MeshComponentsFromMMesh(model.GetMesh(meshId), /* useModelSpace */ false); - if (model.HasMesh(meshId)) { - // If a mesh with this ID exists in the model, we add an entry to the cache. - meshSpaceComponentsByMaterialForMeshId.Add(meshId, output); - } - return output; - } + AssertOrThrow.True(model.HasMesh(meshId), + "Attempted to get components for a mesh that does not exist in the model"); - /// - /// Invalidates the preview templates for the given mesh ID. - /// - /// The ID of the mesh to invalidate. - public void InvalidatePreviews(int meshId) { - if (normalTemplateForMeshId.ContainsKey(meshId)) { - GameObject.DestroyImmediate(normalTemplateForMeshId[meshId]); - normalTemplateForMeshId.Remove(meshId); - } - foreach (Dictionary dict in highlightedTemplatesForMeshIdsByMaterial.Values) { - if (dict.ContainsKey(meshId)) { - GameObject.DestroyImmediate(dict[meshId]); - dict.Remove(meshId); + // Else create the required information. + output = MeshHelper.MeshComponentsFromMMesh(model.GetMesh(meshId), /* useModelSpace */ false); + if (model.HasMesh(meshId)) + { + // If a mesh with this ID exists in the model, we add an entry to the cache. + meshSpaceComponentsByMaterialForMeshId.Add(meshId, output); + } + return output; } - } - } - /// - /// Empties the entire cache, destroying any Preview GameObjects. - /// - public void Clear() { - foreach (int meshId in normalTemplateForMeshId.Keys) { - GameObject.DestroyImmediate(normalTemplateForMeshId[meshId]); - } - foreach (Dictionary dict in highlightedTemplatesForMeshIdsByMaterial.Values) { - foreach (GameObject preview in dict.Values) { - GameObject.DestroyImmediate(preview); + /// + /// Invalidates the preview templates for the given mesh ID. + /// + /// The ID of the mesh to invalidate. + public void InvalidatePreviews(int meshId) + { + if (normalTemplateForMeshId.ContainsKey(meshId)) + { + GameObject.DestroyImmediate(normalTemplateForMeshId[meshId]); + normalTemplateForMeshId.Remove(meshId); + } + foreach (Dictionary dict in highlightedTemplatesForMeshIdsByMaterial.Values) + { + if (dict.ContainsKey(meshId)) + { + GameObject.DestroyImmediate(dict[meshId]); + dict.Remove(meshId); + } + } + } + + /// + /// Empties the entire cache, destroying any Preview GameObjects. + /// + public void Clear() + { + foreach (int meshId in normalTemplateForMeshId.Keys) + { + GameObject.DestroyImmediate(normalTemplateForMeshId[meshId]); + } + foreach (Dictionary dict in highlightedTemplatesForMeshIdsByMaterial.Values) + { + foreach (GameObject preview in dict.Values) + { + GameObject.DestroyImmediate(preview); + } + } + normalTemplateForMeshId.Clear(); + highlightedTemplatesForMeshIdsByMaterial.Clear(); + componentsByMaterialForMeshId.Clear(); + meshSpaceComponentsByMaterialForMeshId.Clear(); } - } - normalTemplateForMeshId.Clear(); - highlightedTemplatesForMeshIdsByMaterial.Clear(); - componentsByMaterialForMeshId.Clear(); - meshSpaceComponentsByMaterialForMeshId.Clear(); } - } } diff --git a/Assets/Scripts/model/core/MeshUtil.cs b/Assets/Scripts/model/core/MeshUtil.cs index 26f2906c..2819fb71 100644 --- a/Assets/Scripts/model/core/MeshUtil.cs +++ b/Assets/Scripts/model/core/MeshUtil.cs @@ -20,332 +20,369 @@ using System.Linq; using System.Collections.ObjectModel; -namespace com.google.apps.peltzer.client.model.core { - /// - /// Utilities for modifing MMeshes. - /// - public class MeshUtil { +namespace com.google.apps.peltzer.client.model.core +{ /// - /// Maximum distance from a vertex to the face's plane for the vertex to be considered coplanar with the face. - /// If the distance between a face's vertex and the face's plane is more than this, the face is considered - /// to be non-coplanar. + /// Utilities for modifing MMeshes. /// - public const float MAX_COPLANAR_DISTANCE = 0.001f; - - /// - /// Split a face at a given vertex if needed. It is needed if the Face is not coplanar. - /// - /// The current operation on the mutated mesh. - /// The face to split if necessary. - /// The id of the moved vertex. - /// Whether a face was split. - public static bool SplitFaceIfNeeded(MMesh.GeometryOperation operation, Face face, int vertId) { - bool mutated = false; - - // Check if face is coplanar. - if (IsFaceCoplanar(operation, face)) { - // No need to split, just update the normals. - - Face replacementFace = Face.FaceWithPendingNormal(face.id, face.vertexIds, face.properties); - Vector3 newNormal = MeshMath.CalculateNormal(replacementFace, operation); - if (face.normal != newNormal) { - operation.ModifyFace(face.id, face.vertexIds, face.properties); + public class MeshUtil + { + /// + /// Maximum distance from a vertex to the face's plane for the vertex to be considered coplanar with the face. + /// If the distance between a face's vertex and the face's plane is more than this, the face is considered + /// to be non-coplanar. + /// + public const float MAX_COPLANAR_DISTANCE = 0.001f; + + /// + /// Split a face at a given vertex if needed. It is needed if the Face is not coplanar. + /// + /// The current operation on the mutated mesh. + /// The face to split if necessary. + /// The id of the moved vertex. + /// Whether a face was split. + public static bool SplitFaceIfNeeded(MMesh.GeometryOperation operation, Face face, int vertId) + { + bool mutated = false; + + // Check if face is coplanar. + if (IsFaceCoplanar(operation, face)) + { + // No need to split, just update the normals. + + Face replacementFace = Face.FaceWithPendingNormal(face.id, face.vertexIds, face.properties); + Vector3 newNormal = MeshMath.CalculateNormal(replacementFace, operation); + if (face.normal != newNormal) + { + operation.ModifyFace(face.id, face.vertexIds, face.properties); + } + return false; + } + + // Face is not coplanar. Split the face at the vertex that was moved. + SplitFaceAt(operation, face, vertId); + + return true; } - return false; - } - - // Face is not coplanar. Split the face at the vertex that was moved. - SplitFaceAt(operation, face, vertId); - - return true; - } - /// - /// Determines whether or not a face is coplanar, that is, if all the vertices on the face lie on the same plane. - /// - /// The current operation on the mutated mesh. - /// The face to check. - /// True iff the face is coplanar (all vertices are on the same plane). - public static bool IsFaceCoplanar(MMesh.GeometryOperation operation, Face face) { - return AreVerticesCoplanar(operation, face.vertexIds); - } - - /// - /// Determines whether or not the given vertices are all coplanar. Optionally, computes the unique plane - /// to which they belong. - /// - /// The mesh to which the vertices belong. - /// The ID of the vertices to check. - /// True iff all the given vertices are coplanar. - public static bool AreVerticesCoplanar(MMesh mesh, ReadOnlyCollection vertexIds) { - // Easy case: if there are 3 or fewer vertices, they are coplanar. - if (vertexIds.Count <= 3) return true; - - Plane facePlane; - int indexOfThird; - if (!CalculateCommonPlane(mesh, vertexIds, out facePlane, out indexOfThird)) { - return false; - } - - // Now we need to check the remaining vertices to see if they are on the same plane. - // We start at i == 2 because we know 0 and 1 are on the plane (as they were part of the definition). - for (int i = 2; i < vertexIds.Count; i++) { - // The third vertex that was used to define the plane is also coplanar by definition, so skip it for - // a mild performance gain. - if (i == indexOfThird) continue; - - Vector3 pos = mesh.VertexPositionInMeshCoords(vertexIds[i]); - - // To check if it's coplanar, we just calculate the distance from the vertex to the plane. - // If that's not close to 0, then the vertex is not on the face's plane. - if (Mathf.Abs(facePlane.GetDistanceToPoint(pos)) > MAX_COPLANAR_DISTANCE) { - return false; + /// + /// Determines whether or not a face is coplanar, that is, if all the vertices on the face lie on the same plane. + /// + /// The current operation on the mutated mesh. + /// The face to check. + /// True iff the face is coplanar (all vertices are on the same plane). + public static bool IsFaceCoplanar(MMesh.GeometryOperation operation, Face face) + { + return AreVerticesCoplanar(operation, face.vertexIds); } - } - // If we got here, we didn't find any vertices that were too far away from the face's plane, so we have - // determined that the face is coplanar. - return true; - } - - /// - /// Determines whether or not the given vertices are all coplanar in an ongoing GeometryOperation. - /// - /// The current operation on the mutated mesh. - /// The ID of the vertices to check. - - /// True iff all the given vertices are coplanar. - public static bool AreVerticesCoplanar(MMesh.GeometryOperation operation, ReadOnlyCollection vertexIds) { - // Easy case: if there are 3 or fewer vertices, they are coplanar. - if (vertexIds.Count <= 3) return true; - - Plane facePlane; - int indexOfThird; - if (!CalculateCommonPlane(operation, vertexIds, out facePlane, out indexOfThird)) { - return false; - } - - // Now we need to check the remaining vertices to see if they are on the same plane. - // We start at i == 2 because we know 0 and 1 are on the plane (as they were part of the definition). - for (int i = 2; i < vertexIds.Count; i++) { - // The third vertex that was used to define the plane is also coplanar by definition, so skip it for - // a mild performance gain. - if (i == indexOfThird) continue; - - Vector3 pos = operation.GetCurrentVertexPositionMeshSpace(vertexIds[i]); - - // To check if it's coplanar, we just calculate the distance from the vertex to the plane. - // If that's not close to 0, then the vertex is not on the face's plane. - if (Mathf.Abs(facePlane.GetDistanceToPoint(pos)) > MAX_COPLANAR_DISTANCE) { - return false; + /// + /// Determines whether or not the given vertices are all coplanar. Optionally, computes the unique plane + /// to which they belong. + /// + /// The mesh to which the vertices belong. + /// The ID of the vertices to check. + /// True iff all the given vertices are coplanar. + public static bool AreVerticesCoplanar(MMesh mesh, ReadOnlyCollection vertexIds) + { + // Easy case: if there are 3 or fewer vertices, they are coplanar. + if (vertexIds.Count <= 3) return true; + + Plane facePlane; + int indexOfThird; + if (!CalculateCommonPlane(mesh, vertexIds, out facePlane, out indexOfThird)) + { + return false; + } + + // Now we need to check the remaining vertices to see if they are on the same plane. + // We start at i == 2 because we know 0 and 1 are on the plane (as they were part of the definition). + for (int i = 2; i < vertexIds.Count; i++) + { + // The third vertex that was used to define the plane is also coplanar by definition, so skip it for + // a mild performance gain. + if (i == indexOfThird) continue; + + Vector3 pos = mesh.VertexPositionInMeshCoords(vertexIds[i]); + + // To check if it's coplanar, we just calculate the distance from the vertex to the plane. + // If that's not close to 0, then the vertex is not on the face's plane. + if (Mathf.Abs(facePlane.GetDistanceToPoint(pos)) > MAX_COPLANAR_DISTANCE) + { + return false; + } + } + + // If we got here, we didn't find any vertices that were too far away from the face's plane, so we have + // determined that the face is coplanar. + return true; } - } - // If we got here, we didn't find any vertices that were too far away from the face's plane, so we have - // determined that the face is coplanar. - return true; - } - - /// - /// Calculates the common plane of all the provided vertices. - /// If the vertices are not all coplanar, returns the (somewhat arbitrarily picked) plane that is defined - /// by the first two vertices and a third vertex that's not colinear with the first two. - /// - /// The mesh to which the vertices belong. - /// The vertices to process. - /// The common plane of the vertices. - /// The index of the third vertex used to define the plane (the first two - /// are vertices [0] and [1]). - /// True if the common plane could be calculated (in which case commonPlane and - /// indexOfThird are valid), or false if it could not (in which case commonPlane and indexOfThird - /// have undefined contents). - public static bool CalculateCommonPlane(MMesh mesh, ReadOnlyCollection vertexIds, - out Plane commonPlane, out int indexOfThird) { - commonPlane = new Plane(); - indexOfThird = -1; - - // We need at least 3 vertices to get a plane. - if (vertexIds.Count < 3) return false; - - // We need three non-colinear vertices to define the face's plane. - // We will use vertices 0, 1 and search for another one that's not colinear with them. - Vector3 first = mesh.VertexPositionInMeshCoords(vertexIds[0]); - Vector3 second = mesh.VertexPositionInMeshCoords(vertexIds[1]); - for (int i = 2; i < vertexIds.Count; i++) { - Vector3 third = mesh.VertexPositionInMeshCoords(vertexIds[i]); - if (!Math3d.AreColinear(first, second, third)) { - // Found it. Vertices 0, 1 and i are non-colinear so we will use them to define a plane. - commonPlane = new Plane(first, second, third); - indexOfThird = i; - return true; + /// + /// Determines whether or not the given vertices are all coplanar in an ongoing GeometryOperation. + /// + /// The current operation on the mutated mesh. + /// The ID of the vertices to check. + + /// True iff all the given vertices are coplanar. + public static bool AreVerticesCoplanar(MMesh.GeometryOperation operation, ReadOnlyCollection vertexIds) + { + // Easy case: if there are 3 or fewer vertices, they are coplanar. + if (vertexIds.Count <= 3) return true; + + Plane facePlane; + int indexOfThird; + if (!CalculateCommonPlane(operation, vertexIds, out facePlane, out indexOfThird)) + { + return false; + } + + // Now we need to check the remaining vertices to see if they are on the same plane. + // We start at i == 2 because we know 0 and 1 are on the plane (as they were part of the definition). + for (int i = 2; i < vertexIds.Count; i++) + { + // The third vertex that was used to define the plane is also coplanar by definition, so skip it for + // a mild performance gain. + if (i == indexOfThird) continue; + + Vector3 pos = operation.GetCurrentVertexPositionMeshSpace(vertexIds[i]); + + // To check if it's coplanar, we just calculate the distance from the vertex to the plane. + // If that's not close to 0, then the vertex is not on the face's plane. + if (Mathf.Abs(facePlane.GetDistanceToPoint(pos)) > MAX_COPLANAR_DISTANCE) + { + return false; + } + } + + // If we got here, we didn't find any vertices that were too far away from the face's plane, so we have + // determined that the face is coplanar. + return true; } - } - return false; - } - - /// - /// Calculates the common plane of all the provided vertices. - /// If the vertices are not all coplanar, returns the (somewhat arbitrarily picked) plane that is defined - /// by the first two vertices and a third vertex that's not colinear with the first two. - /// - /// The current operation on the mutated mesh. - /// The vertices to process. - /// The common plane of the vertices. - /// The index of the third vertex used to define the plane (the first two - /// are vertices [0] and [1]). - /// True if the common plane could be calculated (in which case commonPlane and - /// indexOfThird are valid), or false if it could not (in which case commonPlane and indexOfThird - /// have undefined contents). - public static bool CalculateCommonPlane(MMesh.GeometryOperation operation, ReadOnlyCollection vertexIds, - out Plane commonPlane, out int indexOfThird) { - commonPlane = new Plane(); - indexOfThird = -1; - - // We need at least 3 vertices to get a plane. - if (vertexIds.Count < 3) return false; - - // We need three non-colinear vertices to define the face's plane. - // We will use vertices 0, 1 and search for another one that's not colinear with them. - Vector3 first = operation.GetCurrentVertexPositionMeshSpace(vertexIds[0]); - Vector3 second = operation.GetCurrentVertexPositionMeshSpace(vertexIds[1]); - for (int i = 2; i < vertexIds.Count; i++) { - Vector3 third = operation.GetCurrentVertexPositionMeshSpace(vertexIds[i]); - if (!Math3d.AreColinear(first, second, third)) { - // Found it. Vertices 0, 1 and i are non-colinear so we will use them to define a plane. - commonPlane = new Plane(first, second, third); - indexOfThird = i; - return true; + + /// + /// Calculates the common plane of all the provided vertices. + /// If the vertices are not all coplanar, returns the (somewhat arbitrarily picked) plane that is defined + /// by the first two vertices and a third vertex that's not colinear with the first two. + /// + /// The mesh to which the vertices belong. + /// The vertices to process. + /// The common plane of the vertices. + /// The index of the third vertex used to define the plane (the first two + /// are vertices [0] and [1]). + /// True if the common plane could be calculated (in which case commonPlane and + /// indexOfThird are valid), or false if it could not (in which case commonPlane and indexOfThird + /// have undefined contents). + public static bool CalculateCommonPlane(MMesh mesh, ReadOnlyCollection vertexIds, + out Plane commonPlane, out int indexOfThird) + { + commonPlane = new Plane(); + indexOfThird = -1; + + // We need at least 3 vertices to get a plane. + if (vertexIds.Count < 3) return false; + + // We need three non-colinear vertices to define the face's plane. + // We will use vertices 0, 1 and search for another one that's not colinear with them. + Vector3 first = mesh.VertexPositionInMeshCoords(vertexIds[0]); + Vector3 second = mesh.VertexPositionInMeshCoords(vertexIds[1]); + for (int i = 2; i < vertexIds.Count; i++) + { + Vector3 third = mesh.VertexPositionInMeshCoords(vertexIds[i]); + if (!Math3d.AreColinear(first, second, third)) + { + // Found it. Vertices 0, 1 and i are non-colinear so we will use them to define a plane. + commonPlane = new Plane(first, second, third); + indexOfThird = i; + return true; + } + } + return false; } - } - return false; - } - /// - /// Split the given Face at the given vertex. Handles concanve and convex vertices. - /// If this is a convex vertex, the face will be split such that the vertex and its - /// neighbors form a new triangular face. If the vertex is concave, the face will be - /// triangulated. - /// - private static void SplitFaceAt(MMesh.GeometryOperation operation, Face face, int vertId) { - // First let's check if the vertex is convex. - int vertIndex = face.vertexIds.IndexOf(vertId); - AssertOrThrow.True(vertIndex >= 0, "Vertex not found in face."); - int prevIndex = (vertIndex - 1 + face.vertexIds.Count) % face.vertexIds.Count; - int nextIndex = (vertIndex + 1) % face.vertexIds.Count; - Vector3 vertexPos = operation.GetCurrentVertexPositionMeshSpace(vertId); - Vector3 prevPos = operation.GetCurrentVertexPositionMeshSpace(face.vertexIds[prevIndex]); - Vector3 nextPos = operation.GetCurrentVertexPositionMeshSpace(face.vertexIds[nextIndex]); - - Vector3 faceNormal = MeshMath.CalculateNormal(face, operation); - if (Math3d.IsConvex(vertexPos, prevPos, nextPos, faceNormal)) { - // Vertex is convex, so we can use the simple approach of just splitting out a new triangle with - // the vertex and its neighbors. - SplitFaceAtConvexVertex(operation, face, vertId); - } else { - // Vertex is concave. For now, for simplicity, just split the whole face into triangles. - SplitFaceIntoTriangles(operation, face); - } - } + /// + /// Calculates the common plane of all the provided vertices. + /// If the vertices are not all coplanar, returns the (somewhat arbitrarily picked) plane that is defined + /// by the first two vertices and a third vertex that's not colinear with the first two. + /// + /// The current operation on the mutated mesh. + /// The vertices to process. + /// The common plane of the vertices. + /// The index of the third vertex used to define the plane (the first two + /// are vertices [0] and [1]). + /// True if the common plane could be calculated (in which case commonPlane and + /// indexOfThird are valid), or false if it could not (in which case commonPlane and indexOfThird + /// have undefined contents). + public static bool CalculateCommonPlane(MMesh.GeometryOperation operation, ReadOnlyCollection vertexIds, + out Plane commonPlane, out int indexOfThird) + { + commonPlane = new Plane(); + indexOfThird = -1; + + // We need at least 3 vertices to get a plane. + if (vertexIds.Count < 3) return false; + + // We need three non-colinear vertices to define the face's plane. + // We will use vertices 0, 1 and search for another one that's not colinear with them. + Vector3 first = operation.GetCurrentVertexPositionMeshSpace(vertexIds[0]); + Vector3 second = operation.GetCurrentVertexPositionMeshSpace(vertexIds[1]); + for (int i = 2; i < vertexIds.Count; i++) + { + Vector3 third = operation.GetCurrentVertexPositionMeshSpace(vertexIds[i]); + if (!Math3d.AreColinear(first, second, third)) + { + // Found it. Vertices 0, 1 and i are non-colinear so we will use them to define a plane. + commonPlane = new Plane(first, second, third); + indexOfThird = i; + return true; + } + } + return false; + } - /// - /// Split the given Face at the given vertex. This assumes that the vertex is CONVEX. - /// To split the face, we remove the vertex from the original face - /// and create a new Face containing that vertex and the two neighboring ones. The - /// new face will have "flat" normals. - /// - private static void SplitFaceAtConvexVertex(MMesh.GeometryOperation operation, Face face, int vertId) { - List vertIds = new List(face.vertexIds); - - int indexOf = vertIds.FindIndex(x => x == vertId); - // Due to hole slicing, the same vertex that was moved may appear in multiple places but it - // should be in at least one. - AssertOrThrow.True(indexOf >= 0, "Internal error. Should have found matching vertex."); - - // Each appearance will need its own slice so just run the slicing code multiple times. Each - // iteration will remove one appearance and create the appropriate geometry. - do { - // Create a new face with exactly 3 vertices, including the one we are splitting at. - List newVertIds = new List(); - List newNormals = new List(); - for (int i = -1; i < 2; i++) { - newVertIds.Add(vertIds[(indexOf + i + vertIds.Count) % vertIds.Count]); + /// + /// Split the given Face at the given vertex. Handles concanve and convex vertices. + /// If this is a convex vertex, the face will be split such that the vertex and its + /// neighbors form a new triangular face. If the vertex is concave, the face will be + /// triangulated. + /// + private static void SplitFaceAt(MMesh.GeometryOperation operation, Face face, int vertId) + { + // First let's check if the vertex is convex. + int vertIndex = face.vertexIds.IndexOf(vertId); + AssertOrThrow.True(vertIndex >= 0, "Vertex not found in face."); + int prevIndex = (vertIndex - 1 + face.vertexIds.Count) % face.vertexIds.Count; + int nextIndex = (vertIndex + 1) % face.vertexIds.Count; + Vector3 vertexPos = operation.GetCurrentVertexPositionMeshSpace(vertId); + Vector3 prevPos = operation.GetCurrentVertexPositionMeshSpace(face.vertexIds[prevIndex]); + Vector3 nextPos = operation.GetCurrentVertexPositionMeshSpace(face.vertexIds[nextIndex]); + + Vector3 faceNormal = MeshMath.CalculateNormal(face, operation); + if (Math3d.IsConvex(vertexPos, prevPos, nextPos, faceNormal)) + { + // Vertex is convex, so we can use the simple approach of just splitting out a new triangle with + // the vertex and its neighbors. + SplitFaceAtConvexVertex(operation, face, vertId); + } + else + { + // Vertex is concave. For now, for simplicity, just split the whole face into triangles. + SplitFaceIntoTriangles(operation, face); + } } - Face newFace = operation.AddFace(newVertIds, face.properties); - - // Remove the vert from the existing face. - vertIds.RemoveAt(indexOf); - operation.ModifyFace(new Face(face.id, vertIds.AsReadOnly(), face.normal, face.properties)); - indexOf = vertIds.FindIndex(x => x == vertId); - } while (indexOf >= 0); - } + /// + /// Split the given Face at the given vertex. This assumes that the vertex is CONVEX. + /// To split the face, we remove the vertex from the original face + /// and create a new Face containing that vertex and the two neighboring ones. The + /// new face will have "flat" normals. + /// + private static void SplitFaceAtConvexVertex(MMesh.GeometryOperation operation, Face face, int vertId) + { + List vertIds = new List(face.vertexIds); + + int indexOf = vertIds.FindIndex(x => x == vertId); + // Due to hole slicing, the same vertex that was moved may appear in multiple places but it + // should be in at least one. + AssertOrThrow.True(indexOf >= 0, "Internal error. Should have found matching vertex."); + + // Each appearance will need its own slice so just run the slicing code multiple times. Each + // iteration will remove one appearance and create the appropriate geometry. + do + { + // Create a new face with exactly 3 vertices, including the one we are splitting at. + List newVertIds = new List(); + List newNormals = new List(); + for (int i = -1; i < 2; i++) + { + newVertIds.Add(vertIds[(indexOf + i + vertIds.Count) % vertIds.Count]); + } + + Face newFace = operation.AddFace(newVertIds, face.properties); + + // Remove the vert from the existing face. + vertIds.RemoveAt(indexOf); + operation.ModifyFace(new Face(face.id, vertIds.AsReadOnly(), face.normal, face.properties)); + indexOf = vertIds.FindIndex(x => x == vertId); + } while (indexOf >= 0); + } - /// - /// Splits the given face into its constituent triangles. - /// - /// The mesh to which the face belongs. - /// The face to split. - private static void SplitFaceIntoTriangles(MMesh.GeometryOperation operation, Face face) { - List triangles = face.GetTriangulation(operation); - operation.DeleteFace(face.id); - - for (int i = 0; i < triangles.Count; i++) { - Triangle tri = triangles[i]; - operation.AddFace(new List() {tri.vertId0, tri.vertId1, tri.vertId2}, face.properties); - } - } + /// + /// Splits the given face into its constituent triangles. + /// + /// The mesh to which the face belongs. + /// The face to split. + private static void SplitFaceIntoTriangles(MMesh.GeometryOperation operation, Face face) + { + List triangles = face.GetTriangulation(operation); + operation.DeleteFace(face.id); + + for (int i = 0; i < triangles.Count; i++) + { + Triangle tri = triangles[i]; + operation.AddFace(new List() { tri.vertId0, tri.vertId1, tri.vertId2 }, face.properties); + } + } - /// - /// Returns whether or not the face contains an edge connecting the two given vertices. - /// The edge can be in any order (1 - 2 or 2 - 1). - /// - /// The face to check. - /// The ID of the first vertex. - /// The ID of the second vertex. - /// True iff the given vertices define an edge of the given face. - public static bool FaceContainsEdge(Face face, int vertexId1, int vertexId2) { - for (int i = 0; i < face.vertexIds.Count; i++) { - int next = (i + 1 >= face.vertexIds.Count) ? 0 : i + 1; - if (vertexId1 == face.vertexIds[i] && vertexId2 == face.vertexIds[next]) return true; - if (vertexId2 == face.vertexIds[i] && vertexId1 == face.vertexIds[next]) return true; - } - return false; - } + /// + /// Returns whether or not the face contains an edge connecting the two given vertices. + /// The edge can be in any order (1 - 2 or 2 - 1). + /// + /// The face to check. + /// The ID of the first vertex. + /// The ID of the second vertex. + /// True iff the given vertices define an edge of the given face. + public static bool FaceContainsEdge(Face face, int vertexId1, int vertexId2) + { + for (int i = 0; i < face.vertexIds.Count; i++) + { + int next = (i + 1 >= face.vertexIds.Count) ? 0 : i + 1; + if (vertexId1 == face.vertexIds[i] && vertexId2 == face.vertexIds[next]) return true; + if (vertexId2 == face.vertexIds[i] && vertexId1 == face.vertexIds[next]) return true; + } + return false; + } - public static String Vector3ToString(Vector3 v3) { - return "<" + v3.x.ToString("0.00000000") + ", " + v3.y.ToString("0.00000000") + ", " + - v3.z.ToString("0.00000000") + ">"; - } + public static String Vector3ToString(Vector3 v3) + { + return "<" + v3.x.ToString("0.00000000") + ", " + v3.y.ToString("0.00000000") + ", " + + v3.z.ToString("0.00000000") + ">"; + } - public static void PrintBounds(Bounds bounds, int indent) { - String indentString = new String(' ', indent); - Debug.Log(indentString + "center: " + Vector3ToString(bounds.center)); - Debug.Log(indentString + "extents: " + Vector3ToString(bounds.extents)); - Debug.Log(indentString + "max: " + Vector3ToString(bounds.max)); - Debug.Log(indentString + "min: " + Vector3ToString(bounds.min)); - Debug.Log(indentString + "size: " + Vector3ToString(bounds.size)); - } + public static void PrintBounds(Bounds bounds, int indent) + { + String indentString = new String(' ', indent); + Debug.Log(indentString + "center: " + Vector3ToString(bounds.center)); + Debug.Log(indentString + "extents: " + Vector3ToString(bounds.extents)); + Debug.Log(indentString + "max: " + Vector3ToString(bounds.max)); + Debug.Log(indentString + "min: " + Vector3ToString(bounds.min)); + Debug.Log(indentString + "size: " + Vector3ToString(bounds.size)); + } - /// - /// Computes a dictionary from edge IDs to the list of face IDs that edge connects. - /// - public static Dictionary> ComputeEdgeKeysToFaceIdsMap(MMesh mesh) { - Dictionary> edgeKeysToFaceIds = new Dictionary>(); - foreach (Face face in mesh.GetFaces()) { - int previousVertexId = face.vertexIds[face.vertexIds.Count - 1]; - for (int i = 0; i < face.vertexIds.Count; i++) { - int currentVertexId = face.vertexIds[i]; - EdgeKey edgeKey = new EdgeKey(mesh.id, currentVertexId, previousVertexId); - List faceIds; - if (!edgeKeysToFaceIds.TryGetValue(edgeKey, out faceIds)) { - faceIds = new List(); - edgeKeysToFaceIds.Add(edgeKey, faceIds); - } - faceIds.Add(face.id); - previousVertexId = currentVertexId; + /// + /// Computes a dictionary from edge IDs to the list of face IDs that edge connects. + /// + public static Dictionary> ComputeEdgeKeysToFaceIdsMap(MMesh mesh) + { + Dictionary> edgeKeysToFaceIds = new Dictionary>(); + foreach (Face face in mesh.GetFaces()) + { + int previousVertexId = face.vertexIds[face.vertexIds.Count - 1]; + for (int i = 0; i < face.vertexIds.Count; i++) + { + int currentVertexId = face.vertexIds[i]; + EdgeKey edgeKey = new EdgeKey(mesh.id, currentVertexId, previousVertexId); + List faceIds; + if (!edgeKeysToFaceIds.TryGetValue(edgeKey, out faceIds)) + { + faceIds = new List(); + edgeKeysToFaceIds.Add(edgeKey, faceIds); + } + faceIds.Add(face.id); + previousVertexId = currentVertexId; + } + } + return edgeKeysToFaceIds; } - } - return edgeKeysToFaceIds; } - } } diff --git a/Assets/Scripts/model/core/MeshValidator.cs b/Assets/Scripts/model/core/MeshValidator.cs index 8db0a0f0..7fb1e9d6 100644 --- a/Assets/Scripts/model/core/MeshValidator.cs +++ b/Assets/Scripts/model/core/MeshValidator.cs @@ -21,198 +21,212 @@ using UnityEngine; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.core { - public class MeshValidator { - /// - /// By how much we scale the mesh for validation purposes. - /// This is for numerical stability and to avoid floating-point errors. - /// - private const float STABILIZATION_SCALE_FACTOR = 1000.0f; - - /// - /// Tolerance when checking for intersections between rays and triangles. This makes the triangles we use - /// in the test slightly bigger than they really are, to avoid degeneracy problems at the edges. - /// - private const float TRIANGLE_INTERSECTION_TOLERANCE = 1.005f; - - /// - /// Tests whether or not the given mesh is valid (heuristically). - /// - /// For the purposes of Poly, a mesh is defined to be valid if no back faces are exposed. In other words, a mesh - /// is valid if and only if there is no angle from which an outside observer could see an incorrectly wound - /// face (a back face). Note that self-intersecting meshes can still be valid! For example, the Freeform tool - /// can create self intersecting meshes that are valid, because even though they have self intersections, they - /// DO NOT expose any back faces. - /// - /// This algorithm implements that definition heuristically, checking each face to see if it's exposed. - /// It's not bullet-proof but the hope is that it's difficult for an average user to accidentally construct a - /// case where this algorithm doesn't work. - /// - /// In particular, the algorithm is designed in a way that a false positives are more likely than false negatives. - /// That is, if this algorithm says a mesh is INVALID, then it almost certainly is invalid. - /// - /// The mesh to test. - /// The vertices that were updated - /// True if and only if the mesh is valid (heuristically). - public static bool IsValidMesh(MMesh mesh, HashSet updatedVertIds) { - // Our algorithm is very brittle on tiny meshes because of floating point and numerical stability issues, - // so for the purposes of validation we work on a scaled-up copy that's more reliable to work with. - // This fixes some problems related to working with tiny meshes. - // The cost of this is making a MMesh copy per frame. - mesh = CloneScaled(mesh, STABILIZATION_SCALE_FACTOR); - - // For now we need to triangulate the whole mesh because the algorithm is hard-coded to triangular faces. - // The affected faces have to be tested against all other faces, so all faces need to be available as triangles. - List geometry = FaceTriangulator.TriangulateMesh(mesh); - - // Calculate the normals and vertex positions of each triangle. - List triangleInfo = CalculateTriangleInfo(mesh, geometry); - - // Test rays we will use during the main loop. - Ray[] testRays = new Ray[4]; - Vector3[] triangleVerts = new Vector3[3]; - - // The main idea of this algorithm is to check each face to see if it's "exposed", which makes the mesh invalid. - // Checking in a mathematically correct way is hard and doesn't work well in self-intersecting meshes, so our - // method is approximate. - // - // Intuitively, to check if a back-face is "exposed", you could imagine a wide-angle camera placed on the back - // of the face. Now imagine what you would see in the image. If the mesh is valid, all back faces are "inside" - // so you would only the inside of the mesh. If the face is exposed (visible from the outside) then in that image - // you would be able to see a hole from which you could see the sky. So to us a mesh is valid if no back faces - // can "see the sky". - // - // How do we check if a back face can see the sky? - // - // For each face, we calculate its anti-normal (negative of the normal), which is a vector that points INWARDS, - // opposite to the normal of the face. In a correct mesh, the anti-normal will point to the INSIDE of the mesh. - // So if we start at the center of the face and follow along the anti-normal, we should be inside the mesh. This - // means that if we continue, we should eventually EXIT the mesh (that is, intersect with the back side of a - // face). If we never exit the mesh, then it's because that back face is exposed (we can "see the sky"), - // which makes the mesh invalid. - // - // It's not sufficient to check just the center of the face. Technically we should check ALL points on the face - // and all possible rays to see if there's a ray that doesn't exit the mesh. Naturally checking all possible - // rays would be pretty expensive, so instead we just check the CENTER and the VERTICES. To increase coverage, the - // rays we use when checking the vertices point slightly away from the anti-normal, to allow us to catch a bigger - // field of view. - for (int i = 0; i < geometry.Count; i++) { - Triangle triangle = geometry[i]; - TriangleInfo thisTriangleInfo = triangleInfo[i]; - Vector3 antiNormal = -thisTriangleInfo.normal; - - // If the triangle wasn't modified, skip it. - if (!updatedVertIds.Contains(triangle.vertId0) && !updatedVertIds.Contains(triangle.vertId1) && - !updatedVertIds.Contains(triangle.vertId2)) continue; - - // We will now check if the back of this face can "see the sky". To do this, let's figure out our - // four "test rays": one from the center and one from each vertex. The rays go in the direction of the - // anti-normal. The rays from each vertex are slightly bent away from the center for bigger "wide angle" - // coverage. - triangleVerts[0] = thisTriangleInfo.v1; - triangleVerts[1] = thisTriangleInfo.v2; - triangleVerts[2] = thisTriangleInfo.v3; - - // Construct a test ray from (a point close to) each vertex. - for (int j = 0; j < 3; j++) { - testRays[j] = new Ray( - // Pick a point that's close to the vertex but still inside the face, to avoid degeneracy problems. - triangleVerts[j] * 0.95f + thisTriangleInfo.center * 0.05f, - // Bend the direction of the ray slightly away from the anti-normal for wider-angle coverage. - antiNormal + (triangleVerts[j]- thisTriangleInfo.center).normalized * 0.1f); +namespace com.google.apps.peltzer.client.model.core +{ + public class MeshValidator + { + /// + /// By how much we scale the mesh for validation purposes. + /// This is for numerical stability and to avoid floating-point errors. + /// + private const float STABILIZATION_SCALE_FACTOR = 1000.0f; + + /// + /// Tolerance when checking for intersections between rays and triangles. This makes the triangles we use + /// in the test slightly bigger than they really are, to avoid degeneracy problems at the edges. + /// + private const float TRIANGLE_INTERSECTION_TOLERANCE = 1.005f; + + /// + /// Tests whether or not the given mesh is valid (heuristically). + /// + /// For the purposes of Poly, a mesh is defined to be valid if no back faces are exposed. In other words, a mesh + /// is valid if and only if there is no angle from which an outside observer could see an incorrectly wound + /// face (a back face). Note that self-intersecting meshes can still be valid! For example, the Freeform tool + /// can create self intersecting meshes that are valid, because even though they have self intersections, they + /// DO NOT expose any back faces. + /// + /// This algorithm implements that definition heuristically, checking each face to see if it's exposed. + /// It's not bullet-proof but the hope is that it's difficult for an average user to accidentally construct a + /// case where this algorithm doesn't work. + /// + /// In particular, the algorithm is designed in a way that a false positives are more likely than false negatives. + /// That is, if this algorithm says a mesh is INVALID, then it almost certainly is invalid. + /// + /// The mesh to test. + /// The vertices that were updated + /// True if and only if the mesh is valid (heuristically). + public static bool IsValidMesh(MMesh mesh, HashSet updatedVertIds) + { + // Our algorithm is very brittle on tiny meshes because of floating point and numerical stability issues, + // so for the purposes of validation we work on a scaled-up copy that's more reliable to work with. + // This fixes some problems related to working with tiny meshes. + // The cost of this is making a MMesh copy per frame. + mesh = CloneScaled(mesh, STABILIZATION_SCALE_FACTOR); + + // For now we need to triangulate the whole mesh because the algorithm is hard-coded to triangular faces. + // The affected faces have to be tested against all other faces, so all faces need to be available as triangles. + List geometry = FaceTriangulator.TriangulateMesh(mesh); + + // Calculate the normals and vertex positions of each triangle. + List triangleInfo = CalculateTriangleInfo(mesh, geometry); + + // Test rays we will use during the main loop. + Ray[] testRays = new Ray[4]; + Vector3[] triangleVerts = new Vector3[3]; + + // The main idea of this algorithm is to check each face to see if it's "exposed", which makes the mesh invalid. + // Checking in a mathematically correct way is hard and doesn't work well in self-intersecting meshes, so our + // method is approximate. + // + // Intuitively, to check if a back-face is "exposed", you could imagine a wide-angle camera placed on the back + // of the face. Now imagine what you would see in the image. If the mesh is valid, all back faces are "inside" + // so you would only the inside of the mesh. If the face is exposed (visible from the outside) then in that image + // you would be able to see a hole from which you could see the sky. So to us a mesh is valid if no back faces + // can "see the sky". + // + // How do we check if a back face can see the sky? + // + // For each face, we calculate its anti-normal (negative of the normal), which is a vector that points INWARDS, + // opposite to the normal of the face. In a correct mesh, the anti-normal will point to the INSIDE of the mesh. + // So if we start at the center of the face and follow along the anti-normal, we should be inside the mesh. This + // means that if we continue, we should eventually EXIT the mesh (that is, intersect with the back side of a + // face). If we never exit the mesh, then it's because that back face is exposed (we can "see the sky"), + // which makes the mesh invalid. + // + // It's not sufficient to check just the center of the face. Technically we should check ALL points on the face + // and all possible rays to see if there's a ray that doesn't exit the mesh. Naturally checking all possible + // rays would be pretty expensive, so instead we just check the CENTER and the VERTICES. To increase coverage, the + // rays we use when checking the vertices point slightly away from the anti-normal, to allow us to catch a bigger + // field of view. + for (int i = 0; i < geometry.Count; i++) + { + Triangle triangle = geometry[i]; + TriangleInfo thisTriangleInfo = triangleInfo[i]; + Vector3 antiNormal = -thisTriangleInfo.normal; + + // If the triangle wasn't modified, skip it. + if (!updatedVertIds.Contains(triangle.vertId0) && !updatedVertIds.Contains(triangle.vertId1) && + !updatedVertIds.Contains(triangle.vertId2)) continue; + + // We will now check if the back of this face can "see the sky". To do this, let's figure out our + // four "test rays": one from the center and one from each vertex. The rays go in the direction of the + // anti-normal. The rays from each vertex are slightly bent away from the center for bigger "wide angle" + // coverage. + triangleVerts[0] = thisTriangleInfo.v1; + triangleVerts[1] = thisTriangleInfo.v2; + triangleVerts[2] = thisTriangleInfo.v3; + + // Construct a test ray from (a point close to) each vertex. + for (int j = 0; j < 3; j++) + { + testRays[j] = new Ray( + // Pick a point that's close to the vertex but still inside the face, to avoid degeneracy problems. + triangleVerts[j] * 0.95f + thisTriangleInfo.center * 0.05f, + // Bend the direction of the ray slightly away from the anti-normal for wider-angle coverage. + antiNormal + (triangleVerts[j] - thisTriangleInfo.center).normalized * 0.1f); + } + // The last test ray starts at the center and goes along the anti-normal. + testRays[3] = new Ray(thisTriangleInfo.center, antiNormal); + + // Check all the test rays. If one of them doesn't exit the mesh, it means this back face can see the sky, + // so the mesh is invalid. + if (!RaysExitMesh(mesh, geometry, triangleInfo, testRays, i)) return false; + } + // We found no reason to suspect the mesh is invalid. + return true; } - // The last test ray starts at the center and goes along the anti-normal. - testRays[3] = new Ray(thisTriangleInfo.center, antiNormal); - - // Check all the test rays. If one of them doesn't exit the mesh, it means this back face can see the sky, - // so the mesh is invalid. - if (!RaysExitMesh(mesh, geometry, triangleInfo, testRays, i)) return false; - } - // We found no reason to suspect the mesh is invalid. - return true; - } - private static bool RaysExitMesh(MMesh mesh, List geometry, List triangleInfo, - Ray[] rays, int triangleIndexToIgnore) { - for (int i = 0; i < rays.Length; i++) { - if (!RayExitsMesh(mesh, geometry, triangleInfo, rays[i], triangleIndexToIgnore)) return false; - } - return true; - } + private static bool RaysExitMesh(MMesh mesh, List geometry, List triangleInfo, + Ray[] rays, int triangleIndexToIgnore) + { + for (int i = 0; i < rays.Length; i++) + { + if (!RayExitsMesh(mesh, geometry, triangleInfo, rays[i], triangleIndexToIgnore)) return false; + } + return true; + } - private static bool RayExitsMesh(MMesh mesh, List geometry, List triangleInfo, - Ray ray, int triangleIndexToIgnore = -1) { - for (int i = 0; i < geometry.Count; i++) { - // Check if we should ignore this triangle. - if (i == triangleIndexToIgnore) continue; - - // Check if the ray intersects this triangle. - Triangle thisTriangle = geometry[i]; - TriangleInfo thisTriangleInfo = triangleInfo[i]; - - // Use the vertex positions adjusted for tolerance, so we don't miss edges between faces. - bool intersects = Math3d.RayIntersectsTriangle(ray, - thisTriangleInfo.v1WithTolerance, thisTriangleInfo.v2WithTolerance, - thisTriangleInfo.v3WithTolerance, thisTriangleInfo.normal); - if (!intersects) continue; - - // If the dot product is < 0, then the ray is going against the normal, so it's an entry. - // Otherwise, it's an exit. - if (Vector3.Dot(ray.direction, thisTriangleInfo.normal) > 0) return true; - } - // No exit detected. - return false; - } + private static bool RayExitsMesh(MMesh mesh, List geometry, List triangleInfo, + Ray ray, int triangleIndexToIgnore = -1) + { + for (int i = 0; i < geometry.Count; i++) + { + // Check if we should ignore this triangle. + if (i == triangleIndexToIgnore) continue; + + // Check if the ray intersects this triangle. + Triangle thisTriangle = geometry[i]; + TriangleInfo thisTriangleInfo = triangleInfo[i]; + + // Use the vertex positions adjusted for tolerance, so we don't miss edges between faces. + bool intersects = Math3d.RayIntersectsTriangle(ray, + thisTriangleInfo.v1WithTolerance, thisTriangleInfo.v2WithTolerance, + thisTriangleInfo.v3WithTolerance, thisTriangleInfo.normal); + if (!intersects) continue; + + // If the dot product is < 0, then the ray is going against the normal, so it's an entry. + // Otherwise, it's an exit. + if (Vector3.Dot(ray.direction, thisTriangleInfo.normal) > 0) return true; + } + // No exit detected. + return false; + } - private static List CalculateTriangleInfo(MMesh mesh, List geometry) { - List info = new List(geometry.Count); - for (int i = 0; i < geometry.Count; i++) { - TriangleInfo thisInfo = new TriangleInfo(); - Triangle triangle = geometry[i]; - Vector3 threeMinusOne = mesh.VertexPositionInMeshCoords(triangle.vertId2) - - mesh.VertexPositionInMeshCoords(triangle.vertId0); - Vector3 twoMinusOne = mesh.VertexPositionInMeshCoords(triangle.vertId1) - - mesh.VertexPositionInMeshCoords(triangle.vertId0); - thisInfo.normal = Vector3.Cross(twoMinusOne, threeMinusOne).normalized; - - // For the validity test, we want to be a little bit lenient with triangles because if we use the exact - // triangles, then we might miss a ray that that passes exactly through an edge between two faces (as we will - // consider the ray as not having intersected either face). - // Therefore, we need to use a slightly enlarged triangle so that there's some overlap at the - // edges and we don't miss any rays. - thisInfo.v1 = mesh.VertexPositionInMeshCoords(triangle.vertId0); - thisInfo.v2 = mesh.VertexPositionInMeshCoords(triangle.vertId1); - thisInfo.v3 = mesh.VertexPositionInMeshCoords(triangle.vertId2); - thisInfo.center = (thisInfo.v1 + thisInfo.v2 + thisInfo.v3) / 3.0f; - thisInfo.v1WithTolerance = thisInfo.center + (thisInfo.v1 - thisInfo.center) * TRIANGLE_INTERSECTION_TOLERANCE; - thisInfo.v2WithTolerance = thisInfo.center + (thisInfo.v2 - thisInfo.center) * TRIANGLE_INTERSECTION_TOLERANCE; - thisInfo.v3WithTolerance = thisInfo.center + (thisInfo.v3 - thisInfo.center) * TRIANGLE_INTERSECTION_TOLERANCE; - - info.Add(thisInfo); - } - AssertOrThrow.True(info.Count == geometry.Count, "# of triangle infos should be same as # of triangles."); - return info; - } + private static List CalculateTriangleInfo(MMesh mesh, List geometry) + { + List info = new List(geometry.Count); + for (int i = 0; i < geometry.Count; i++) + { + TriangleInfo thisInfo = new TriangleInfo(); + Triangle triangle = geometry[i]; + Vector3 threeMinusOne = mesh.VertexPositionInMeshCoords(triangle.vertId2) - + mesh.VertexPositionInMeshCoords(triangle.vertId0); + Vector3 twoMinusOne = mesh.VertexPositionInMeshCoords(triangle.vertId1) - + mesh.VertexPositionInMeshCoords(triangle.vertId0); + thisInfo.normal = Vector3.Cross(twoMinusOne, threeMinusOne).normalized; + + // For the validity test, we want to be a little bit lenient with triangles because if we use the exact + // triangles, then we might miss a ray that that passes exactly through an edge between two faces (as we will + // consider the ray as not having intersected either face). + // Therefore, we need to use a slightly enlarged triangle so that there's some overlap at the + // edges and we don't miss any rays. + thisInfo.v1 = mesh.VertexPositionInMeshCoords(triangle.vertId0); + thisInfo.v2 = mesh.VertexPositionInMeshCoords(triangle.vertId1); + thisInfo.v3 = mesh.VertexPositionInMeshCoords(triangle.vertId2); + thisInfo.center = (thisInfo.v1 + thisInfo.v2 + thisInfo.v3) / 3.0f; + thisInfo.v1WithTolerance = thisInfo.center + (thisInfo.v1 - thisInfo.center) * TRIANGLE_INTERSECTION_TOLERANCE; + thisInfo.v2WithTolerance = thisInfo.center + (thisInfo.v2 - thisInfo.center) * TRIANGLE_INTERSECTION_TOLERANCE; + thisInfo.v3WithTolerance = thisInfo.center + (thisInfo.v3 - thisInfo.center) * TRIANGLE_INTERSECTION_TOLERANCE; + + info.Add(thisInfo); + } + AssertOrThrow.True(info.Count == geometry.Count, "# of triangle infos should be same as # of triangles."); + return info; + } - private static MMesh CloneScaled(MMesh mesh, float factor) { - MMesh clone = mesh.Clone(); - MMesh.GeometryOperation cloneScaleOperation = clone.StartOperation(); - foreach (int id in clone.GetVertexIds()) { - cloneScaleOperation.ModifyVertexMeshSpace(id, clone.VertexPositionInMeshCoords(id) * factor); - } - cloneScaleOperation.Commit(); - return clone; - } + private static MMesh CloneScaled(MMesh mesh, float factor) + { + MMesh clone = mesh.Clone(); + MMesh.GeometryOperation cloneScaleOperation = clone.StartOperation(); + foreach (int id in clone.GetVertexIds()) + { + cloneScaleOperation.ModifyVertexMeshSpace(id, clone.VertexPositionInMeshCoords(id) * factor); + } + cloneScaleOperation.Commit(); + return clone; + } - private struct TriangleInfo { - // Original position of each vertex of the triangle (in mesh coords). - public Vector3 v1, v2, v3; - // Positions of each vertex in the slightly enlarged triangle (used for testing intersections). - public Vector3 v1WithTolerance, v2WithTolerance, v3WithTolerance; - // Position of center of triangle (in mesh coords). - public Vector3 center; - // Normal of the triangle. - public Vector3 normal; + private struct TriangleInfo + { + // Original position of each vertex of the triangle (in mesh coords). + public Vector3 v1, v2, v3; + // Positions of each vertex in the slightly enlarged triangle (used for testing intersections). + public Vector3 v1WithTolerance, v2WithTolerance, v3WithTolerance; + // Position of center of triangle (in mesh coords). + public Vector3 center; + // Normal of the triangle. + public Vector3 normal; + } } - } } diff --git a/Assets/Scripts/model/core/MeshWithMaterialRenderer.cs b/Assets/Scripts/model/core/MeshWithMaterialRenderer.cs index 6b543a14..2ac21955 100644 --- a/Assets/Scripts/model/core/MeshWithMaterialRenderer.cs +++ b/Assets/Scripts/model/core/MeshWithMaterialRenderer.cs @@ -17,241 +17,273 @@ using com.google.apps.peltzer.client.model.render; using UnityEngine; -namespace com.google.apps.peltzer.client.model.core { - - public class MeshWithMaterialRenderer : MonoBehaviour { - - // Layer settings, for cameras. - public static readonly int DEFAULT_LAYER = 10; // PolyAssets -- won't show up in thumbnails - public static readonly int NO_SHADOWS_LAYER = 9; // NoShadowsLayer -- won't cast a shadow - - public List meshes; - public WorldSpace worldSpace; - - protected bool overrideWithPreviewShader = false; - - public void UsePreviewShader(bool usePreview) { - overrideWithPreviewShader = usePreview; - } - - private SmoothMoves smoother; - - public bool IgnoreWorldScale; - public bool IgnoreWorldRotation; - /// - /// For entities that exist in world space, not model space. - /// - public bool UseGameObjectPosition; - - public float fade = 0.3f; - - // Which Layer this mesh will be drawn to. - public int Layer = DEFAULT_LAYER; - - public void Init(WorldSpace worldSpace) { - this.worldSpace = worldSpace; - smoother = new SmoothMoves(worldSpace, Vector3.zero, Quaternion.identity); - } - - // All rendering needs to be done at the very end of the frame after all the state changes to the models have been - // made. - void LateUpdate() { - // If along the GameObject's hierarchy a parent is set to inactive - // these will return null and logically we don't need to render. We cannot simply check activeSelf - // as it may still return true. - if (worldSpace == null || transform == null) return; - - // For positioning, we calculate our position using supplied model coordinates and WorldSpace, and then transform - // that by our parent transform. - // Usually we'll be in one of two cases: - // 1. Parent transform is identity, and we're positioning purely via worldSpace and supplied coords. - // 2. worldSpace and supplied coords are identity (or contain only scale) and we're positioned by the parent. - // - // It is also possible to use both in conjunction in order to position within a parent's frame of reference - // but at present that isn't used anywhere. - Vector3 pos = UseGameObjectPosition ? gameObject.transform.position : smoother.GetDisplayPositionInWorldSpace(); - Quaternion orientation = IgnoreWorldRotation ? gameObject.transform.rotation : smoother.GetDisplayOrientationInWorldSpace(); - Vector3 scale = IgnoreWorldScale ? smoother.GetScale() - : smoother.GetScale() * worldSpace.scale; - - Matrix4x4 matrix; - if (UseGameObjectPosition) { - matrix = Matrix4x4.TRS(pos, orientation, scale) ; - } else { - matrix = transform.localToWorldMatrix * Matrix4x4.TRS(pos, orientation, scale); - } - Render(matrix); - } - - public void Render(Matrix4x4 transformMatrix) { - foreach (MeshWithMaterial meshWithMaterial in meshes) { - Material renderMat = meshWithMaterial.materialAndColor.material; - if (overrideWithPreviewShader && meshWithMaterial.materialAndColor.matId != MaterialRegistry.GLASS_ID) { - renderMat = MaterialRegistry.GetPreviewOfMaterialById(meshWithMaterial.materialAndColor.matId).material; - renderMat.SetFloat("_MultiplicitiveAlpha", fade); - Graphics.DrawMesh(meshWithMaterial.mesh, transformMatrix, renderMat, Layer); +namespace com.google.apps.peltzer.client.model.core +{ + + public class MeshWithMaterialRenderer : MonoBehaviour + { + + // Layer settings, for cameras. + public static readonly int DEFAULT_LAYER = 10; // PolyAssets -- won't show up in thumbnails + public static readonly int NO_SHADOWS_LAYER = 9; // NoShadowsLayer -- won't cast a shadow + + public List meshes; + public WorldSpace worldSpace; + + protected bool overrideWithPreviewShader = false; + + public void UsePreviewShader(bool usePreview) + { + overrideWithPreviewShader = usePreview; } - else { - Graphics.DrawMesh(meshWithMaterial.mesh, transformMatrix, renderMat, Layer); - if (meshWithMaterial.materialAndColor.material2 != null) { - Graphics.DrawMesh(meshWithMaterial.mesh, transformMatrix, meshWithMaterial.materialAndColor.material2, Layer); - } + + private SmoothMoves smoother; + + public bool IgnoreWorldScale; + public bool IgnoreWorldRotation; + /// + /// For entities that exist in world space, not model space. + /// + public bool UseGameObjectPosition; + + public float fade = 0.3f; + + // Which Layer this mesh will be drawn to. + public int Layer = DEFAULT_LAYER; + + public void Init(WorldSpace worldSpace) + { + this.worldSpace = worldSpace; + smoother = new SmoothMoves(worldSpace, Vector3.zero, Quaternion.identity); } - } - } - - /// - /// Overrides the material of all meshes in this renderer with the given MaterialAndColor. - /// - public void OverrideWithNewMaterial(MaterialAndColor newMaterialAndColor) { - List newMeshes = new List(meshes.Count); - foreach (MeshWithMaterial mwm in meshes) { - // Override the vertex colours. - Color32[] colors = new Color32[mwm.mesh.vertexCount]; - for (int i = 0; i < colors.Length; i++) { - colors[i] = newMaterialAndColor.color; + // All rendering needs to be done at the very end of the frame after all the state changes to the models have been + // made. + void LateUpdate() + { + // If along the GameObject's hierarchy a parent is set to inactive + // these will return null and logically we don't need to render. We cannot simply check activeSelf + // as it may still return true. + if (worldSpace == null || transform == null) return; + + // For positioning, we calculate our position using supplied model coordinates and WorldSpace, and then transform + // that by our parent transform. + // Usually we'll be in one of two cases: + // 1. Parent transform is identity, and we're positioning purely via worldSpace and supplied coords. + // 2. worldSpace and supplied coords are identity (or contain only scale) and we're positioned by the parent. + // + // It is also possible to use both in conjunction in order to position within a parent's frame of reference + // but at present that isn't used anywhere. + Vector3 pos = UseGameObjectPosition ? gameObject.transform.position : smoother.GetDisplayPositionInWorldSpace(); + Quaternion orientation = IgnoreWorldRotation ? gameObject.transform.rotation : smoother.GetDisplayOrientationInWorldSpace(); + Vector3 scale = IgnoreWorldScale ? smoother.GetScale() + : smoother.GetScale() * worldSpace.scale; + + Matrix4x4 matrix; + if (UseGameObjectPosition) + { + matrix = Matrix4x4.TRS(pos, orientation, scale); + } + else + { + matrix = transform.localToWorldMatrix * Matrix4x4.TRS(pos, orientation, scale); + } + Render(matrix); } - mwm.mesh.colors32 = colors; - newMeshes.Add(new MeshWithMaterial(mwm.mesh, newMaterialAndColor)); - } - meshes = newMeshes; - } + public void Render(Matrix4x4 transformMatrix) + { + foreach (MeshWithMaterial meshWithMaterial in meshes) + { + Material renderMat = meshWithMaterial.materialAndColor.material; + if (overrideWithPreviewShader && meshWithMaterial.materialAndColor.matId != MaterialRegistry.GLASS_ID) + { + renderMat = MaterialRegistry.GetPreviewOfMaterialById(meshWithMaterial.materialAndColor.matId).material; + renderMat.SetFloat("_MultiplicitiveAlpha", fade); + Graphics.DrawMesh(meshWithMaterial.mesh, transformMatrix, renderMat, Layer); + } + else + { + Graphics.DrawMesh(meshWithMaterial.mesh, transformMatrix, renderMat, Layer); + if (meshWithMaterial.materialAndColor.material2 != null) + { + Graphics.DrawMesh(meshWithMaterial.mesh, transformMatrix, meshWithMaterial.materialAndColor.material2, Layer); + } + } - /// - /// As above, taking a material by ID. - /// - /// - public void OverrideWithNewMaterial(int newMaterialId) { - OverrideWithNewMaterial(MaterialRegistry.GetMaterialAndColorById(newMaterialId)); - } + } + } - - void Update() { - smoother.UpdateDisplayPosition(); - } - - public Vector3 positionModelSpace { - get { return smoother.GetPositionInModelSpace(); } - } + /// + /// Overrides the material of all meshes in this renderer with the given MaterialAndColor. + /// + public void OverrideWithNewMaterial(MaterialAndColor newMaterialAndColor) + { + List newMeshes = new List(meshes.Count); + foreach (MeshWithMaterial mwm in meshes) + { + // Override the vertex colours. + Color32[] colors = new Color32[mwm.mesh.vertexCount]; + for (int i = 0; i < colors.Length; i++) + { + colors[i] = newMaterialAndColor.color; + } + mwm.mesh.colors32 = colors; - /// - /// Returns the position in world space. Note that if this IS NOT affected by smoothing. Smoothing is a purely - /// visual effect and does not alter the object's position. - /// - /// - public Vector3 GetPositionInWorldSpace() { - return transform.localToWorldMatrix * smoother.GetPositionInWorldSpace(); - } + newMeshes.Add(new MeshWithMaterial(mwm.mesh, newMaterialAndColor)); + } + meshes = newMeshes; + } - public Vector3 GetPositionInModelSpace() { - return smoother.GetPositionInModelSpace(); - } + /// + /// As above, taking a material by ID. + /// + /// + public void OverrideWithNewMaterial(int newMaterialId) + { + OverrideWithNewMaterial(MaterialRegistry.GetMaterialAndColorById(newMaterialId)); + } - /// - /// Returns the orientation in model space. - /// - public Quaternion GetOrientationInModelSpace() { - return smoother.GetOrientationInModelSpace(); - } - /// - /// Sets the position in model space, optionally with smoothing. - /// - /// The new position in model space. - /// True if a smoothing effect is desired. Note that smoothing is a purely visual effect. - /// The actual position is instantaneously updated regardless of smoothing. - public void SetPositionModelSpace(Vector3 newPositionModelSpace, bool smooth = false) { - smoother.SetPositionModelSpace(newPositionModelSpace, smooth); - } + void Update() + { + smoother.UpdateDisplayPosition(); + } - /// - /// Sets the position in world space, optionally with smoothing. - /// - /// The new position in model space. - /// True if a smoothing effect is desired. Note that smoothing is a purely visual effect. - /// The actual position is instantaneously updated regardless of smoothing. - public void SetPositionWorldSpace(Vector3 newPositionWorldSpace, bool smooth = false) { - // Coords relative to parent node - Vector3 parentCoords = transform.worldToLocalMatrix* newPositionWorldSpace; - smoother.SetPositionWorldSpace(parentCoords, smooth); - } - - /// - /// Sets the position in model space, overriding smoothing with an override display position. This should primarily - /// be used when an external tool is handling smoothing (smoothing a parent rotation, for example) where lerping - /// position would result in an incorrect display position. - /// Positions will not be linearly interpolated until SetPositionModelSpace is called again. - /// - /// The new position in model space. - /// The override position to display the mesh at. - public void SetPositionWithDisplayOverrideModelSpace(Vector3 newPositionModelSpace, - Vector3 newDisplayPositionModelSpace) { - smoother.SetPositionWithDisplayOverrideModelSpace(newPositionModelSpace, newDisplayPositionModelSpace); - } + public Vector3 positionModelSpace + { + get { return smoother.GetPositionInModelSpace(); } + } - /// - /// Sets the orientation in model space, optionally with smoothing. - /// - /// The new orientation in model space. - /// True if a smoothing effect is desired. Note that smoothing is a purely visual effect. - /// The actual orientation is instantaneously updated regardless of smoothing. - public void SetOrientationModelSpace(Quaternion newOrientationModelSpace, bool smooth = false) { - smoother.SetOrientationModelSpace(newOrientationModelSpace, smooth); - } + /// + /// Returns the position in world space. Note that if this IS NOT affected by smoothing. Smoothing is a purely + /// visual effect and does not alter the object's position. + /// + /// + public Vector3 GetPositionInWorldSpace() + { + return transform.localToWorldMatrix * smoother.GetPositionInWorldSpace(); + } - /// - /// Sets the orientation in model space, with a display override (for when a tool is managing its own smoothing, - /// ie, when the smoothing is being done on a parent transform). - /// - /// The new orientation in model space. - /// The orientation to display. - /// Whether to smooth transitions to and from the display orientation. - /// This option is here primarily to smooth a transition into an override mode. - public void SetOrientationWithDisplayOverrideModelSpace(Quaternion newOrientationModelSpace, - Quaternion newDisplayOrientationModelSpace, bool smooth) { - smoother.SetOrientationWithDisplayOverrideModelSpace(newOrientationModelSpace, newDisplayOrientationModelSpace, - smooth); - } + public Vector3 GetPositionInModelSpace() + { + return smoother.GetPositionInModelSpace(); + } - /// - /// Resets the local transform to identity. - /// - public void ResetTransform() { - SetOrientationModelSpace(Quaternion.identity); - SetPositionModelSpace(Vector3.zero); - } + /// + /// Returns the orientation in model space. + /// + public Quaternion GetOrientationInModelSpace() + { + return smoother.GetOrientationInModelSpace(); + } - /// - /// Animates this object's displayed position from the given position to the current one. - /// - /// Old position, in the model space. - public void AnimatePositionFrom(Vector3 oldPosModelSpace) { - smoother.AnimatePositionFrom(oldPosModelSpace); - } + /// + /// Sets the position in model space, optionally with smoothing. + /// + /// The new position in model space. + /// True if a smoothing effect is desired. Note that smoothing is a purely visual effect. + /// The actual position is instantaneously updated regardless of smoothing. + public void SetPositionModelSpace(Vector3 newPositionModelSpace, bool smooth = false) + { + smoother.SetPositionModelSpace(newPositionModelSpace, smooth); + } - /// - /// Animates this object's displayed scale from the given scale to the default scale (1.0f). - /// - /// Old scale factor. - public void AnimateScaleFrom(float fromScale) { - smoother.AnimateScaleFrom(fromScale); - } + /// + /// Sets the position in world space, optionally with smoothing. + /// + /// The new position in model space. + /// True if a smoothing effect is desired. Note that smoothing is a purely visual effect. + /// The actual position is instantaneously updated regardless of smoothing. + public void SetPositionWorldSpace(Vector3 newPositionWorldSpace, bool smooth = false) + { + // Coords relative to parent node + Vector3 parentCoords = transform.worldToLocalMatrix * newPositionWorldSpace; + smoother.SetPositionWorldSpace(parentCoords, smooth); + } - /// - /// Sets this MeshWithMaterialRenderer up as a copy of another MeshWithMaterialRenderer. - /// - /// The other MeshWithMaterialRenderer - public void SetupAsCopyOf(MeshWithMaterialRenderer other) { - meshes = new List(other.meshes); - worldSpace = other.worldSpace; - smoother = new SmoothMoves(other.smoother); - } + /// + /// Sets the position in model space, overriding smoothing with an override display position. This should primarily + /// be used when an external tool is handling smoothing (smoothing a parent rotation, for example) where lerping + /// position would result in an incorrect display position. + /// Positions will not be linearly interpolated until SetPositionModelSpace is called again. + /// + /// The new position in model space. + /// The override position to display the mesh at. + public void SetPositionWithDisplayOverrideModelSpace(Vector3 newPositionModelSpace, + Vector3 newDisplayPositionModelSpace) + { + smoother.SetPositionWithDisplayOverrideModelSpace(newPositionModelSpace, newDisplayPositionModelSpace); + } + + /// + /// Sets the orientation in model space, optionally with smoothing. + /// + /// The new orientation in model space. + /// True if a smoothing effect is desired. Note that smoothing is a purely visual effect. + /// The actual orientation is instantaneously updated regardless of smoothing. + public void SetOrientationModelSpace(Quaternion newOrientationModelSpace, bool smooth = false) + { + smoother.SetOrientationModelSpace(newOrientationModelSpace, smooth); + } + + /// + /// Sets the orientation in model space, with a display override (for when a tool is managing its own smoothing, + /// ie, when the smoothing is being done on a parent transform). + /// + /// The new orientation in model space. + /// The orientation to display. + /// Whether to smooth transitions to and from the display orientation. + /// This option is here primarily to smooth a transition into an override mode. + public void SetOrientationWithDisplayOverrideModelSpace(Quaternion newOrientationModelSpace, + Quaternion newDisplayOrientationModelSpace, bool smooth) + { + smoother.SetOrientationWithDisplayOverrideModelSpace(newOrientationModelSpace, newDisplayOrientationModelSpace, + smooth); + } + + /// + /// Resets the local transform to identity. + /// + public void ResetTransform() + { + SetOrientationModelSpace(Quaternion.identity); + SetPositionModelSpace(Vector3.zero); + } + + /// + /// Animates this object's displayed position from the given position to the current one. + /// + /// Old position, in the model space. + public void AnimatePositionFrom(Vector3 oldPosModelSpace) + { + smoother.AnimatePositionFrom(oldPosModelSpace); + } + + /// + /// Animates this object's displayed scale from the given scale to the default scale (1.0f). + /// + /// Old scale factor. + public void AnimateScaleFrom(float fromScale) + { + smoother.AnimateScaleFrom(fromScale); + } + + /// + /// Sets this MeshWithMaterialRenderer up as a copy of another MeshWithMaterialRenderer. + /// + /// The other MeshWithMaterialRenderer + public void SetupAsCopyOf(MeshWithMaterialRenderer other) + { + meshes = new List(other.meshes); + worldSpace = other.worldSpace; + smoother = new SmoothMoves(other.smoother); + } + + public Vector3 GetCurrentAnimatedScale() + { + return smoother.GetScale(); + } - public Vector3 GetCurrentAnimatedScale() { - return smoother.GetScale(); } - - } } \ No newline at end of file diff --git a/Assets/Scripts/model/core/Model.cs b/Assets/Scripts/model/core/Model.cs index eeb0ef4d..50741ff7 100644 --- a/Assets/Scripts/model/core/Model.cs +++ b/Assets/Scripts/model/core/Model.cs @@ -24,1003 +24,1148 @@ using System.Text; using com.google.apps.peltzer.client.model.export; -namespace com.google.apps.peltzer.client.model.core { - /// - /// The Poly model. This is the top-level class that represents the whole "scene" that the user is currently - /// editing. A Poly model is essentially a collection of MMeshes. - /// - /// All meshes in the model are represented in Model Space coordinates. When displaying, these are converted to - /// world space (the Unity coordinate system) via the WorldSpace class, which allows the user to pan/rotate/zoom - /// the model. - /// - /// User-driven mutations of the model are represented through Command objects, which represent individual - /// operations that change the model (adding meshes, deleting meshes, etc). - /// - /// Note that, as a general principle, each MMesh should be immutable once added to the Model. Modifying an MMesh - /// is represented by replacing the original mesh with a new MMesh with the same ID. - /// - public class Model : IMeshRenderOwner, IMeshRenderOwnerOwner { - - // Maximum size of the undo stack - when we hit this size we discard the older half of the stack. - private static int undoStackMaxSize = 80; - - // Model change events. - public event Action OnMeshAdded; - public event Action OnMeshChanged; - public event Action OnMeshDeleted; - public event Action OnUndo; - public event Action OnRedo; - public event Action OnCommandApplied; - - /// - /// Delegate that approves or rejects a proposed command before it gets applied to the model. - /// - /// The command to validate. - /// True if the command should be accepted and applied, false if it should be rejected. - public delegate bool CommandValidator(Command command); - - /// - /// Command validators. If present, they have the prerrogative to approve or reject each Command before - /// it gets applied to the model. This is used, for example, for the tutorial where we want to carefully - /// validate each thing the user does. - /// - public event CommandValidator OnValidateCommand; - - private Bounds bounds; - private readonly Dictionary meshById = - new Dictionary(); - private Dictionary renderOwners = new Dictionary(); - // Tracks previous render owner for any mesh currently owned by Model. Primarily for debugging. - private Dictionary previousRenderOwners = new Dictionary(); - - private Command currentCommand; - private readonly List allCommands = new List(); - - private readonly Stack undoStack = new Stack(); - private readonly Stack redoStack = new Stack(); - private readonly ReMesher remesher = new ReMesher(); - private readonly HashSet hiddenMeshes = new HashSet(); - private readonly System.Random random = new System.Random(); - - public MeshRepresentationCache meshRepresentationCache { private set; get; } - - /// - /// Maps group ID to the list of meshes that belong to that group. - /// The "null group" (GROUP_NONE) is not included in this list. - /// - private readonly Dictionary> groupById = new Dictionary>(); - - public Dictionary> GetAllGroups() { - return groupById; - } - - /// - /// Meshes which are scheduled for deletion and should not be shown anywhere, regardless of what their - /// 'preview' says. This is necessary as the 'delete' tool will batch its commands, rather than deleting - /// as meshes are touched, but we wish for users to believe the meshes are deleted immediately. - /// - private HashSet meshesMarkedForDeletion = new HashSet(); - - // Whether we are currently allowing changes to the model. This is a bit of a hack to allow us to do - // read-only operations on the background thread. - // NOTE: This variable should only be changed on the main thread. - public bool writeable { get; set; } - - // We batch all undo commands within a certain short timeframe. - private const float BATCH_FREQUENCY_SECONDS = 0.5f; - private float undoBatchStartTime; - private bool lastCommandWasNewBatch = false; - - /// - /// Create a default, empty Model, with given bounds. - /// - /// The bounds of the model's space. - public Model(Bounds bounds) { - this.bounds = bounds; - - // Start as writeable. - writeable = true; - - meshRepresentationCache = PeltzerMain.Instance.gameObject.AddComponent(); - meshRepresentationCache.Setup(this, PeltzerMain.Instance.worldSpace); - } - - /// - /// Clears the model of all its meshes and resets bounds to default value - /// - public void Clear(WorldSpace worldspace) { - bounds = worldspace.bounds; - remesher.Clear(); - meshById.Clear(); - undoStack.Clear(); - redoStack.Clear(); - undoBatchStartTime = 0.0f; - hiddenMeshes.Clear(); - meshRepresentationCache.Clear(); - } - - /// - /// Render the model to the scene. - /// - public void Render() { - remesher.Render(this); - } - - public ReMesher GetReMesher() { - return remesher; - } - +namespace com.google.apps.peltzer.client.model.core +{ /// - /// Deals with the batching of commands: all commands performed within BATCH_FREQUENCY_SECONDS of a command - /// will be batched together into a single command for 'undo' purposes. - /// This helps with tools that can create tens of commands per second, and also when a user is just spamming - /// the trigger button in any mode. + /// The Poly model. This is the top-level class that represents the whole "scene" that the user is currently + /// editing. A Poly model is essentially a collection of MMeshes. + /// + /// All meshes in the model are represented in Model Space coordinates. When displaying, these are converted to + /// world space (the Unity coordinate system) via the WorldSpace class, which allows the user to pan/rotate/zoom + /// the model. + /// + /// User-driven mutations of the model are represented through Command objects, which represent individual + /// operations that change the model (adding meshes, deleting meshes, etc). + /// + /// Note that, as a general principle, each MMesh should be immutable once added to the Model. Modifying an MMesh + /// is represented by replacing the original mesh with a new MMesh with the same ID. /// - /// - /// A command which will be added to allCommands and the undo stack (potentially batched with - /// other recent commands) - /// - private void AddAndMaybeBatchCommands(Command command) { - LimitUndoStack(); - - Command undoCommand = command.GetUndoCommand(this); - - // Check if we're still in a batch. - if (Time.time - undoBatchStartTime <= BATCH_FREQUENCY_SECONDS) { - if (lastCommandWasNewBatch) { - // Convert the last command in allCommands to a composite command, and add the new command - // to it. - List compositeCommandList = new List(); - compositeCommandList.Add(currentCommand); - compositeCommandList.Add(command); - CompositeCommand bundledCommand = new CompositeCommand(compositeCommandList); - currentCommand = bundledCommand; - - // Convert the top undo command in the undoStack to a composite command, and add the new - // undo command to it. - Command lastUndoCommand = undoStack.Pop(); - List undoCommandList = new List(); - // Need to reverse the order for undo. - undoCommandList.Insert(0, lastUndoCommand); - undoCommandList.Insert(0, undoCommand); - CompositeCommand bundledUndoCommand = new CompositeCommand(undoCommandList); - undoStack.Push(bundledUndoCommand); - - lastCommandWasNewBatch = false; - } else { - // Replace the active compositeCommand with a version that includes the new command. - CompositeCommand baseCommand = (CompositeCommand)currentCommand; - List forwardCommandList = baseCommand.GetCommands(); - forwardCommandList.Add(command); - currentCommand = new CompositeCommand(forwardCommandList); - - // Replace the active undo compositeCommand with a version that includes the new undo command. - List undoCommandList = ((CompositeCommand)undoStack.Pop()).GetCommands(); - undoCommandList.Insert(0, undoCommand); - CompositeCommand bundledUndoCommand = new CompositeCommand(undoCommandList); - undoStack.Push(bundledUndoCommand); + public class Model : IMeshRenderOwner, IMeshRenderOwnerOwner + { + + // Maximum size of the undo stack - when we hit this size we discard the older half of the stack. + private static int undoStackMaxSize = 80; + + // Model change events. + public event Action OnMeshAdded; + public event Action OnMeshChanged; + public event Action OnMeshDeleted; + public event Action OnUndo; + public event Action OnRedo; + public event Action OnCommandApplied; + + /// + /// Delegate that approves or rejects a proposed command before it gets applied to the model. + /// + /// The command to validate. + /// True if the command should be accepted and applied, false if it should be rejected. + public delegate bool CommandValidator(Command command); + + /// + /// Command validators. If present, they have the prerrogative to approve or reject each Command before + /// it gets applied to the model. This is used, for example, for the tutorial where we want to carefully + /// validate each thing the user does. + /// + public event CommandValidator OnValidateCommand; + + private Bounds bounds; + private readonly Dictionary meshById = + new Dictionary(); + private Dictionary renderOwners = new Dictionary(); + // Tracks previous render owner for any mesh currently owned by Model. Primarily for debugging. + private Dictionary previousRenderOwners = new Dictionary(); + + private Command currentCommand; + private readonly List allCommands = new List(); + + private readonly Stack undoStack = new Stack(); + private readonly Stack redoStack = new Stack(); + private readonly ReMesher remesher = new ReMesher(); + private readonly HashSet hiddenMeshes = new HashSet(); + private readonly System.Random random = new System.Random(); + + public MeshRepresentationCache meshRepresentationCache { private set; get; } + + /// + /// Maps group ID to the list of meshes that belong to that group. + /// The "null group" (GROUP_NONE) is not included in this list. + /// + private readonly Dictionary> groupById = new Dictionary>(); + + public Dictionary> GetAllGroups() + { + return groupById; } - } else { - lastCommandWasNewBatch = true; - undoBatchStartTime = Time.time; - undoStack.Push(undoCommand); - currentCommand = command; - } - } - // Checks if the undo stack is about to exceed the maximum size, and discard the older half if it is. - private void LimitUndoStack() { - if (undoStack.Count > undoStackMaxSize - 1) { - List reducedCommandList = new List(undoStackMaxSize / 2); - for (int i = 0; i < undoStackMaxSize / 2; i++) { - reducedCommandList.Add(undoStack.Pop()); + /// + /// Meshes which are scheduled for deletion and should not be shown anywhere, regardless of what their + /// 'preview' says. This is necessary as the 'delete' tool will batch its commands, rather than deleting + /// as meshes are touched, but we wish for users to believe the meshes are deleted immediately. + /// + private HashSet meshesMarkedForDeletion = new HashSet(); + + // Whether we are currently allowing changes to the model. This is a bit of a hack to allow us to do + // read-only operations on the background thread. + // NOTE: This variable should only be changed on the main thread. + public bool writeable { get; set; } + + // We batch all undo commands within a certain short timeframe. + private const float BATCH_FREQUENCY_SECONDS = 0.5f; + private float undoBatchStartTime; + private bool lastCommandWasNewBatch = false; + + /// + /// Create a default, empty Model, with given bounds. + /// + /// The bounds of the model's space. + public Model(Bounds bounds) + { + this.bounds = bounds; + + // Start as writeable. + writeable = true; + + meshRepresentationCache = PeltzerMain.Instance.gameObject.AddComponent(); + meshRepresentationCache.Setup(this, PeltzerMain.Instance.worldSpace); } - undoStack.Clear(); - for (int i = reducedCommandList.Count - 1; i >= 0; i--) { - undoStack.Push(reducedCommandList[i]); + /// + /// Clears the model of all its meshes and resets bounds to default value + /// + public void Clear(WorldSpace worldspace) + { + bounds = worldspace.bounds; + remesher.Clear(); + meshById.Clear(); + undoStack.Clear(); + redoStack.Clear(); + undoBatchStartTime = 0.0f; + hiddenMeshes.Clear(); + meshRepresentationCache.Clear(); } - } - } - - // Sets the maximum size of the undo stack. - public static void SetMaxUndoStackSize(int maxSize) { - undoStackMaxSize = maxSize; - } - /// - /// Apply a Command to the model. - /// - /// - public void ApplyCommand(Command command) { - AssertOrThrow.True(writeable, "Model is not writable."); - - if (OnValidateCommand != null) { - // Check with all registered command validators. It only takes one of them to veto the command. - foreach (Delegate del in OnValidateCommand.GetInvocationList()) { - CommandValidator validator = (CommandValidator)del; - if (!validator.Invoke(command)) { - // Validator rejected the command. - return; - } + /// + /// Render the model to the scene. + /// + public void Render() + { + remesher.Render(this); } - } - - redoStack.Clear(); // Once we apply a command, clear all redos - AddAndMaybeBatchCommands(command); - command.ApplyToModel(this); - if (OnCommandApplied != null) { - OnCommandApplied(command); - } - } - - /// - /// Whether an 'undo' can be applied at this time. - /// - /// - public bool CanUndo() { - return undoStack.Count > 0; - } - - /// - /// Undo the last command applied to the model -- if there is one. - /// - public bool Undo() { - AssertOrThrow.True(writeable, "Model is not writable."); - if (undoStack.Count > 0) { - // Abort all undo batching. - undoBatchStartTime = 0.0f; - - Command command = undoStack.Pop(); - redoStack.Push(command.GetUndoCommand(this)); - command.ApplyToModel(this); - currentCommand = command; - - if (OnUndo != null) { - OnUndo(command); + public ReMesher GetReMesher() + { + return remesher; } - return true; - } - return false; - } - /// - /// Redo the previously undone command, if possible. - /// - public bool Redo() { - AssertOrThrow.True(writeable, "Model is not writable."); - - if (redoStack.Count > 0) { - // Abort all undo batching. - undoBatchStartTime = 0.0f; - - Command command = redoStack.Pop(); - undoStack.Push(command.GetUndoCommand(this)); - command.ApplyToModel(this); - currentCommand = command; - - if (OnRedo != null) { - OnRedo(command); + /// + /// Deals with the batching of commands: all commands performed within BATCH_FREQUENCY_SECONDS of a command + /// will be batched together into a single command for 'undo' purposes. + /// This helps with tools that can create tens of commands per second, and also when a user is just spamming + /// the trigger button in any mode. + /// + /// + /// A command which will be added to allCommands and the undo stack (potentially batched with + /// other recent commands) + /// + private void AddAndMaybeBatchCommands(Command command) + { + LimitUndoStack(); + + Command undoCommand = command.GetUndoCommand(this); + + // Check if we're still in a batch. + if (Time.time - undoBatchStartTime <= BATCH_FREQUENCY_SECONDS) + { + if (lastCommandWasNewBatch) + { + // Convert the last command in allCommands to a composite command, and add the new command + // to it. + List compositeCommandList = new List(); + compositeCommandList.Add(currentCommand); + compositeCommandList.Add(command); + CompositeCommand bundledCommand = new CompositeCommand(compositeCommandList); + currentCommand = bundledCommand; + + // Convert the top undo command in the undoStack to a composite command, and add the new + // undo command to it. + Command lastUndoCommand = undoStack.Pop(); + List undoCommandList = new List(); + // Need to reverse the order for undo. + undoCommandList.Insert(0, lastUndoCommand); + undoCommandList.Insert(0, undoCommand); + CompositeCommand bundledUndoCommand = new CompositeCommand(undoCommandList); + undoStack.Push(bundledUndoCommand); + + lastCommandWasNewBatch = false; + } + else + { + // Replace the active compositeCommand with a version that includes the new command. + CompositeCommand baseCommand = (CompositeCommand)currentCommand; + List forwardCommandList = baseCommand.GetCommands(); + forwardCommandList.Add(command); + currentCommand = new CompositeCommand(forwardCommandList); + + // Replace the active undo compositeCommand with a version that includes the new undo command. + List undoCommandList = ((CompositeCommand)undoStack.Pop()).GetCommands(); + undoCommandList.Insert(0, undoCommand); + CompositeCommand bundledUndoCommand = new CompositeCommand(undoCommandList); + undoStack.Push(bundledUndoCommand); + } + } + else + { + lastCommandWasNewBatch = true; + undoBatchStartTime = Time.time; + undoStack.Push(undoCommand); + currentCommand = command; + } } - return true; - } - return false; - } - /// - /// Whether a given mesh can be added to the model, specifically whether it would fit within the model's bounds. - /// - /// The Mesh. - /// True if the mesh can be added. - public bool CanAddMesh(MMesh mesh) { - return Math3d.ContainsBounds(bounds, mesh.bounds); - } - - /// - /// Whether a given mesh can be moved by specific dimensions and remain within the model's bounds. - /// - /// The Mesh. - /// The positional move delta. - /// The rotational move delta. - /// True if the mesh can be moved as specified. - public bool CanMoveMesh(MMesh mesh, Vector3 positionDelta, Quaternion rotDelta) { - Bounds newBounds = mesh.CalculateBounds(positionDelta, rotDelta); - return Math3d.ContainsBounds(bounds, newBounds); - } - - /// - /// Attempts to add a mesh to the model. - /// - /// The Mesh. - /// Whether to apply a shiny effect to the mesh addition. - /// True if the mesh was added to the model. - /// Thrown when a mesh with the same id is already in the model. - public bool AddMesh(MMesh mesh, bool applyAddMeshEffect = false) { - AssertOrThrow.True(writeable, "Model is not writable."); - AssertOrThrow.False(meshById.ContainsKey(mesh.id), "Mesh already exists."); - - if (!CanAddMesh(mesh)) - return false; - - meshById[mesh.id] = mesh; - if (applyAddMeshEffect) { - UXEffectManager.GetEffectManager().StartEffect(new MeshInsertEffect(mesh, this)); - } else { - remesher.AddMesh(mesh); - } - - if (mesh.groupId != MMesh.GROUP_NONE) { - // Assign mesh to its group. This will update our index which says which meshes belong - // to which group. - SetMeshGroup(mesh.id, mesh.groupId); - } - - if (OnMeshAdded != null) { - OnMeshAdded(mesh); - } - return true; - } - - /// - /// Attempts to add a mesh to the model, given the contents of an obj file and an mtl file. - /// This will position the imported mesh in model space such that the imported geometry sits in the direction - /// that the viewer is currently viewing and at a given minimum distance. - /// - /// The contents of a .obj file. - /// The contents of a .mtl file. - /// The position of the viewer, in model space. - /// The direction the viewer is facing, in model space. - /// The minimum distance from the viewer at which the imported geometry - /// should be placed. - /// True if the mesh was added to the model. - public bool AddMeshFromObjAndMtl(string objFileContents, string mtlFileContents, Vector3 viewerPosInModelSpace, - Vector3 viewerDirInModelSpace, float minDistanceFromViewer) { - AssertOrThrow.True(writeable, "Model is not writable."); - - MMesh mesh; - if (!ObjImporter.MMeshFromObjFile(objFileContents, mtlFileContents, GenerateMeshId(), out mesh) || - !CanAddMesh(mesh)) { - return false; - } - - // We will now transform the mesh such that it's in front of the user but at the specified minimum distance. - - // Get the original bounding box. - Bounds bounds = mesh.bounds; - - // Rotate the mesh to match the direction of the viewer. - MMesh.MoveMMesh(mesh, Vector3.zero, Quaternion.FromToRotation(Vector3.forward, viewerDirInModelSpace)); - - // Now figure out where the center of the imported geometry should be. We can figure this out by starting - // at the viewer position and then moving along the viewing direction. The distance we should move is - // the minimum distance plus half the depth of the bounding box, to guarantee that the nearest part of - // the geometry will be at least minDistanceFromViewer away from the viewer. - float distToCenterOfGeometry = minDistanceFromViewer + mesh.bounds.extents.z * 0.5f; - Vector3 correctCenter = viewerPosInModelSpace + distToCenterOfGeometry * viewerDirInModelSpace.normalized; - - // Now transform the mesh to place the center of its bounding box in the right place. - Vector3 offset = correctCenter - mesh.bounds.center; - MMesh.MoveMMesh(mesh, offset, Quaternion.identity); - - // Finally, add the mesh to the model. - AddMesh(mesh); - return true; - } - - /// - /// Remove a given mesh from the model. The mesh must be in the model. - /// - /// - /// Thrown when a mesh with the given id is not in the model, or if the model is not writeable. - /// - public void DeleteMesh(int meshId) { - AssertOrThrow.True(writeable, "Model is not writable."); - AssertOrThrow.True(meshById.ContainsKey(meshId), "Mesh not found."); - - // Remove mesh from the set of all meshes, and from its group, before sending the call to the spatial - // index. This will allow the spatial index to check the state of the model before returning any items - // that may be queued for spatial index removal. - MMesh mesh = meshById[meshId]; - RemoveMeshFromGroup(mesh); - meshById.Remove(meshId); - - // If the mesh was marked for deletion (not all are), then remove it from that set. - bool wasMarkedForDeletion = meshesMarkedForDeletion.Remove(meshId); - - // Remove the mesh from either hiddenMeshes, or the remesher. - hiddenMeshes.Remove(meshId); - remesher.RemoveMesh(meshId); - - PeltzerMain.Instance.GetSelector().ResetInactive(); - - // Queue the mesh for deletion from the spatial index, and trigger any other code registered to listen for - // mesh deletion. - if (OnMeshDeleted != null) { - OnMeshDeleted(mesh); - } - } - - /// - /// Set the face properties for all faces of a mesh. - /// - /// The mesh ID of the mesh to modify. - /// The properties to set on all faces. - public void ChangeAllFaceProperties(int meshId, FaceProperties newPropertiesForAllFaces) { - ChangeFaceProperties(meshId, newPropertiesForAllFaces, null); - } - - /// - /// Set face properties for each face of a mesh. - /// - /// The mesh ID of the mesh to modify. - /// The properties to set on each face. - public void ChangeFaceProperties(int meshId, Dictionary propertiesByFaceId) { - ChangeFaceProperties(meshId, null, propertiesByFaceId); - } + // Checks if the undo stack is about to exceed the maximum size, and discard the older half if it is. + private void LimitUndoStack() + { + if (undoStack.Count > undoStackMaxSize - 1) + { + List reducedCommandList = new List(undoStackMaxSize / 2); + for (int i = 0; i < undoStackMaxSize / 2; i++) + { + reducedCommandList.Add(undoStack.Pop()); + } + + undoStack.Clear(); + for (int i = reducedCommandList.Count - 1; i >= 0; i--) + { + undoStack.Push(reducedCommandList[i]); + } + } + } - /// - /// Changes the face properties for the indicated mesh. - /// - /// The ID of the mesh whose face properties are to be changed. - /// If not null, the new FaceProperties to apply to all mesh faces. - /// If propertiesForAllFaces is null, this is a dictionary indicating - /// which FaceProperties to apply to each face. - private void ChangeFaceProperties(int meshId, FaceProperties? propertiesForAllFaces, - Dictionary propertiesByFaceId) { - MMesh mesh = GetMesh(meshId); - if (propertiesForAllFaces != null) { - foreach (int faceId in mesh.GetFaceIds()) { - mesh.GetFace(faceId).SetProperties(propertiesForAllFaces.Value); + // Sets the maximum size of the undo stack. + public static void SetMaxUndoStackSize(int maxSize) + { + undoStackMaxSize = maxSize; } - } else { - foreach (KeyValuePair pair in propertiesByFaceId) { - mesh.GetFace(pair.Key).SetProperties(pair.Value); + + /// + /// Apply a Command to the model. + /// + /// + public void ApplyCommand(Command command) + { + AssertOrThrow.True(writeable, "Model is not writable."); + + if (OnValidateCommand != null) + { + // Check with all registered command validators. It only takes one of them to veto the command. + foreach (Delegate del in OnValidateCommand.GetInvocationList()) + { + CommandValidator validator = (CommandValidator)del; + if (!validator.Invoke(command)) + { + // Validator rejected the command. + return; + } + } + } + + redoStack.Clear(); // Once we apply a command, clear all redos + AddAndMaybeBatchCommands(command); + command.ApplyToModel(this); + if (OnCommandApplied != null) + { + OnCommandApplied(command); + } } - } - MeshUpdated(meshId, /* materialsChanged */ true, /* geometryChanged */ false); - } - /// - /// Notify the model that a mesh was moved (or changed in any way that - /// affects its bounds). - /// - /// The mesh's id. - /// If true, the mesh's materials changed. - /// If true, the mesh's offset, rotation, faces or verts changed. - /// If true, the mesh's faces or verts changed. - public void MeshUpdated(int meshId, bool materialsChanged, bool geometryChanged, bool vertsOrFacesChanged = true) { - AssertOrThrow.True(writeable, "Model is not writable."); - MMesh mesh; - bool hasMesh = meshById.TryGetValue(meshId, out mesh); - AssertOrThrow.True(hasMesh, "Mesh not found."); - - PeltzerMain.Instance.GetSelector().ResetInactive(); - - if (OnMeshChanged != null) { - OnMeshChanged(mesh, materialsChanged, geometryChanged, vertsOrFacesChanged); - } - // Update the renderer. - remesher.RemoveMesh(meshId); - if (!hiddenMeshes.Contains(meshId)) { - remesher.AddMesh(mesh); - } - } + /// + /// Whether an 'undo' can be applied at this time. + /// + /// + public bool CanUndo() + { + return undoStack.Count > 0; + } - /// - /// If not already hidden, temporarily hide the mesh while rendering since it is - /// being actively edited. Other tools are responsible for drawing the mesh - /// during this time. - /// - /// The mesh id. - private void HideMesh(int meshId) { - remesher.RemoveMesh(meshId); - hiddenMeshes.Add(meshId); - } + /// + /// Undo the last command applied to the model -- if there is one. + /// + public bool Undo() + { + AssertOrThrow.True(writeable, "Model is not writable."); + + if (undoStack.Count > 0) + { + // Abort all undo batching. + undoBatchStartTime = 0.0f; + + Command command = undoStack.Pop(); + redoStack.Push(command.GetUndoCommand(this)); + command.ApplyToModel(this); + currentCommand = command; + + if (OnUndo != null) + { + OnUndo(command); + } + return true; + } + return false; + } - /// - /// If the given mesh is hidden, unhides it. - /// - /// The mesh id. - private void UnhideMesh(int meshId) { - if (!hiddenMeshes.Contains(meshId)) - return; + /// + /// Redo the previously undone command, if possible. + /// + public bool Redo() + { + AssertOrThrow.True(writeable, "Model is not writable."); + + if (redoStack.Count > 0) + { + // Abort all undo batching. + undoBatchStartTime = 0.0f; + + Command command = redoStack.Pop(); + undoStack.Push(command.GetUndoCommand(this)); + command.ApplyToModel(this); + currentCommand = command; + + if (OnRedo != null) + { + OnRedo(command); + } + return true; + } + return false; + } - hiddenMeshes.Remove(meshId); - remesher.AddMesh(meshById[meshId]); - } + /// + /// Whether a given mesh can be added to the model, specifically whether it would fit within the model's bounds. + /// + /// The Mesh. + /// True if the mesh can be added. + public bool CanAddMesh(MMesh mesh) + { + return Math3d.ContainsBounds(bounds, mesh.bounds); + } - /// - /// Marks a mesh for deletion, removing it from the ReMesher or destroying its preview. - /// A mesh marked for deletion will never be shown. - /// - public void MarkMeshForDeletion(int meshId) { - remesher.RemoveMesh(meshId); + /// + /// Whether a given mesh can be moved by specific dimensions and remain within the model's bounds. + /// + /// The Mesh. + /// The positional move delta. + /// The rotational move delta. + /// True if the mesh can be moved as specified. + public bool CanMoveMesh(MMesh mesh, Vector3 positionDelta, Quaternion rotDelta) + { + Bounds newBounds = mesh.CalculateBounds(positionDelta, rotDelta); + return Math3d.ContainsBounds(bounds, newBounds); + } - if (renderOwners.ContainsKey(meshId)) { - renderOwners[meshId].ClaimMesh(meshId, this); - renderOwners.Remove(meshId); - } + /// + /// Attempts to add a mesh to the model. + /// + /// The Mesh. + /// Whether to apply a shiny effect to the mesh addition. + /// True if the mesh was added to the model. + /// Thrown when a mesh with the same id is already in the model. + public bool AddMesh(MMesh mesh, bool applyAddMeshEffect = false) + { + AssertOrThrow.True(writeable, "Model is not writable."); + AssertOrThrow.False(meshById.ContainsKey(mesh.id), "Mesh already exists."); + + if (!CanAddMesh(mesh)) + return false; + + meshById[mesh.id] = mesh; + if (applyAddMeshEffect) + { + UXEffectManager.GetEffectManager().StartEffect(new MeshInsertEffect(mesh, this)); + } + else + { + remesher.AddMesh(mesh); + } + + if (mesh.groupId != MMesh.GROUP_NONE) + { + // Assign mesh to its group. This will update our index which says which meshes belong + // to which group. + SetMeshGroup(mesh.id, mesh.groupId); + } + + if (OnMeshAdded != null) + { + OnMeshAdded(mesh); + } + return true; + } - meshesMarkedForDeletion.Add(meshId); - } + /// + /// Attempts to add a mesh to the model, given the contents of an obj file and an mtl file. + /// This will position the imported mesh in model space such that the imported geometry sits in the direction + /// that the viewer is currently viewing and at a given minimum distance. + /// + /// The contents of a .obj file. + /// The contents of a .mtl file. + /// The position of the viewer, in model space. + /// The direction the viewer is facing, in model space. + /// The minimum distance from the viewer at which the imported geometry + /// should be placed. + /// True if the mesh was added to the model. + public bool AddMeshFromObjAndMtl(string objFileContents, string mtlFileContents, Vector3 viewerPosInModelSpace, + Vector3 viewerDirInModelSpace, float minDistanceFromViewer) + { + AssertOrThrow.True(writeable, "Model is not writable."); + + MMesh mesh; + if (!ObjImporter.MMeshFromObjFile(objFileContents, mtlFileContents, GenerateMeshId(), out mesh) || + !CanAddMesh(mesh)) + { + return false; + } + + // We will now transform the mesh such that it's in front of the user but at the specified minimum distance. + + // Get the original bounding box. + Bounds bounds = mesh.bounds; + + // Rotate the mesh to match the direction of the viewer. + MMesh.MoveMMesh(mesh, Vector3.zero, Quaternion.FromToRotation(Vector3.forward, viewerDirInModelSpace)); + + // Now figure out where the center of the imported geometry should be. We can figure this out by starting + // at the viewer position and then moving along the viewing direction. The distance we should move is + // the minimum distance plus half the depth of the bounding box, to guarantee that the nearest part of + // the geometry will be at least minDistanceFromViewer away from the viewer. + float distToCenterOfGeometry = minDistanceFromViewer + mesh.bounds.extents.z * 0.5f; + Vector3 correctCenter = viewerPosInModelSpace + distToCenterOfGeometry * viewerDirInModelSpace.normalized; + + // Now transform the mesh to place the center of its bounding box in the right place. + Vector3 offset = correctCenter - mesh.bounds.center; + MMesh.MoveMMesh(mesh, offset, Quaternion.identity); + + // Finally, add the mesh to the model. + AddMesh(mesh); + return true; + } - /// - /// Unmarks a mesh for deletion, restoring it to the ReMesher. - /// - public void UnmarkMeshForDeletion(int meshId) { - meshesMarkedForDeletion.Remove(meshId); - UnhideMesh(meshId); - MMesh mesh = meshById[meshId]; - } + /// + /// Remove a given mesh from the model. The mesh must be in the model. + /// + /// + /// Thrown when a mesh with the given id is not in the model, or if the model is not writeable. + /// + public void DeleteMesh(int meshId) + { + AssertOrThrow.True(writeable, "Model is not writable."); + AssertOrThrow.True(meshById.ContainsKey(meshId), "Mesh not found."); + + // Remove mesh from the set of all meshes, and from its group, before sending the call to the spatial + // index. This will allow the spatial index to check the state of the model before returning any items + // that may be queued for spatial index removal. + MMesh mesh = meshById[meshId]; + RemoveMeshFromGroup(mesh); + meshById.Remove(meshId); + + // If the mesh was marked for deletion (not all are), then remove it from that set. + bool wasMarkedForDeletion = meshesMarkedForDeletion.Remove(meshId); + + // Remove the mesh from either hiddenMeshes, or the remesher. + hiddenMeshes.Remove(meshId); + remesher.RemoveMesh(meshId); + + PeltzerMain.Instance.GetSelector().ResetInactive(); + + // Queue the mesh for deletion from the spatial index, and trigger any other code registered to listen for + // mesh deletion. + if (OnMeshDeleted != null) + { + OnMeshDeleted(mesh); + } + } - /// - /// Allows another owner to claim a mesh if and only if it is not currently owned. - /// - /// The id of the mesh being claimed. - /// - public int ClaimMeshIfUnowned(int meshId, IMeshRenderOwner fosterRenderer) { - if (renderOwners.ContainsKey(meshId)) { - return -1; - } else { - return ClaimMesh(meshId, fosterRenderer); - } - } + /// + /// Set the face properties for all faces of a mesh. + /// + /// The mesh ID of the mesh to modify. + /// The properties to set on all faces. + public void ChangeAllFaceProperties(int meshId, FaceProperties newPropertiesForAllFaces) + { + ChangeFaceProperties(meshId, newPropertiesForAllFaces, null); + } - /// - /// Claim responsibility for rendering a mesh from this class. - /// - /// The id of the mesh being claimed - /// The id of the mesh that was claimed, or -1 for failure. - public int ClaimMesh(int meshId, IMeshRenderOwner fosterRenderer) { - //Debug.Log("Model claim mesh called " + fosterRenderer.GetType()); - IMeshRenderOwner renderOwner; - if (renderOwners.TryGetValue(meshId, out renderOwner)) { - //Debug.Log("Prev owner was " + renderOwners[meshId].GetType()); - //Debug.Log(fosterRenderer + " claiming " + meshId + " from " + renderOwners[meshId]); - if (meshesMarkedForDeletion.Contains(meshId)) { - throw new Exception("Mesh marked for deletion has an owner when it ought not to be able to."); + /// + /// Set face properties for each face of a mesh. + /// + /// The mesh ID of the mesh to modify. + /// The properties to set on each face. + public void ChangeFaceProperties(int meshId, Dictionary propertiesByFaceId) + { + ChangeFaceProperties(meshId, null, propertiesByFaceId); } - // An error, but shouldn't be a fatal one. - if (renderOwner == fosterRenderer) return meshId; + /// + /// Changes the face properties for the indicated mesh. + /// + /// The ID of the mesh whose face properties are to be changed. + /// If not null, the new FaceProperties to apply to all mesh faces. + /// If propertiesForAllFaces is null, this is a dictionary indicating + /// which FaceProperties to apply to each face. + private void ChangeFaceProperties(int meshId, FaceProperties? propertiesForAllFaces, + Dictionary propertiesByFaceId) + { + MMesh mesh = GetMesh(meshId); + if (propertiesForAllFaces != null) + { + foreach (int faceId in mesh.GetFaceIds()) + { + mesh.GetFace(faceId).SetProperties(propertiesForAllFaces.Value); + } + } + else + { + foreach (KeyValuePair pair in propertiesByFaceId) + { + mesh.GetFace(pair.Key).SetProperties(pair.Value); + } + } + MeshUpdated(meshId, /* materialsChanged */ true, /* geometryChanged */ false); + } - int claimedMeshId = renderOwner.ClaimMesh(meshId, fosterRenderer); - if (claimedMeshId == -1) { - throw new Exception("Mesh owner [" + renderOwner + "] failed to allow mesh to be claimed"); + /// + /// Notify the model that a mesh was moved (or changed in any way that + /// affects its bounds). + /// + /// The mesh's id. + /// If true, the mesh's materials changed. + /// If true, the mesh's offset, rotation, faces or verts changed. + /// If true, the mesh's faces or verts changed. + public void MeshUpdated(int meshId, bool materialsChanged, bool geometryChanged, bool vertsOrFacesChanged = true) + { + AssertOrThrow.True(writeable, "Model is not writable."); + MMesh mesh; + bool hasMesh = meshById.TryGetValue(meshId, out mesh); + AssertOrThrow.True(hasMesh, "Mesh not found."); + + PeltzerMain.Instance.GetSelector().ResetInactive(); + + if (OnMeshChanged != null) + { + OnMeshChanged(mesh, materialsChanged, geometryChanged, vertsOrFacesChanged); + } + // Update the renderer. + remesher.RemoveMesh(meshId); + if (!hiddenMeshes.Contains(meshId)) + { + remesher.AddMesh(mesh); + } } - else { - renderOwners[meshId] = fosterRenderer; - return meshId; + + /// + /// If not already hidden, temporarily hide the mesh while rendering since it is + /// being actively edited. Other tools are responsible for drawing the mesh + /// during this time. + /// + /// The mesh id. + private void HideMesh(int meshId) + { + remesher.RemoveMesh(meshId); + hiddenMeshes.Add(meshId); } - } - else { - // Handle currently unowned mesh. - //Debug.Log(fosterRenderer + " claiming " + meshId + " from Model"); - if (meshesMarkedForDeletion.Contains(meshId)) { - return -1; + + /// + /// If the given mesh is hidden, unhides it. + /// + /// The mesh id. + private void UnhideMesh(int meshId) + { + if (!hiddenMeshes.Contains(meshId)) + return; + + hiddenMeshes.Remove(meshId); + remesher.AddMesh(meshById[meshId]); } - //Debug.Log("Model hiding mesh: " + meshId); - HideMesh(meshId); - renderOwners[meshId] = fosterRenderer; - previousRenderOwners.Remove(meshId); - return meshId; - } - } + /// + /// Marks a mesh for deletion, removing it from the ReMesher or destroying its preview. + /// A mesh marked for deletion will never be shown. + /// + public void MarkMeshForDeletion(int meshId) + { + remesher.RemoveMesh(meshId); + + if (renderOwners.ContainsKey(meshId)) + { + renderOwners[meshId].ClaimMesh(meshId, this); + renderOwners.Remove(meshId); + } + + meshesMarkedForDeletion.Add(meshId); + } - /// - /// Gives responsibility for rendering a mesh to this class. Generally, this should only be done to Model - the - /// general dynamic being that tool classes attempt to claim ownership from the current owner whenever they need a - /// preview, and then bequeath it back to Model when they are done (provided a competing claim hasn't arisen) - if - /// ownership is needed sooner, Model will call Claim on the previous owner. - /// - /// The id of the mesh being bequeathed - /// The id of the mesh that is being bequeathed, or -1 for failure. - public void RelinquishMesh(int meshId, IMeshRenderOwner fosterRenderer) { - IMeshRenderOwner renderOwner; - if (renderOwners.TryGetValue(meshId, out renderOwner) && renderOwner == fosterRenderer) { - UnhideMesh(meshId); - renderOwners.Remove(meshId); - previousRenderOwners[meshId] = fosterRenderer; - } else { - if (renderOwners.ContainsKey(meshId)) { - throw new Exception("Incorrect owner attempted to relinquish mesh. Current owner " + renderOwners[meshId] - + " erroneous owner: " + fosterRenderer); + /// + /// Unmarks a mesh for deletion, restoring it to the ReMesher. + /// + public void UnmarkMeshForDeletion(int meshId) + { + meshesMarkedForDeletion.Remove(meshId); + UnhideMesh(meshId); + MMesh mesh = meshById[meshId]; } - else { - if (!previousRenderOwners.ContainsKey(meshId)) { - throw new Exception("Attempt to relinquish Model owned mesh with no previous owner by " + fosterRenderer); - } - else { - throw new Exception("Attempt to relinquish Model owned mesh. Previous owner " + previousRenderOwners[meshId] - + " erroneous owner: " + fosterRenderer); - } + + /// + /// Allows another owner to claim a mesh if and only if it is not currently owned. + /// + /// The id of the mesh being claimed. + /// + public int ClaimMeshIfUnowned(int meshId, IMeshRenderOwner fosterRenderer) + { + if (renderOwners.ContainsKey(meshId)) + { + return -1; + } + else + { + return ClaimMesh(meshId, fosterRenderer); + } } - } - } - public void AddToRemesher(int meshId) { - if (!meshById.ContainsKey(meshId)) { - return; - } + /// + /// Claim responsibility for rendering a mesh from this class. + /// + /// The id of the mesh being claimed + /// The id of the mesh that was claimed, or -1 for failure. + public int ClaimMesh(int meshId, IMeshRenderOwner fosterRenderer) + { + //Debug.Log("Model claim mesh called " + fosterRenderer.GetType()); + IMeshRenderOwner renderOwner; + if (renderOwners.TryGetValue(meshId, out renderOwner)) + { + //Debug.Log("Prev owner was " + renderOwners[meshId].GetType()); + //Debug.Log(fosterRenderer + " claiming " + meshId + " from " + renderOwners[meshId]); + if (meshesMarkedForDeletion.Contains(meshId)) + { + throw new Exception("Mesh marked for deletion has an owner when it ought not to be able to."); + } + + // An error, but shouldn't be a fatal one. + if (renderOwner == fosterRenderer) return meshId; + + int claimedMeshId = renderOwner.ClaimMesh(meshId, fosterRenderer); + if (claimedMeshId == -1) + { + throw new Exception("Mesh owner [" + renderOwner + "] failed to allow mesh to be claimed"); + } + else + { + renderOwners[meshId] = fosterRenderer; + return meshId; + } + } + else + { + // Handle currently unowned mesh. + //Debug.Log(fosterRenderer + " claiming " + meshId + " from Model"); + if (meshesMarkedForDeletion.Contains(meshId)) + { + return -1; + } + + //Debug.Log("Model hiding mesh: " + meshId); + HideMesh(meshId); + renderOwners[meshId] = fosterRenderer; + previousRenderOwners.Remove(meshId); + return meshId; + } + } - MMesh mesh = meshById[meshId]; - remesher.AddMesh(mesh); - } + /// + /// Gives responsibility for rendering a mesh to this class. Generally, this should only be done to Model - the + /// general dynamic being that tool classes attempt to claim ownership from the current owner whenever they need a + /// preview, and then bequeath it back to Model when they are done (provided a competing claim hasn't arisen) - if + /// ownership is needed sooner, Model will call Claim on the previous owner. + /// + /// The id of the mesh being bequeathed + /// The id of the mesh that is being bequeathed, or -1 for failure. + public void RelinquishMesh(int meshId, IMeshRenderOwner fosterRenderer) + { + IMeshRenderOwner renderOwner; + if (renderOwners.TryGetValue(meshId, out renderOwner) && renderOwner == fosterRenderer) + { + UnhideMesh(meshId); + renderOwners.Remove(meshId); + previousRenderOwners[meshId] = fosterRenderer; + } + else + { + if (renderOwners.ContainsKey(meshId)) + { + throw new Exception("Incorrect owner attempted to relinquish mesh. Current owner " + renderOwners[meshId] + + " erroneous owner: " + fosterRenderer); + } + else + { + if (!previousRenderOwners.ContainsKey(meshId)) + { + throw new Exception("Attempt to relinquish Model owned mesh with no previous owner by " + fosterRenderer); + } + else + { + throw new Exception("Attempt to relinquish Model owned mesh. Previous owner " + previousRenderOwners[meshId] + + " erroneous owner: " + fosterRenderer); + } + } + } + } - /// - /// Get all meshes in the model. - /// - /// A collection of all meshes in the model. - public ICollection GetAllMeshes() { - return meshById.Values; - } + public void AddToRemesher(int meshId) + { + if (!meshById.ContainsKey(meshId)) + { + return; + } - /// - /// Get corresponding meshes for given ids. - /// - /// A collection of meshes in the model matching the given ids. - public List GetMatchingMeshes(HashSet ids) { - List list = new List(ids.Count); - foreach (int id in ids) { - MMesh mesh; - if (meshById.TryGetValue(id, out mesh)) { - list.Add(mesh); + MMesh mesh = meshById[meshId]; + remesher.AddMesh(mesh); } - } - return list; - } - /// - /// Returns the number of meshes in this model (including those that are 'hidden' or outside of ReMesher). - /// - public int GetNumberOfMeshes() { - return meshById.Count; - } + /// + /// Get all meshes in the model. + /// + /// A collection of all meshes in the model. + public ICollection GetAllMeshes() + { + return meshById.Values; + } - /// - /// Get a mesh by id. - /// - /// - /// The mesh. - /// Thrown when a mesh with - /// the given id is not in the model. - public MMesh GetMesh(int meshId) { - MMesh mesh; - bool hasMesh = meshById.TryGetValue(meshId, out mesh); - AssertOrThrow.True(hasMesh, "Mesh not found."); - return mesh; - } + /// + /// Get corresponding meshes for given ids. + /// + /// A collection of meshes in the model matching the given ids. + public List GetMatchingMeshes(HashSet ids) + { + List list = new List(ids.Count); + foreach (int id in ids) + { + MMesh mesh; + if (meshById.TryGetValue(id, out mesh)) + { + list.Add(mesh); + } + } + return list; + } - /// - /// Gets meshes by id. - /// - /// The IDs of the meshes to get. - /// The requested meshes. - /// Thrown when a mesh with - /// one or more of the given ids is not in the model. - public List GetMeshes(IEnumerable meshIds) { - return new List(meshIds.Select(id => GetMesh(id))); - } + /// + /// Returns the number of meshes in this model (including those that are 'hidden' or outside of ReMesher). + /// + public int GetNumberOfMeshes() + { + return meshById.Count; + } - /// - /// Check if a mesh of the given id exists. - /// - /// Mesh id. - /// True if a mesh of that id is in the model. - public bool HasMesh(int meshId) { - return meshById.ContainsKey(meshId); - } + /// + /// Get a mesh by id. + /// + /// + /// The mesh. + /// Thrown when a mesh with + /// the given id is not in the model. + public MMesh GetMesh(int meshId) + { + MMesh mesh; + bool hasMesh = meshById.TryGetValue(meshId, out mesh); + AssertOrThrow.True(hasMesh, "Mesh not found."); + return mesh; + } - /// - /// Returns the list of meshes that have at least one face with the given material ID. - /// - /// The material ID to search for. - /// The list of meshes where at least one of the faces has that material ID. - public List GetMeshesByMaterialId(int materialId) { - List result = new List(); - foreach (MMesh mesh in meshById.Values) { - foreach (Face face in mesh.GetFaces()) { - if (face.properties.materialId == materialId) { - result.Add(mesh); - break; - } + /// + /// Gets meshes by id. + /// + /// The IDs of the meshes to get. + /// The requested meshes. + /// Thrown when a mesh with + /// one or more of the given ids is not in the model. + public List GetMeshes(IEnumerable meshIds) + { + return new List(meshIds.Select(id => GetMesh(id))); } - } - return result; - } - /// - /// Returns the (only) mesh in the scene that has the given material. - /// - /// The material ID to search for. - /// The only mesh with the given material. Throws an exception if there's - /// more than one, or if there are none. - public MMesh GetOnlyMeshWithMaterialId(int materialId) { - List result = GetMeshesByMaterialId(materialId); - AssertOrThrow.True(result.Count == 1, - "Expected exactly one mesh with material ID " + materialId + ", there are " + result.Count); - return result[0]; - } + /// + /// Check if a mesh of the given id exists. + /// + /// Mesh id. + /// True if a mesh of that id is in the model. + public bool HasMesh(int meshId) + { + return meshById.ContainsKey(meshId); + } - /// - /// Check if a group of the given id exists. - /// - /// Group id. - /// True if a group of that id is in the model. - public bool HasGroup(int groupId) { - return groupById.ContainsKey(groupId); - } + /// + /// Returns the list of meshes that have at least one face with the given material ID. + /// + /// The material ID to search for. + /// The list of meshes where at least one of the faces has that material ID. + public List GetMeshesByMaterialId(int materialId) + { + List result = new List(); + foreach (MMesh mesh in meshById.Values) + { + foreach (Face face in mesh.GetFaces()) + { + if (face.properties.materialId == materialId) + { + result.Add(mesh); + break; + } + } + } + return result; + } - /// - /// Fetches the meshes in a given group. - /// Returns true and populates the given list with the meshes in a given group, - /// or returns false and leaves the list in its previous state. - /// - public bool GetMeshesInGroup(int groupId, out List meshes) { - return groupById.TryGetValue(groupId, out meshes); - } + /// + /// Returns the (only) mesh in the scene that has the given material. + /// + /// The material ID to search for. + /// The only mesh with the given material. Throws an exception if there's + /// more than one, or if there are none. + public MMesh GetOnlyMeshWithMaterialId(int materialId) + { + List result = GetMeshesByMaterialId(materialId); + AssertOrThrow.True(result.Count == 1, + "Expected exactly one mesh with material ID " + materialId + ", there are " + result.Count); + return result[0]; + } - /// - /// Returns the number of meshes in a given group, or 0 if the group does not exist. - /// - public int GetNumMeshesInGroup(int groupId) { - List meshesInGroup; - if (groupById.TryGetValue(groupId, out meshesInGroup)) { - return meshesInGroup.Count; - } - return 0; - } + /// + /// Check if a group of the given id exists. + /// + /// Group id. + /// True if a group of that id is in the model. + public bool HasGroup(int groupId) + { + return groupById.ContainsKey(groupId); + } - /// - /// Generates a new mesh ID that does not refer to any existing mesh. - /// - /// An integer id. - public int GenerateMeshId(List badIds = null) { - int meshId; - do { - meshId = random.Next(); - } while (HasMesh(meshId) || (badIds != null && badIds.Contains(meshId))); - return meshId; - } + /// + /// Fetches the meshes in a given group. + /// Returns true and populates the given list with the meshes in a given group, + /// or returns false and leaves the list in its previous state. + /// + public bool GetMeshesInGroup(int groupId, out List meshes) + { + return groupById.TryGetValue(groupId, out meshes); + } - /// - /// Generates a new group ID that does not refer to any existing group. - /// - /// An integer id. - public int GenerateGroupId() { - int groupId; - do { - groupId = random.Next(); - // Note that we can't use MMesh.GROUP_NONE as a group ID because it's a special value - // that means "no group". - } while (groupId == MMesh.GROUP_NONE || HasGroup(groupId)); - return groupId; - } + /// + /// Returns the number of meshes in a given group, or 0 if the group does not exist. + /// + public int GetNumMeshesInGroup(int groupId) + { + List meshesInGroup; + if (groupById.TryGetValue(groupId, out meshesInGroup)) + { + return meshesInGroup.Count; + } + return 0; + } - /// - /// Takes a set of mesh IDs and expands it to also include all mesh IDs of - /// the meshes that are part of the same groups as the original ones. - /// So if meshes { 100, 101, 102, 103 } are a group and { 200, 201, 202 } are another group, - /// then ExpandMeshIdsToGroupMates({ 102, 201 }) would result in - /// { 100, 101, 102, 103, 200, 201, 202 }, which includes the original meshes and - /// all the other meshes in the same groups. - /// The IDs of the meshes whose groups are to be retrieved. - /// This input will be mutated to include the extra mesh IDs. The resulting - /// set will not contain duplicates.. - public void ExpandMeshIdsToGroupMates(HashSet meshIdsInOut) { - // For each mesh in the input set, check which group it belongs to; if it belongs - // to a group, then add the other members of the group to the list. - HashSet originalMeshIds = new HashSet(meshIdsInOut); - foreach (int meshId in originalMeshIds) { - MMesh mesh = GetMesh(meshId); - if (mesh.groupId != MMesh.GROUP_NONE) { - // Mesh is in a group. Add all the members of the group. - List peers = groupById[mesh.groupId]; - foreach (MMesh peer in peers) { - meshIdsInOut.Add(peer.id); - } + /// + /// Generates a new mesh ID that does not refer to any existing mesh. + /// + /// An integer id. + public int GenerateMeshId(List badIds = null) + { + int meshId; + do + { + meshId = random.Next(); + } while (HasMesh(meshId) || (badIds != null && badIds.Contains(meshId))); + return meshId; } - } - } - /// - /// Returns whether or not all the passed meshes belong to a single group. - /// - /// The meshes to test. - /// True if all meshes belong to a single group (or if the list is empty), - /// false otherwise. GROUP_NONE is not considered a "group" so if any meshes are in - /// GROUP_NONE, this method will return false. - public bool AreMeshesInSameGroup(IEnumerable meshes) { - Model model = PeltzerMain.Instance.model; - // Note: there are more readable ways to write this code using Count() and Any() and such - // niceties, but since this method is in the critical path, this is written so that the - // iterator only has to be traversed once. - IEnumerator enumerator = meshes.GetEnumerator(); - if (!enumerator.MoveNext()) { - // List is empty. - return true; - } - // We test by comparing all group IDs to the first one. They must all match. - while (!model.HasMesh(enumerator.Current)) { - if (!enumerator.MoveNext()) { - // Only invalid mesh IDs were passed. - return true; + /// + /// Generates a new group ID that does not refer to any existing group. + /// + /// An integer id. + public int GenerateGroupId() + { + int groupId; + do + { + groupId = random.Next(); + // Note that we can't use MMesh.GROUP_NONE as a group ID because it's a special value + // that means "no group". + } while (groupId == MMesh.GROUP_NONE || HasGroup(groupId)); + return groupId; } - } - int expectedGroupId = model.GetMesh(enumerator.Current).groupId; - if (expectedGroupId == MMesh.GROUP_NONE) { - // GROUP_NONE is not a serious group. - return false; - } - while (enumerator.MoveNext()) { - if (model.HasMesh(enumerator.Current)) { - if (expectedGroupId != model.GetMesh(enumerator.Current).groupId) { - return false; - } + + /// + /// Takes a set of mesh IDs and expands it to also include all mesh IDs of + /// the meshes that are part of the same groups as the original ones. + /// So if meshes { 100, 101, 102, 103 } are a group and { 200, 201, 202 } are another group, + /// then ExpandMeshIdsToGroupMates({ 102, 201 }) would result in + /// { 100, 101, 102, 103, 200, 201, 202 }, which includes the original meshes and + /// all the other meshes in the same groups. + /// The IDs of the meshes whose groups are to be retrieved. + /// This input will be mutated to include the extra mesh IDs. The resulting + /// set will not contain duplicates.. + public void ExpandMeshIdsToGroupMates(HashSet meshIdsInOut) + { + // For each mesh in the input set, check which group it belongs to; if it belongs + // to a group, then add the other members of the group to the list. + HashSet originalMeshIds = new HashSet(meshIdsInOut); + foreach (int meshId in originalMeshIds) + { + MMesh mesh = GetMesh(meshId); + if (mesh.groupId != MMesh.GROUP_NONE) + { + // Mesh is in a group. Add all the members of the group. + List peers = groupById[mesh.groupId]; + foreach (MMesh peer in peers) + { + meshIdsInOut.Add(peer.id); + } + } + } } - } - // All meshes are in the same group, and that group is not GROUP_NONE. - return true; - } - /// - /// (Re)assigns the given mesh to the given group ID. - /// - /// The ID of the mesh to reassign. - /// The new group on which to put the mesh. If this is - /// MMesh.GROUP_NONE, the mesh will be removed from the group. - public void SetMeshGroup(int meshId, int newGroupId) { - MMesh mesh = GetMesh(meshId); - // First, remove it from its previous group, if any. - RemoveMeshFromGroup(mesh); - // Now assign the mesh to the new group. - mesh.groupId = newGroupId; - if (newGroupId != MMesh.GROUP_NONE) { - // Add it to the dictionary. - if (!groupById.ContainsKey(newGroupId)) { - groupById[newGroupId] = new List(); + /// + /// Returns whether or not all the passed meshes belong to a single group. + /// + /// The meshes to test. + /// True if all meshes belong to a single group (or if the list is empty), + /// false otherwise. GROUP_NONE is not considered a "group" so if any meshes are in + /// GROUP_NONE, this method will return false. + public bool AreMeshesInSameGroup(IEnumerable meshes) + { + Model model = PeltzerMain.Instance.model; + // Note: there are more readable ways to write this code using Count() and Any() and such + // niceties, but since this method is in the critical path, this is written so that the + // iterator only has to be traversed once. + IEnumerator enumerator = meshes.GetEnumerator(); + if (!enumerator.MoveNext()) + { + // List is empty. + return true; + } + // We test by comparing all group IDs to the first one. They must all match. + while (!model.HasMesh(enumerator.Current)) + { + if (!enumerator.MoveNext()) + { + // Only invalid mesh IDs were passed. + return true; + } + } + int expectedGroupId = model.GetMesh(enumerator.Current).groupId; + if (expectedGroupId == MMesh.GROUP_NONE) + { + // GROUP_NONE is not a serious group. + return false; + } + while (enumerator.MoveNext()) + { + if (model.HasMesh(enumerator.Current)) + { + if (expectedGroupId != model.GetMesh(enumerator.Current).groupId) + { + return false; + } + } + } + // All meshes are in the same group, and that group is not GROUP_NONE. + return true; } - groupById[newGroupId].Add(mesh); - } - } - /// - /// Removes the mesh from its group, if it's in a group. Does nothing if the mesh - /// is not in a group. This also cleans up the group from the index if the group - /// becomes empty as a result of the removal. - /// - /// The mesh to remove from its group. - private void RemoveMeshFromGroup(MMesh mesh) { - if (mesh.groupId != MMesh.GROUP_NONE && groupById.ContainsKey(mesh.groupId)) { - groupById[mesh.groupId].Remove(mesh); - // If group is now empty, clean it up. - if (groupById[mesh.groupId].Count == 0) { - groupById.Remove(mesh.groupId); + /// + /// (Re)assigns the given mesh to the given group ID. + /// + /// The ID of the mesh to reassign. + /// The new group on which to put the mesh. If this is + /// MMesh.GROUP_NONE, the mesh will be removed from the group. + public void SetMeshGroup(int meshId, int newGroupId) + { + MMesh mesh = GetMesh(meshId); + // First, remove it from its previous group, if any. + RemoveMeshFromGroup(mesh); + // Now assign the mesh to the new group. + mesh.groupId = newGroupId; + if (newGroupId != MMesh.GROUP_NONE) + { + // Add it to the dictionary. + if (!groupById.ContainsKey(newGroupId)) + { + groupById[newGroupId] = new List(); + } + groupById[newGroupId].Add(mesh); + } } - } - mesh.groupId = MMesh.GROUP_NONE; - } - /// - /// Computes and returns the complete bounds of the all of the model's meshes. - /// - /// The bounding box that encapsulates all meshes of the model. - public Bounds FindBoundsOfAllMeshes() { - if (meshById.Count > 0) { - // Must be intialized as an actual bounds to be included in the encapsulation, or else will - // also encapsulate Bounds.zero. - Bounds allBounds = meshById.First().Value.bounds; - foreach(KeyValuePair pair in meshById) { - allBounds.Encapsulate(pair.Value.bounds); + /// + /// Removes the mesh from its group, if it's in a group. Does nothing if the mesh + /// is not in a group. This also cleans up the group from the index if the group + /// becomes empty as a result of the removal. + /// + /// The mesh to remove from its group. + private void RemoveMeshFromGroup(MMesh mesh) + { + if (mesh.groupId != MMesh.GROUP_NONE && groupById.ContainsKey(mesh.groupId)) + { + groupById[mesh.groupId].Remove(mesh); + // If group is now empty, clean it up. + if (groupById[mesh.groupId].Count == 0) + { + groupById.Remove(mesh.groupId); + } + } + mesh.groupId = MMesh.GROUP_NONE; } - return allBounds; - } else { - return new Bounds(Vector3.zero, Vector3.zero); - } - } - public bool IsMeshHidden(int meshId) { - return hiddenMeshes.Contains(meshId); - } + /// + /// Computes and returns the complete bounds of the all of the model's meshes. + /// + /// The bounding box that encapsulates all meshes of the model. + public Bounds FindBoundsOfAllMeshes() + { + if (meshById.Count > 0) + { + // Must be intialized as an actual bounds to be included in the encapsulation, or else will + // also encapsulate Bounds.zero. + Bounds allBounds = meshById.First().Value.bounds; + foreach (KeyValuePair pair in meshById) + { + allBounds.Encapsulate(pair.Value.bounds); + } + return allBounds; + } + else + { + return new Bounds(Vector3.zero, Vector3.zero); + } + } - public bool MeshIsMarkedForDeletion(int meshId) { - return meshesMarkedForDeletion.Contains(meshId); - } + public bool IsMeshHidden(int meshId) + { + return hiddenMeshes.Contains(meshId); + } - // For serialization - public List GetAllCommands() { - return allCommands; - } + public bool MeshIsMarkedForDeletion(int meshId) + { + return meshesMarkedForDeletion.Contains(meshId); + } - // For serialization. - public Stack GetUndoStack() { - return undoStack; - } + // For serialization + public List GetAllCommands() + { + return allCommands; + } - // For serialization. - public Stack GetRedoStack() { - return redoStack; - } + // For serialization. + public Stack GetUndoStack() + { + return undoStack; + } - // For test or tutorial only. - public void HideMeshForTestOrTutorial(int meshId) { - HideMesh(meshId); - } + // For serialization. + public Stack GetRedoStack() + { + return redoStack; + } - // For test or tutorial only. - public void UnhideMeshForTestOrTutorial(int meshId) { - UnhideMesh(meshId); - } + // For test or tutorial only. + public void HideMeshForTestOrTutorial(int meshId) + { + HideMesh(meshId); + } - public HashSet GetHiddenMeshes() { - return hiddenMeshes; - } + // For test or tutorial only. + public void UnhideMeshForTestOrTutorial(int meshId) + { + UnhideMesh(meshId); + } - // Meshinfos should not be modified outside of remesher. - // This exists to enable exporting coalesced meshes. - public HashSet GetAllRemesherMeshInfos() { - return remesher.GetAllMeshInfos(); - } + public HashSet GetHiddenMeshes() + { + return hiddenMeshes; + } - /// - /// Overloaded. Returns a set with the union of all remix IDs being used by meshes in the model. - /// - public HashSet GetAllRemixIds() { - return GetAllRemixIds(meshById.Values); - } + // Meshinfos should not be modified outside of remesher. + // This exists to enable exporting coalesced meshes. + public HashSet GetAllRemesherMeshInfos() + { + return remesher.GetAllMeshInfos(); + } - /// - /// Overloaded. Returns a set with the union of all remix IDs being used by the given meshes. - /// - public HashSet GetAllRemixIds(ICollection meshes) { - HashSet allRemixIds = new HashSet(); - foreach (MMesh mesh in meshes) { - if (mesh.remixIds != null) { - allRemixIds.UnionWith(mesh.remixIds); + /// + /// Overloaded. Returns a set with the union of all remix IDs being used by meshes in the model. + /// + public HashSet GetAllRemixIds() + { + return GetAllRemixIds(meshById.Values); } - } - return allRemixIds; - } - public String DebugConsoleDump() { - StringBuilder builder = new StringBuilder(); - HashSet meshInfos = remesher.GetAllMeshInfos(); - builder.Append("Remesher contents:\n"); - foreach (ReMesher.MeshInfo info in meshInfos) { - builder.Append("mat id: " + info.materialAndColor.matId + " contains [\n"); - foreach (int meshId in info.GetMeshIds()) { - builder.Append(" " + meshId + "\n"); + /// + /// Overloaded. Returns a set with the union of all remix IDs being used by the given meshes. + /// + public HashSet GetAllRemixIds(ICollection meshes) + { + HashSet allRemixIds = new HashSet(); + foreach (MMesh mesh in meshes) + { + if (mesh.remixIds != null) + { + allRemixIds.UnionWith(mesh.remixIds); + } + } + return allRemixIds; } - builder.Append("]\n"); - } - builder.Append("Hidden meshes: [\n"); - foreach (int meshId in hiddenMeshes) { - builder.Append(" " + meshId + "\n"); - } - builder.Append("]\n"); - builder.Append("Owned meshes: [\n"); - foreach (int meshId in this.renderOwners.Keys) { - builder.Append(" " + meshId + ":" + renderOwners[meshId].ToString() + "\n"); - } - builder.Append("]\n"); - builder.Append("Previous Render Owners: [\n"); - foreach (int meshId in previousRenderOwners.Keys) { - builder.Append(" " + meshId + ":" + previousRenderOwners[meshId].ToString() + "\n"); - } - builder.Append("]\n"); - MeshWithMaterialRenderer[] renderers = GameObject.FindObjectsOfType(); - builder.Append("Mesh with material renderers: [\n"); - foreach (MeshWithMaterialRenderer mwmr in renderers) { - builder.Append(" MeshWithMaterialRenderer: " + mwmr.gameObject.name + "\n"); - foreach (MeshWithMaterial mesh in mwmr.meshes) { - builder.Append(" meshmat: " + mesh.materialAndColor.matId + "\n"); - builder.Append(" mesh tricount: " + mesh.mesh.triangles.Length + "\n"); + + public String DebugConsoleDump() + { + StringBuilder builder = new StringBuilder(); + HashSet meshInfos = remesher.GetAllMeshInfos(); + builder.Append("Remesher contents:\n"); + foreach (ReMesher.MeshInfo info in meshInfos) + { + builder.Append("mat id: " + info.materialAndColor.matId + " contains [\n"); + foreach (int meshId in info.GetMeshIds()) + { + builder.Append(" " + meshId + "\n"); + } + builder.Append("]\n"); + } + builder.Append("Hidden meshes: [\n"); + foreach (int meshId in hiddenMeshes) + { + builder.Append(" " + meshId + "\n"); + } + builder.Append("]\n"); + builder.Append("Owned meshes: [\n"); + foreach (int meshId in this.renderOwners.Keys) + { + builder.Append(" " + meshId + ":" + renderOwners[meshId].ToString() + "\n"); + } + builder.Append("]\n"); + builder.Append("Previous Render Owners: [\n"); + foreach (int meshId in previousRenderOwners.Keys) + { + builder.Append(" " + meshId + ":" + previousRenderOwners[meshId].ToString() + "\n"); + } + builder.Append("]\n"); + MeshWithMaterialRenderer[] renderers = GameObject.FindObjectsOfType(); + builder.Append("Mesh with material renderers: [\n"); + foreach (MeshWithMaterialRenderer mwmr in renderers) + { + builder.Append(" MeshWithMaterialRenderer: " + mwmr.gameObject.name + "\n"); + foreach (MeshWithMaterial mesh in mwmr.meshes) + { + builder.Append(" meshmat: " + mesh.materialAndColor.matId + "\n"); + builder.Append(" mesh tricount: " + mesh.mesh.triangles.Length + "\n"); + } + } + builder.Append("]\n"); + return builder.ToString(); } - } - builder.Append("]\n"); - return builder.ToString(); } - } } diff --git a/Assets/Scripts/model/core/MoveMeshCommand.cs b/Assets/Scripts/model/core/MoveMeshCommand.cs index ad92ddbc..4792925b 100644 --- a/Assets/Scripts/model/core/MoveMeshCommand.cs +++ b/Assets/Scripts/model/core/MoveMeshCommand.cs @@ -14,32 +14,37 @@ using UnityEngine; -namespace com.google.apps.peltzer.client.model.core { +namespace com.google.apps.peltzer.client.model.core +{ - /// - /// Move a mesh to a new location. - /// - public class MoveMeshCommand : Command { - public const string COMMAND_NAME = "move"; + /// + /// Move a mesh to a new location. + /// + public class MoveMeshCommand : Command + { + public const string COMMAND_NAME = "move"; - internal readonly int meshId; - internal Vector3 positionDelta; - internal Quaternion rotDelta = Quaternion.identity; + internal readonly int meshId; + internal Vector3 positionDelta; + internal Quaternion rotDelta = Quaternion.identity; - public MoveMeshCommand(int meshId, Vector3 positionDelta, Quaternion rotDelta) { - this.meshId = meshId; - this.positionDelta = positionDelta; - this.rotDelta = rotDelta; - } + public MoveMeshCommand(int meshId, Vector3 positionDelta, Quaternion rotDelta) + { + this.meshId = meshId; + this.positionDelta = positionDelta; + this.rotDelta = rotDelta; + } - public void ApplyToModel(Model model) { - MMesh mesh = model.GetMesh(meshId); - MMesh.MoveMMesh(mesh, positionDelta, rotDelta); - model.MeshUpdated(meshId, materialsChanged:false, geometryChanged:true, vertsOrFacesChanged:false); - } + public void ApplyToModel(Model model) + { + MMesh mesh = model.GetMesh(meshId); + MMesh.MoveMMesh(mesh, positionDelta, rotDelta); + model.MeshUpdated(meshId, materialsChanged: false, geometryChanged: true, vertsOrFacesChanged: false); + } - public Command GetUndoCommand(Model model) { - return new MoveMeshCommand(meshId, -positionDelta, Quaternion.Inverse(rotDelta)); + public Command GetUndoCommand(Model model) + { + return new MoveMeshCommand(meshId, -positionDelta, Quaternion.Inverse(rotDelta)); + } } - } } diff --git a/Assets/Scripts/model/core/MoveVideoViewerCommand.cs b/Assets/Scripts/model/core/MoveVideoViewerCommand.cs index 1ed72cf0..812bd422 100644 --- a/Assets/Scripts/model/core/MoveVideoViewerCommand.cs +++ b/Assets/Scripts/model/core/MoveVideoViewerCommand.cs @@ -15,27 +15,32 @@ using com.google.apps.peltzer.client.model.main; using UnityEngine; -namespace com.google.apps.peltzer.client.model.core { - /// - /// This command moves the video viewer. It does not affect the Model. - /// - public class MoveVideoViewerCommand : Command { - private Vector3 positionDelta; - private Quaternion rotDelta = Quaternion.identity; +namespace com.google.apps.peltzer.client.model.core +{ + /// + /// This command moves the video viewer. It does not affect the Model. + /// + public class MoveVideoViewerCommand : Command + { + private Vector3 positionDelta; + private Quaternion rotDelta = Quaternion.identity; - public MoveVideoViewerCommand(Vector3 positionDelta, Quaternion rotDelta) { - this.positionDelta = positionDelta; - this.rotDelta = rotDelta; - } + public MoveVideoViewerCommand(Vector3 positionDelta, Quaternion rotDelta) + { + this.positionDelta = positionDelta; + this.rotDelta = rotDelta; + } - public void ApplyToModel(Model model) { - GameObject videoViewer = PeltzerMain.Instance.GetVideoViewer(); - videoViewer.transform.position += positionDelta; - PeltzerMain.Instance.GetVideoViewer().transform.rotation *= rotDelta; - } + public void ApplyToModel(Model model) + { + GameObject videoViewer = PeltzerMain.Instance.GetVideoViewer(); + videoViewer.transform.position += positionDelta; + PeltzerMain.Instance.GetVideoViewer().transform.rotation *= rotDelta; + } - public Command GetUndoCommand(Model model) { - return new MoveVideoViewerCommand(-positionDelta, Quaternion.Inverse(rotDelta)); + public Command GetUndoCommand(Model model) + { + return new MoveVideoViewerCommand(-positionDelta, Quaternion.Inverse(rotDelta)); + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/core/ObjectMesh.cs b/Assets/Scripts/model/core/ObjectMesh.cs index 6a8acac4..a06b270f 100644 --- a/Assets/Scripts/model/core/ObjectMesh.cs +++ b/Assets/Scripts/model/core/ObjectMesh.cs @@ -18,12 +18,14 @@ /// /// Convenience struct containing a GameObject and its mesh. /// -public class ObjectMesh { - public GameObject gameObject { get; set; } - public Mesh mesh { get; set; } +public class ObjectMesh +{ + public GameObject gameObject { get; set; } + public Mesh mesh { get; set; } - public ObjectMesh(GameObject gameObject, Mesh mesh) { - this.gameObject = gameObject; - this.mesh = mesh; - } + public ObjectMesh(GameObject gameObject, Mesh mesh) + { + this.gameObject = gameObject; + this.mesh = mesh; + } } diff --git a/Assets/Scripts/model/core/Primitives.cs b/Assets/Scripts/model/core/Primitives.cs index 30393f25..897f3096 100644 --- a/Assets/Scripts/model/core/Primitives.cs +++ b/Assets/Scripts/model/core/Primitives.cs @@ -18,20 +18,22 @@ using System.Linq; using UnityEngine; -namespace com.google.apps.peltzer.client.model.core { - - /// - /// Helper methods that generate MMeshes for geometric primitives. - /// - public class Primitives { - // Note: the Shape enum is used to index things, so it should always start at 0 and count up - // without skipping numbers. Do not define items to have arbitrary values. You will have a bad time. - public enum Shape { CONE, SPHERE, CUBE, CYLINDER, TORUS }; - private const int LINES_OF_LATITUDE = 8; - private const int LINES_OF_LONGITUDE = 12; - public static readonly int NUM_SHAPES = Enum.GetValues(typeof(Shape)).Length; - - private static readonly int[,] CUBE_POINTS = { +namespace com.google.apps.peltzer.client.model.core +{ + + /// + /// Helper methods that generate MMeshes for geometric primitives. + /// + public class Primitives + { + // Note: the Shape enum is used to index things, so it should always start at 0 and count up + // without skipping numbers. Do not define items to have arbitrary values. You will have a bad time. + public enum Shape { CONE, SPHERE, CUBE, CYLINDER, TORUS }; + private const int LINES_OF_LATITUDE = 8; + private const int LINES_OF_LONGITUDE = 12; + public static readonly int NUM_SHAPES = Enum.GetValues(typeof(Shape)).Length; + + private static readonly int[,] CUBE_POINTS = { { 0, 4, 6, 2 }, // left { 1, 3, 7, 5 }, // right { 0, 1, 5, 4 }, // bottom @@ -39,9 +41,9 @@ public enum Shape { CONE, SPHERE, CUBE, CYLINDER, TORUS }; { 0, 2, 3, 1}, // front { 4, 5, 7, 6}}; // back - private static readonly float GOLDEN_RATIO_SCALED = 0.1618033988749895f; + private static readonly float GOLDEN_RATIO_SCALED = 0.1618033988749895f; - private static readonly Vector3[] ICOSAHEDRON_POINTS = { + private static readonly Vector3[] ICOSAHEDRON_POINTS = { new Vector3(-0.1f, GOLDEN_RATIO_SCALED, 0), new Vector3(0.1f, GOLDEN_RATIO_SCALED, 0), new Vector3(-0.1f, -GOLDEN_RATIO_SCALED, 0), @@ -57,7 +59,7 @@ public enum Shape { CONE, SPHERE, CUBE, CYLINDER, TORUS }; new Vector3(-GOLDEN_RATIO_SCALED, 0, -0.1f), new Vector3(-GOLDEN_RATIO_SCALED, 0, 0.1f)}; - private static readonly List[] ICOSAHEDRON_FACES = { + private static readonly List[] ICOSAHEDRON_FACES = { // Faces around point 0. new List { 0, 11, 5 }, new List { 0, 5, 1 }, @@ -86,538 +88,580 @@ public enum Shape { CONE, SPHERE, CUBE, CYLINDER, TORUS }; new List { 8, 6, 7 }, new List { 9, 8, 1 }}; - public static MMesh BuildPrimitive(Shape shape, Vector3 scale, Vector3 offset, int id, int material) { - switch (shape) { - case Shape.CONE: - return AxisAlignedCone(id, offset, scale, material); - case Shape.CUBE: - return AxisAlignedBox(id, offset, scale, material); - case Shape.CYLINDER: - return AxisAlignedCylinder(id, offset, scale, /* holeRadius */ null, material); - case Shape.SPHERE: - return AxisAlignedUVSphere(LINES_OF_LONGITUDE, LINES_OF_LATITUDE, id, offset, scale, material); - case Shape.TORUS: - return Torus(id, offset, scale, material); - default: - return AxisAlignedBox(id, offset, scale, material); - } - } - - /// - /// Create an axis-aligned box. - /// - /// Id for the mesh. - /// Center of the box. - /// Scale of box. - /// Material id for the mesh. - /// An MMesh that renders a box. - public static MMesh AxisAlignedBox( - int id, Vector3 center, Vector3 scale, int materialId) { - FaceProperties faceProperties = new FaceProperties(materialId); - - List corners = new List(8); - - // First make the vertices. Use the first 3 binary bits of an int - // for the direction on each axis. - for (int i = 0; i < /* Corners in a cube. */ 8; i++) { - float x = (i & 1) == 0 ? -1 : 1; - float y = (i & 2) == 0 ? -1 : 1; - float z = (i & 4) == 0 ? -1 : 1; - corners.Add(new Vertex(i, new Vector3(x, y, z))); - } - - corners = new List(Math3d.ScaleVertices(corners, scale)); - Dictionary vertices = corners.ToDictionary(c => c.id); - // Create the faces based on our template. - List faces = new List(); - for (int i = 0; i < /* Faces in a cube. */ 6; i++) { - List verts = new List(); - for (int j = 0; j < /* Verts per face (i.e. rectangle). */ 4; j++) { - verts.Add(CUBE_POINTS[i, j]); + public static MMesh BuildPrimitive(Shape shape, Vector3 scale, Vector3 offset, int id, int material) + { + switch (shape) + { + case Shape.CONE: + return AxisAlignedCone(id, offset, scale, material); + case Shape.CUBE: + return AxisAlignedBox(id, offset, scale, material); + case Shape.CYLINDER: + return AxisAlignedCylinder(id, offset, scale, /* holeRadius */ null, material); + case Shape.SPHERE: + return AxisAlignedUVSphere(LINES_OF_LONGITUDE, LINES_OF_LATITUDE, id, offset, scale, material); + case Shape.TORUS: + return Torus(id, offset, scale, material); + default: + return AxisAlignedBox(id, offset, scale, material); + } } - faces.Add(new Face(i, verts.AsReadOnly(), vertices, faceProperties)); - } - return new MMesh(id, center, Quaternion.identity, vertices, faces.ToDictionary(f => f.id)); - } - /// - /// Create an axis-aligned cylinder, with 'height' on the y-axis and 'radius' on the x and z axes. - /// The ids for the start vertices will be from 0 to SLICES-1 and for the end, SLICES to SLICES*2-1. - /// Faces have ids from 0 to SLICES-1 for the outside, SLICES for the start and SLICES+1 for end. - /// - /// Id for the mesh. - /// Center of the cylinder. - /// Scale of cylinder. - /// Radius of hole inside cylinder. - /// Material id for the mesh. - /// An MMesh that renders a cylinder. - public static MMesh AxisAlignedCylinder(int id, Vector3 center, Vector3 scale, float? holeRadius, - int materialId) { - // Controls the smoothness of the cylinder, we could make this a parameter later if we wanted. - const int SLICES = 12; - const int START_OFFSET = 0; - const int END_OFFSET = SLICES; - const int INNER_START_OFFSET = SLICES * 2; - const int INNER_END_OFFSET = SLICES * 3; - - FaceProperties faceProperties = new FaceProperties(materialId); - - Dictionary vertices = new Dictionary(); - Dictionary faces = new Dictionary(); - - // Here we force 'height' to the y axis around the center. - Vector3 startLocation = new Vector3(0, -1, 0); - Vector3 endLocation = new Vector3(0, 1, 0); - - // This'll be useful when we want to add cylinders aligned to X or Z axes. - Vector3 ray = endLocation - startLocation; - - Vector3 axisZ = ray.normalized; - bool isY = (Mathf.Abs(axisZ.y) > 0.5); - Vector3 axisX = Vector3.Cross(new Vector3(isY ? 1 : 0, !isY ? 1 : 0, 0), axisZ).normalized; - Vector3 axisY = Vector3.Cross(axisX, axisZ).normalized; - - // Go around the cylinder and create all the vertices - for (int i = 0; i < SLICES; i++) { - float radians = (i / (float)SLICES) * 2 * Mathf.PI; - - Vector3 outt = axisX * Mathf.Cos(radians) + axisY * Mathf.Sin(radians); - Vector3 start = startLocation + ray + outt; - Vector3 end = startLocation + outt; - - vertices[i + START_OFFSET] = new Vertex(i + START_OFFSET, start); - vertices[i + END_OFFSET] = new Vertex(i + END_OFFSET, end); - - if (holeRadius.HasValue) { - start = startLocation + ray + outt * holeRadius.Value; - end = startLocation + outt * holeRadius.Value; - vertices[i + INNER_START_OFFSET] = new Vertex(i + INNER_START_OFFSET, start); - vertices[i + INNER_END_OFFSET] = new Vertex(i + INNER_END_OFFSET, end); + /// + /// Create an axis-aligned box. + /// + /// Id for the mesh. + /// Center of the box. + /// Scale of box. + /// Material id for the mesh. + /// An MMesh that renders a box. + public static MMesh AxisAlignedBox( + int id, Vector3 center, Vector3 scale, int materialId) + { + FaceProperties faceProperties = new FaceProperties(materialId); + + List corners = new List(8); + + // First make the vertices. Use the first 3 binary bits of an int + // for the direction on each axis. + for (int i = 0; i < /* Corners in a cube. */ 8; i++) + { + float x = (i & 1) == 0 ? -1 : 1; + float y = (i & 2) == 0 ? -1 : 1; + float z = (i & 4) == 0 ? -1 : 1; + corners.Add(new Vertex(i, new Vector3(x, y, z))); + } + + corners = new List(Math3d.ScaleVertices(corners, scale)); + Dictionary vertices = corners.ToDictionary(c => c.id); + // Create the faces based on our template. + List faces = new List(); + for (int i = 0; i < /* Faces in a cube. */ 6; i++) + { + List verts = new List(); + for (int j = 0; j < /* Verts per face (i.e. rectangle). */ 4; j++) + { + verts.Add(CUBE_POINTS[i, j]); + } + faces.Add(new Face(i, verts.AsReadOnly(), vertices, faceProperties)); + } + return new MMesh(id, center, Quaternion.identity, vertices, faces.ToDictionary(f => f.id)); } - } - vertices = - new Dictionary(Math3d.ScaleVertices(vertices.Values, scale).ToDictionary(v => v.id)); - - // Go around the cylinder again and create the outside and inside faces. - for (int i = 0; i < SLICES; i++) { - List faceVerts = new List { + /// + /// Create an axis-aligned cylinder, with 'height' on the y-axis and 'radius' on the x and z axes. + /// The ids for the start vertices will be from 0 to SLICES-1 and for the end, SLICES to SLICES*2-1. + /// Faces have ids from 0 to SLICES-1 for the outside, SLICES for the start and SLICES+1 for end. + /// + /// Id for the mesh. + /// Center of the cylinder. + /// Scale of cylinder. + /// Radius of hole inside cylinder. + /// Material id for the mesh. + /// An MMesh that renders a cylinder. + public static MMesh AxisAlignedCylinder(int id, Vector3 center, Vector3 scale, float? holeRadius, + int materialId) + { + // Controls the smoothness of the cylinder, we could make this a parameter later if we wanted. + const int SLICES = 12; + const int START_OFFSET = 0; + const int END_OFFSET = SLICES; + const int INNER_START_OFFSET = SLICES * 2; + const int INNER_END_OFFSET = SLICES * 3; + + FaceProperties faceProperties = new FaceProperties(materialId); + + Dictionary vertices = new Dictionary(); + Dictionary faces = new Dictionary(); + + // Here we force 'height' to the y axis around the center. + Vector3 startLocation = new Vector3(0, -1, 0); + Vector3 endLocation = new Vector3(0, 1, 0); + + // This'll be useful when we want to add cylinders aligned to X or Z axes. + Vector3 ray = endLocation - startLocation; + + Vector3 axisZ = ray.normalized; + bool isY = (Mathf.Abs(axisZ.y) > 0.5); + Vector3 axisX = Vector3.Cross(new Vector3(isY ? 1 : 0, !isY ? 1 : 0, 0), axisZ).normalized; + Vector3 axisY = Vector3.Cross(axisX, axisZ).normalized; + + // Go around the cylinder and create all the vertices + for (int i = 0; i < SLICES; i++) + { + float radians = (i / (float)SLICES) * 2 * Mathf.PI; + + Vector3 outt = axisX * Mathf.Cos(radians) + axisY * Mathf.Sin(radians); + Vector3 start = startLocation + ray + outt; + Vector3 end = startLocation + outt; + + vertices[i + START_OFFSET] = new Vertex(i + START_OFFSET, start); + vertices[i + END_OFFSET] = new Vertex(i + END_OFFSET, end); + + if (holeRadius.HasValue) + { + start = startLocation + ray + outt * holeRadius.Value; + end = startLocation + outt * holeRadius.Value; + vertices[i + INNER_START_OFFSET] = new Vertex(i + INNER_START_OFFSET, start); + vertices[i + INNER_END_OFFSET] = new Vertex(i + INNER_END_OFFSET, end); + } + } + + vertices = + new Dictionary(Math3d.ScaleVertices(vertices.Values, scale).ToDictionary(v => v.id)); + + // Go around the cylinder again and create the outside and inside faces. + for (int i = 0; i < SLICES; i++) + { + List faceVerts = new List { i + START_OFFSET, (i + 1) % SLICES + START_OFFSET, (i + 1) % SLICES + END_OFFSET, i + END_OFFSET }; - - faces[i] = new Face(i, faceVerts.AsReadOnly(), vertices, faceProperties); - if (holeRadius.HasValue) { - faceVerts = new List { + faces[i] = new Face(i, faceVerts.AsReadOnly(), vertices, faceProperties); + + if (holeRadius.HasValue) + { + faceVerts = new List { i + INNER_START_OFFSET, i + INNER_END_OFFSET, (i + 1) % SLICES + INNER_END_OFFSET, (i + 1) % SLICES + INNER_START_OFFSET }; - - faces[i + SLICES] = - new Face(i + SLICES, faceVerts.AsReadOnly(), vertices, faceProperties); - } - } - - // Go around one last time and create the caps. - List startVerts = new List(); - List startNorms = new List(); - List startHole = new List(); - List startHoleNorms = new List(); - List endVerts = new List(); - List endNorms = new List(); - List endHole = new List(); - List endHoleNorms = new List(); - for (int i = 0; i < SLICES; i++) { - startVerts.Add(SLICES - i - 1 + START_OFFSET); // Reverse-order. - startNorms.Add(ray.normalized); - endVerts.Add(i + END_OFFSET); - endNorms.Add(-ray.normalized); - - if (holeRadius.HasValue) { - startHole.Add(i + INNER_START_OFFSET); - startHoleNorms.Add(ray.normalized); - endHole.Add(SLICES - i - 1 + INNER_END_OFFSET); // Reverse-order. - endHoleNorms.Add(-ray.normalized); - } - } - - List startHoles = new List(); - List endHoles = new List(); - - if (holeRadius.HasValue) { - startHoles.Add(new Hole(startHole.AsReadOnly(), startHoleNorms.AsReadOnly())); - endHoles.Add(new Hole(endHole.AsReadOnly(), endHoleNorms.AsReadOnly())); - } - int faceIdOff = faces.Count; - faces[faceIdOff] = new Face( - faceIdOff, startVerts.AsReadOnly(), vertices, faceProperties); - faces[faceIdOff + 1] = new Face( - faceIdOff + 1, endVerts.AsReadOnly(), vertices, faceProperties); - - return new MMesh(id, center, Quaternion.identity, vertices, faces); - } - - /// - /// Create an axis-aligned icosphere. - /// - /// Id for the mesh. - /// Center of the sphere. - /// Scale of sphere. - /// Material id for the mesh. - /// How many times to recursively split triangles on the original icosphere. - /// An MMesh that renders an icosphere. - public static MMesh AxisAlignedIcosphere(int id, Vector3 center, Vector3 scale, int materialId, int recursionLevel = 1) { - // We won't go straight to Vertex or Face as we're going to subdivide points first. - List vertexLocations = new List(); - List> vertexIndicesForFaces = new List>(); - - // Set up outputs. - List vertices = new List(); - List faces = new List(); - FaceProperties faceProperties = new FaceProperties(materialId); - - // Create the vertices of the icosahedron. - foreach (Vector3 icosahedronPoint in ICOSAHEDRON_POINTS) { - vertexLocations.Add(icosahedronPoint.normalized); - } - - // Create the initial faces of the icosahedron. - vertexIndicesForFaces.AddRange(ICOSAHEDRON_FACES); - - // Repeatedly subdivide the faces to get a smoother mesh. - Dictionary middlePointIndexCache = new Dictionary(); - for (int i = 0; i < recursionLevel; i++) { - List> vertexIndicesForSubdividedFaces = new List>(); - foreach (List face in vertexIndicesForFaces) { - // Split each triangle into 4 smaller triangles. - int a = getMiddlePoint(face[0], face[1], ref vertexLocations, ref middlePointIndexCache, 1); - int b = getMiddlePoint(face[1], face[2], ref vertexLocations, ref middlePointIndexCache, 1); - int c = getMiddlePoint(face[2], face[0], ref vertexLocations, ref middlePointIndexCache, 1); - - vertexIndicesForSubdividedFaces.Add(new List { face[0], a, c }); - vertexIndicesForSubdividedFaces.Add(new List { face[1], b, a }); - vertexIndicesForSubdividedFaces.Add(new List { face[2], c, b }); - vertexIndicesForSubdividedFaces.Add(new List { a, b, c }); + faces[i + SLICES] = + new Face(i + SLICES, faceVerts.AsReadOnly(), vertices, faceProperties); + } + } + + // Go around one last time and create the caps. + List startVerts = new List(); + List startNorms = new List(); + List startHole = new List(); + List startHoleNorms = new List(); + List endVerts = new List(); + List endNorms = new List(); + List endHole = new List(); + List endHoleNorms = new List(); + for (int i = 0; i < SLICES; i++) + { + startVerts.Add(SLICES - i - 1 + START_OFFSET); // Reverse-order. + startNorms.Add(ray.normalized); + endVerts.Add(i + END_OFFSET); + endNorms.Add(-ray.normalized); + + if (holeRadius.HasValue) + { + startHole.Add(i + INNER_START_OFFSET); + startHoleNorms.Add(ray.normalized); + endHole.Add(SLICES - i - 1 + INNER_END_OFFSET); // Reverse-order. + endHoleNorms.Add(-ray.normalized); + } + } + + List startHoles = new List(); + List endHoles = new List(); + + if (holeRadius.HasValue) + { + startHoles.Add(new Hole(startHole.AsReadOnly(), startHoleNorms.AsReadOnly())); + endHoles.Add(new Hole(endHole.AsReadOnly(), endHoleNorms.AsReadOnly())); + } + + int faceIdOff = faces.Count; + faces[faceIdOff] = new Face( + faceIdOff, startVerts.AsReadOnly(), vertices, faceProperties); + faces[faceIdOff + 1] = new Face( + faceIdOff + 1, endVerts.AsReadOnly(), vertices, faceProperties); + + return new MMesh(id, center, Quaternion.identity, vertices, faces); } - vertexIndicesForFaces = vertexIndicesForSubdividedFaces; - } - - // Convert to our Vertex type. - foreach (Vector3 vertexLocation in vertexLocations) { - vertices.Add(new Vertex(vertices.Count, vertexLocation)); - } - - vertices = new List(Math3d.ScaleVertices(vertices, scale)); - - Dictionary vertsById = vertices.ToDictionary(v => v.id); - // Convert to our Face type. - foreach (List vertexIndices in vertexIndicesForFaces) { - AddFace(id, vertexIndices, ref faces, vertsById, faceProperties); - } - return new MMesh(id, center, Quaternion.identity, vertsById, faces.ToDictionary(f => f.id)); - } - - /// - /// Create an axis-aligned UV sphere. - /// - /// The number of vertical lines on the UV sphere. - /// The number of horizontal lines on the UV sphere. - /// Id for the mesh. - /// Center of the sphere. - /// Scale of sphere. - /// Material id for the mesh. - /// An MMesh that renders a UV sphere. - public static MMesh AxisAlignedUVSphere(int numLon, int numLat, int id, Vector3 center, Vector3 scale, - int materialId) { - // Find the number of vertices that will be on the UV sphere. This is equal to the number of intersections - // between lines of longitude and latitude plus the north and south poles. - int numVerts = (numLon * numLat) + 2; - List> vertexIndicesForFaces = new List>(); - List vertices = new List(numVerts); - - // Add the poles vertices. - vertices.Add(new Vertex(0, Vector3.up)); - vertices.Add(new Vertex(numVerts - 1, -Vector3.up)); - - // Find the position of all the other vertices. - for (int lat = 0; lat < numLat; lat++) { - float latAngle = Mathf.PI * (float)(lat + 1) / (numLat + 1); - - for (int lon = 0; lon < numLon; lon++) { - float lonAngle = 2 * Mathf.PI * (float)(lon == numLon ? 0 : lon) / numLon; - int vertexId = (lon + 1) + (lat * numLon); - - vertices.Add(new Vertex(vertexId, new Vector3( - Mathf.Sin(latAngle) * Mathf.Cos(lonAngle), - Mathf.Cos(latAngle), - Mathf.Sin(latAngle) * Mathf.Sin(lonAngle)))); + /// + /// Create an axis-aligned icosphere. + /// + /// Id for the mesh. + /// Center of the sphere. + /// Scale of sphere. + /// Material id for the mesh. + /// How many times to recursively split triangles on the original icosphere. + /// An MMesh that renders an icosphere. + public static MMesh AxisAlignedIcosphere(int id, Vector3 center, Vector3 scale, int materialId, int recursionLevel = 1) + { + // We won't go straight to Vertex or Face as we're going to subdivide points first. + List vertexLocations = new List(); + List> vertexIndicesForFaces = new List>(); + + // Set up outputs. + List vertices = new List(); + List faces = new List(); + FaceProperties faceProperties = new FaceProperties(materialId); + + // Create the vertices of the icosahedron. + foreach (Vector3 icosahedronPoint in ICOSAHEDRON_POINTS) + { + vertexLocations.Add(icosahedronPoint.normalized); + } + + // Create the initial faces of the icosahedron. + vertexIndicesForFaces.AddRange(ICOSAHEDRON_FACES); + + // Repeatedly subdivide the faces to get a smoother mesh. + Dictionary middlePointIndexCache = new Dictionary(); + for (int i = 0; i < recursionLevel; i++) + { + List> vertexIndicesForSubdividedFaces = new List>(); + foreach (List face in vertexIndicesForFaces) + { + // Split each triangle into 4 smaller triangles. + int a = getMiddlePoint(face[0], face[1], ref vertexLocations, ref middlePointIndexCache, 1); + int b = getMiddlePoint(face[1], face[2], ref vertexLocations, ref middlePointIndexCache, 1); + int c = getMiddlePoint(face[2], face[0], ref vertexLocations, ref middlePointIndexCache, 1); + + vertexIndicesForSubdividedFaces.Add(new List { face[0], a, c }); + vertexIndicesForSubdividedFaces.Add(new List { face[1], b, a }); + vertexIndicesForSubdividedFaces.Add(new List { face[2], c, b }); + vertexIndicesForSubdividedFaces.Add(new List { a, b, c }); + } + vertexIndicesForFaces = vertexIndicesForSubdividedFaces; + } + + // Convert to our Vertex type. + foreach (Vector3 vertexLocation in vertexLocations) + { + vertices.Add(new Vertex(vertices.Count, vertexLocation)); + } + + vertices = new List(Math3d.ScaleVertices(vertices, scale)); + + Dictionary vertsById = vertices.ToDictionary(v => v.id); + // Convert to our Face type. + foreach (List vertexIndices in vertexIndicesForFaces) + { + AddFace(id, vertexIndices, ref faces, vertsById, faceProperties); + } + + return new MMesh(id, center, Quaternion.identity, vertsById, faces.ToDictionary(f => f.id)); } - } - - // Determine the vertex indices for the faces that make up the top cap. - for (int lon = 1; lon <= numLon; lon++) { - vertexIndicesForFaces.Add(new List { 0, lon == numLon ? 1 : lon + 1, lon }); - } - - // Determine the vertex indices for the faces that make up the middle of the sphere. - for (int lat = 0; lat < numLat - 1; lat++) { - for (int lon = 1; lon <= numLon; lon++) { - int current = lon + (lat * numLon); - int next = lon == numLon ? 1 + (lat * numLon) : current + 1; - - vertexIndicesForFaces.Add(new List { current, next, next + numLon, current + numLon }); - } - } - - // Find the index for the south pole or final vertex. - int final = numVerts - 1; - // Determine the vertex indices for the faces that make up the bottom cap. - for (int lon = 0; lon < numLon; lon++) { - vertexIndicesForFaces.Add(new List { + /// + /// Create an axis-aligned UV sphere. + /// + /// The number of vertical lines on the UV sphere. + /// The number of horizontal lines on the UV sphere. + /// Id for the mesh. + /// Center of the sphere. + /// Scale of sphere. + /// Material id for the mesh. + /// An MMesh that renders a UV sphere. + public static MMesh AxisAlignedUVSphere(int numLon, int numLat, int id, Vector3 center, Vector3 scale, + int materialId) + { + // Find the number of vertices that will be on the UV sphere. This is equal to the number of intersections + // between lines of longitude and latitude plus the north and south poles. + int numVerts = (numLon * numLat) + 2; + List> vertexIndicesForFaces = new List>(); + List vertices = new List(numVerts); + + // Add the poles vertices. + vertices.Add(new Vertex(0, Vector3.up)); + vertices.Add(new Vertex(numVerts - 1, -Vector3.up)); + + // Find the position of all the other vertices. + for (int lat = 0; lat < numLat; lat++) + { + float latAngle = Mathf.PI * (float)(lat + 1) / (numLat + 1); + + for (int lon = 0; lon < numLon; lon++) + { + float lonAngle = 2 * Mathf.PI * (float)(lon == numLon ? 0 : lon) / numLon; + int vertexId = (lon + 1) + (lat * numLon); + + vertices.Add(new Vertex(vertexId, new Vector3( + Mathf.Sin(latAngle) * Mathf.Cos(lonAngle), + Mathf.Cos(latAngle), + Mathf.Sin(latAngle) * Mathf.Sin(lonAngle)))); + } + } + + // Determine the vertex indices for the faces that make up the top cap. + for (int lon = 1; lon <= numLon; lon++) + { + vertexIndicesForFaces.Add(new List { 0, lon == numLon ? 1 : lon + 1, lon }); + } + + // Determine the vertex indices for the faces that make up the middle of the sphere. + for (int lat = 0; lat < numLat - 1; lat++) + { + for (int lon = 1; lon <= numLon; lon++) + { + int current = lon + (lat * numLon); + int next = lon == numLon ? 1 + (lat * numLon) : current + 1; + + vertexIndicesForFaces.Add(new List { current, next, next + numLon, current + numLon }); + } + } + + // Find the index for the south pole or final vertex. + int final = numVerts - 1; + + // Determine the vertex indices for the faces that make up the bottom cap. + for (int lon = 0; lon < numLon; lon++) + { + vertexIndicesForFaces.Add(new List { (final - numLon) + lon, lon == numLon - 1 ? final - numLon : (final - numLon) + (lon + 1), final}); - } + } - // Scale the vertices. - vertices = new List(Math3d.ScaleVertices(vertices, scale)); + // Scale the vertices. + vertices = new List(Math3d.ScaleVertices(vertices, scale)); - // Set up the properties to convert to our face type. - List faces = new List(); - FaceProperties faceProperties = new FaceProperties(materialId); - Dictionary vertsById = vertices.ToDictionary(v => v.id); + // Set up the properties to convert to our face type. + List faces = new List(); + FaceProperties faceProperties = new FaceProperties(materialId); + Dictionary vertsById = vertices.ToDictionary(v => v.id); - // Convert to our Face type. - foreach (List vertexIndices in vertexIndicesForFaces) { - AddFace(id, vertexIndices, ref faces, vertsById, faceProperties); - } + // Convert to our Face type. + foreach (List vertexIndices in vertexIndicesForFaces) + { + AddFace(id, vertexIndices, ref faces, vertsById, faceProperties); + } - return new MMesh(id, center, Quaternion.identity, vertsById, faces.ToDictionary(f => f.id)); - } + return new MMesh(id, center, Quaternion.identity, vertsById, faces.ToDictionary(f => f.id)); + } - /// - /// Create a Face from the given constituents. - /// - /// A list of indices into 'vertices' representing the vertices of this face. - /// A master list of vertices into which 'vertexIds' will index. - /// A list of faces to which the new list will be appended. - /// Properties for this face. - private static void AddFace(int meshId, List vertexIds, - ref List faces, Dictionary vertsById, FaceProperties faceProperties) { - faces.Add(new Face(faces.Count, vertexIds.AsReadOnly(), vertsById, faceProperties)); - } + /// + /// Create a Face from the given constituents. + /// + /// A list of indices into 'vertices' representing the vertices of this face. + /// A master list of vertices into which 'vertexIds' will index. + /// A list of faces to which the new list will be appended. + /// Properties for this face. + private static void AddFace(int meshId, List vertexIds, + ref List faces, Dictionary vertsById, FaceProperties faceProperties) + { + faces.Add(new Face(faces.Count, vertexIds.AsReadOnly(), vertsById, faceProperties)); + } - /// - /// Either adds to the given list of vertices, or finds in a cache, the middle point between two given points, - /// and returns the index of this midpoint. - /// - /// An index into 'vertices' representing a point on the icosphere. - /// A second index into 'vertices' representing a different point on the icosphere. - /// The master list of vertices in the icosphere. - /// A cache of points, for quick lookup. - /// The radius of the icosphere being created. - /// An index into 'vertices' pointing to the midpoint. - private static int getMiddlePoint(int p1, int p2, ref List vertices, - ref Dictionary cache, float radius) { - // First try the cache. - bool firstIsSmaller = p1 < p2; - long smallerIndex = firstIsSmaller ? p1 : p2; - long greaterIndex = firstIsSmaller ? p2 : p1; - long key = (smallerIndex << 32) + greaterIndex; - int ret; - if (cache.TryGetValue(key, out ret)) { - return ret; - } - - // If not found, calculate it. - Vector3 point1 = vertices[p1]; - Vector3 point2 = vertices[p2]; - Vector3 middle = new Vector3( - (point1.x + point2.x) / 2f, - (point1.y + point2.y) / 2f, - (point1.z + point2.z) / 2f); - - // Add the midpoint, ensuring it is on the sphere's circumference. - int i = vertices.Count; - vertices.Add(middle.normalized * radius); - - // Add it to the cache and return. - cache.Add(key, i); - return i; - } + /// + /// Either adds to the given list of vertices, or finds in a cache, the middle point between two given points, + /// and returns the index of this midpoint. + /// + /// An index into 'vertices' representing a point on the icosphere. + /// A second index into 'vertices' representing a different point on the icosphere. + /// The master list of vertices in the icosphere. + /// A cache of points, for quick lookup. + /// The radius of the icosphere being created. + /// An index into 'vertices' pointing to the midpoint. + private static int getMiddlePoint(int p1, int p2, ref List vertices, + ref Dictionary cache, float radius) + { + // First try the cache. + bool firstIsSmaller = p1 < p2; + long smallerIndex = firstIsSmaller ? p1 : p2; + long greaterIndex = firstIsSmaller ? p2 : p1; + long key = (smallerIndex << 32) + greaterIndex; + int ret; + if (cache.TryGetValue(key, out ret)) + { + return ret; + } + + // If not found, calculate it. + Vector3 point1 = vertices[p1]; + Vector3 point2 = vertices[p2]; + Vector3 middle = new Vector3( + (point1.x + point2.x) / 2f, + (point1.y + point2.y) / 2f, + (point1.z + point2.z) / 2f); + + // Add the midpoint, ensuring it is on the sphere's circumference. + int i = vertices.Count; + vertices.Add(middle.normalized * radius); + + // Add it to the cache and return. + cache.Add(key, i); + return i; + } - /// - /// Make an axis-aligned cone. - /// - /// ID for newly created mesh. - /// Center of cone. - /// Scale of cone. - /// Cone's material. - /// A new mesh. - public static MMesh AxisAlignedCone(int id, Vector3 center, Vector3 scale, int materialId) { - const int SLICES = 12; - const int TOP_VERT_ID = SLICES; - - FaceProperties properties = new FaceProperties(materialId); - - Vector3 top = new Vector3(0, 1, 0); - Vector3 bottomCenter = -top; - - Dictionary vertices = new Dictionary(); - Dictionary faces = new Dictionary(); - - // Go around circle, add points. - for (int i = 0; i < SLICES; i++) { - float radians = (i / (float)SLICES) * 2 * Mathf.PI; - vertices[i] = new Vertex(i, - new Vector3(Mathf.Cos(radians), -1, Mathf.Sin(radians))); - } - vertices[TOP_VERT_ID] = new Vertex(TOP_VERT_ID, top); - - vertices = - new Dictionary(Math3d.ScaleVertices(vertices.Values, scale).ToDictionary(v => v.id)); - - // Go around again and create faces. Each face is a triangle. - for (int i = 0; i < SLICES; i++) { - List vertIds = new List() { TOP_VERT_ID, (i + 1) % SLICES, i }; - faces[i] = new Face(i, vertIds.AsReadOnly(), vertices, properties); - } - - // Go around one last time and create the bottom face. - List baseVertIds = new List(); - for (int i = 0; i < SLICES; i++) { - baseVertIds.Add(i); - } - faces[SLICES + 1] = new Face(SLICES + 1, baseVertIds.AsReadOnly(), vertices, properties); - - return new MMesh(id, center, Quaternion.identity, vertices, faces); - } + /// + /// Make an axis-aligned cone. + /// + /// ID for newly created mesh. + /// Center of cone. + /// Scale of cone. + /// Cone's material. + /// A new mesh. + public static MMesh AxisAlignedCone(int id, Vector3 center, Vector3 scale, int materialId) + { + const int SLICES = 12; + const int TOP_VERT_ID = SLICES; + + FaceProperties properties = new FaceProperties(materialId); + + Vector3 top = new Vector3(0, 1, 0); + Vector3 bottomCenter = -top; + + Dictionary vertices = new Dictionary(); + Dictionary faces = new Dictionary(); + + // Go around circle, add points. + for (int i = 0; i < SLICES; i++) + { + float radians = (i / (float)SLICES) * 2 * Mathf.PI; + vertices[i] = new Vertex(i, + new Vector3(Mathf.Cos(radians), -1, Mathf.Sin(radians))); + } + vertices[TOP_VERT_ID] = new Vertex(TOP_VERT_ID, top); + + vertices = + new Dictionary(Math3d.ScaleVertices(vertices.Values, scale).ToDictionary(v => v.id)); + + // Go around again and create faces. Each face is a triangle. + for (int i = 0; i < SLICES; i++) + { + List vertIds = new List() { TOP_VERT_ID, (i + 1) % SLICES, i }; + faces[i] = new Face(i, vertIds.AsReadOnly(), vertices, properties); + } + + // Go around one last time and create the bottom face. + List baseVertIds = new List(); + for (int i = 0; i < SLICES; i++) + { + baseVertIds.Add(i); + } + faces[SLICES + 1] = new Face(SLICES + 1, baseVertIds.AsReadOnly(), vertices, properties); + + return new MMesh(id, center, Quaternion.identity, vertices, faces); + } - /// - /// Create an axis-aligned triangle. - /// - /// Id for newly created mesh. - /// Center of triangle. - /// Scale of triangular pyramid. - /// Material id for mesh. - /// A new mesh. - public static MMesh TriangularPyramid(int id, Vector3 center, Vector3 scale, int materialId) { - const int topId = 0; - const int bottomLeftId = 1; - const int bottomRightId = 2; - const int bottomPointId = 3; - - FaceProperties properties = new FaceProperties(materialId); - - Vector3 top = new Vector3(0, 1, 0); - Vector3 bottomCenter = -top; - - Dictionary vertices = new Dictionary(); - Dictionary faces = new Dictionary(); - - float r = 1; - float twoPiRadOverThree = Mathf.PI / 1.5f; - vertices[topId] = new Vertex(topId, new Vector3(0, 1, 0)); - vertices[bottomLeftId] = new Vertex(bottomLeftId, - new Vector3(Mathf.Cos(twoPiRadOverThree) * r, -1, Mathf.Sin(twoPiRadOverThree) * r)); - vertices[bottomRightId] = new Vertex(bottomRightId, - new Vector3(Mathf.Cos(twoPiRadOverThree * 2) * r, -1, Mathf.Sin(twoPiRadOverThree * 2) * r)); - vertices[bottomPointId] = new Vertex(bottomPointId, - new Vector3(Mathf.Cos(0) * r, -1, Mathf.Sin(0) * r)); - - vertices = new Dictionary(Math3d.ScaleVertices(vertices.Values, scale).ToDictionary(v => v.id)); - - List dummyNormals = new List() { Vector3.one, Vector3.one, Vector3.one }; - - - List> faceIndices = new List>() { + /// + /// Create an axis-aligned triangle. + /// + /// Id for newly created mesh. + /// Center of triangle. + /// Scale of triangular pyramid. + /// Material id for mesh. + /// A new mesh. + public static MMesh TriangularPyramid(int id, Vector3 center, Vector3 scale, int materialId) + { + const int topId = 0; + const int bottomLeftId = 1; + const int bottomRightId = 2; + const int bottomPointId = 3; + + FaceProperties properties = new FaceProperties(materialId); + + Vector3 top = new Vector3(0, 1, 0); + Vector3 bottomCenter = -top; + + Dictionary vertices = new Dictionary(); + Dictionary faces = new Dictionary(); + + float r = 1; + float twoPiRadOverThree = Mathf.PI / 1.5f; + vertices[topId] = new Vertex(topId, new Vector3(0, 1, 0)); + vertices[bottomLeftId] = new Vertex(bottomLeftId, + new Vector3(Mathf.Cos(twoPiRadOverThree) * r, -1, Mathf.Sin(twoPiRadOverThree) * r)); + vertices[bottomRightId] = new Vertex(bottomRightId, + new Vector3(Mathf.Cos(twoPiRadOverThree * 2) * r, -1, Mathf.Sin(twoPiRadOverThree * 2) * r)); + vertices[bottomPointId] = new Vertex(bottomPointId, + new Vector3(Mathf.Cos(0) * r, -1, Mathf.Sin(0) * r)); + + vertices = new Dictionary(Math3d.ScaleVertices(vertices.Values, scale).ToDictionary(v => v.id)); + + List dummyNormals = new List() { Vector3.one, Vector3.one, Vector3.one }; + + + List> faceIndices = new List>() { new List() { 0, 2, 1 }, new List() { 0, 1, 3 }, new List() { 0, 3, 2 }, new List() { 1, 2, 3 } }; - for (int i = 0; i < faceIndices.Count; i++) { - faces[i] = new Face(i, - faceIndices[i].AsReadOnly(), - vertices, - properties); - } + for (int i = 0; i < faceIndices.Count; i++) + { + faces[i] = new Face(i, + faceIndices[i].AsReadOnly(), + vertices, + properties); + } - return new MMesh(id, center, Quaternion.identity, vertices, faces); - } - - - /// - /// Make a torus-shaped mesh. All of the faces an ids are generated by mapping an x axis - /// around the torus and y axis around a slice of the torus. - /// - /// The new mesh's id. - /// Center of the torus. - /// Scale of the torus. - /// Material for the mesh. - /// A new mesh. - public static MMesh Torus(int id, Vector3 center, Vector3 scale, int materialId) { - const int SLICES = 12; - - Vector3 up = new Vector3(0, 1, 0); - - float outerRadius = 1; - float innerRadius = 0.5f; - - float donutRadius = (outerRadius - innerRadius) / 2f; - float centerlineRadius = outerRadius - donutRadius; - - FaceProperties properties = new FaceProperties(materialId); - - Dictionary vertices = new Dictionary(); - Dictionary faces = new Dictionary(); - - // Generate all of the surface points and normals. - List normals = new List(SLICES * SLICES); - for (int i = 0; i < SLICES; i++) { - float outerRads = (i / (float)SLICES) * 2 * Mathf.PI; - for (int j = 0; j < SLICES; j++) { - float innerRads = (j / (float)SLICES) * 2 * Mathf.PI; - - Vector3 centerLinePoint = new Vector3( - Mathf.Cos(outerRads) * centerlineRadius, 0, Mathf.Sin(outerRads) * centerlineRadius); - Vector3 dir = (centerLinePoint - Vector3.zero).normalized; + return new MMesh(id, center, Quaternion.identity, vertices, faces); + } - Vector3 surfacePoint = centerLinePoint + - up * Mathf.Cos(innerRads) * donutRadius + - dir * Mathf.Sin(innerRads) * donutRadius; - vertices[i * SLICES + j] = new Vertex(i * SLICES + j, surfacePoint); - normals.Add((surfacePoint-centerLinePoint).normalized); - } - } - - // Scale. - vertices = new Dictionary(Math3d.ScaleVertices(vertices.Values, scale).ToDictionary(v => v.id)); - - // Now generate the faces. - for (int i = 0; i < SLICES; i++) { - for (int j = 0; j < SLICES; j++) { - int idx1 = i * SLICES + j; - int idx2 = ((i + 1) % SLICES) * SLICES + j; - int idx3 = ((i + 1) % SLICES) * SLICES + (j + 1) % SLICES; - int idx4 = i * SLICES + (j + 1) % SLICES; - - faces[idx1] = new Face(idx1, - new List() { idx1, idx2, idx3, idx4 }.AsReadOnly(), - vertices, - properties); + /// + /// Make a torus-shaped mesh. All of the faces an ids are generated by mapping an x axis + /// around the torus and y axis around a slice of the torus. + /// + /// The new mesh's id. + /// Center of the torus. + /// Scale of the torus. + /// Material for the mesh. + /// A new mesh. + public static MMesh Torus(int id, Vector3 center, Vector3 scale, int materialId) + { + const int SLICES = 12; + + Vector3 up = new Vector3(0, 1, 0); + + float outerRadius = 1; + float innerRadius = 0.5f; + + float donutRadius = (outerRadius - innerRadius) / 2f; + float centerlineRadius = outerRadius - donutRadius; + + FaceProperties properties = new FaceProperties(materialId); + + Dictionary vertices = new Dictionary(); + Dictionary faces = new Dictionary(); + + // Generate all of the surface points and normals. + List normals = new List(SLICES * SLICES); + for (int i = 0; i < SLICES; i++) + { + float outerRads = (i / (float)SLICES) * 2 * Mathf.PI; + for (int j = 0; j < SLICES; j++) + { + float innerRads = (j / (float)SLICES) * 2 * Mathf.PI; + + Vector3 centerLinePoint = new Vector3( + Mathf.Cos(outerRads) * centerlineRadius, 0, Mathf.Sin(outerRads) * centerlineRadius); + Vector3 dir = (centerLinePoint - Vector3.zero).normalized; + + Vector3 surfacePoint = centerLinePoint + + up * Mathf.Cos(innerRads) * donutRadius + + dir * Mathf.Sin(innerRads) * donutRadius; + + vertices[i * SLICES + j] = new Vertex(i * SLICES + j, surfacePoint); + normals.Add((surfacePoint - centerLinePoint).normalized); + } + } + + // Scale. + vertices = new Dictionary(Math3d.ScaleVertices(vertices.Values, scale).ToDictionary(v => v.id)); + + // Now generate the faces. + for (int i = 0; i < SLICES; i++) + { + for (int j = 0; j < SLICES; j++) + { + int idx1 = i * SLICES + j; + int idx2 = ((i + 1) % SLICES) * SLICES + j; + int idx3 = ((i + 1) % SLICES) * SLICES + (j + 1) % SLICES; + int idx4 = i * SLICES + (j + 1) % SLICES; + + faces[idx1] = new Face(idx1, + new List() { idx1, idx2, idx3, idx4 }.AsReadOnly(), + vertices, + properties); + } + } + + return new MMesh(id, center, Quaternion.identity, vertices, faces); } - } - - return new MMesh(id, center, Quaternion.identity, vertices, faces); } - } } diff --git a/Assets/Scripts/model/core/ReplaceMeshCommand.cs b/Assets/Scripts/model/core/ReplaceMeshCommand.cs index db6780ce..66046348 100644 --- a/Assets/Scripts/model/core/ReplaceMeshCommand.cs +++ b/Assets/Scripts/model/core/ReplaceMeshCommand.cs @@ -14,19 +14,22 @@ using System.Collections.Generic; -namespace com.google.apps.peltzer.client.model.core { - public class ReplaceMeshCommand : CompositeCommand { - public new const string COMMAND_NAME = "replace"; +namespace com.google.apps.peltzer.client.model.core +{ + public class ReplaceMeshCommand : CompositeCommand + { + public new const string COMMAND_NAME = "replace"; - public readonly int meshId; - private readonly MMesh mesh; + public readonly int meshId; + private readonly MMesh mesh; - public ReplaceMeshCommand(int meshId, MMesh mesh) : base(new List() { + public ReplaceMeshCommand(int meshId, MMesh mesh) : base(new List() { new DeleteMeshCommand(meshId), new AddMeshCommand(mesh) - }) { - this.meshId = meshId; - this.mesh = mesh; + }) + { + this.meshId = meshId; + this.mesh = mesh; + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/core/SetMeshGroupsCommand.cs b/Assets/Scripts/model/core/SetMeshGroupsCommand.cs index d8f7d081..3de439ee 100644 --- a/Assets/Scripts/model/core/SetMeshGroupsCommand.cs +++ b/Assets/Scripts/model/core/SetMeshGroupsCommand.cs @@ -15,89 +15,102 @@ using System.Collections.Generic; using System.Linq; -namespace com.google.apps.peltzer.client.model.core { - /// - /// Assigns given meshes to given groups. This command is designed to be generic so that it - /// can do and undo grouping and ungrouping operations. It expresses the operation as a - /// set of "assignments", each of which given by a mesh ID, a "from" group and a "to" group. - /// - public class SetMeshGroupsCommand : Command { - public const string COMMAND_NAME = "setMeshGroups"; +namespace com.google.apps.peltzer.client.model.core +{ + /// + /// Assigns given meshes to given groups. This command is designed to be generic so that it + /// can do and undo grouping and ungrouping operations. It expresses the operation as a + /// set of "assignments", each of which given by a mesh ID, a "from" group and a "to" group. + /// + public class SetMeshGroupsCommand : Command + { + public const string COMMAND_NAME = "setMeshGroups"; - // The list of assignments that comprise this command. - public readonly List assignments; + // The list of assignments that comprise this command. + public readonly List assignments; - // Used internally. Users of this class should use one of the static creation methods below - // to create a SetMeshGroupsCommand that's appropriate for each use case. - private SetMeshGroupsCommand(IEnumerable assignments) { - this.assignments = new List(assignments); - } + // Used internally. Users of this class should use one of the static creation methods below + // to create a SetMeshGroupsCommand that's appropriate for each use case. + private SetMeshGroupsCommand(IEnumerable assignments) + { + this.assignments = new List(assignments); + } - /// - /// Creates a SetMeshGroupsCommand that groups the given meshes into a new group. - /// - /// The model. - /// The meshes to group together - /// - public static SetMeshGroupsCommand CreateGroupMeshesCommand(Model model, IEnumerable meshIds) { - int newGroupId = model.GenerateGroupId(); - // Assign each mesh to the new group. - List groups = new List(meshIds.Count()); - foreach (int meshId in meshIds) { - groups.Add(new GroupAssignment(meshId, model.GetMesh(meshId).groupId, newGroupId)); - } - return new SetMeshGroupsCommand(groups); - } + /// + /// Creates a SetMeshGroupsCommand that groups the given meshes into a new group. + /// + /// The model. + /// The meshes to group together + /// + public static SetMeshGroupsCommand CreateGroupMeshesCommand(Model model, IEnumerable meshIds) + { + int newGroupId = model.GenerateGroupId(); + // Assign each mesh to the new group. + List groups = new List(meshIds.Count()); + foreach (int meshId in meshIds) + { + groups.Add(new GroupAssignment(meshId, model.GetMesh(meshId).groupId, newGroupId)); + } + return new SetMeshGroupsCommand(groups); + } - /// - /// Creates a SetMeshGroupsCommand that ungroups the given meshes. - /// - /// The model. - /// The meshes to ungroup. - /// - public static SetMeshGroupsCommand CreateUngroupMeshesCommand(Model model, IEnumerable meshIds) { - // Assign each mesh to the MMesh.GROUP_NONE group. - List groups = new List(meshIds.Count()); - foreach (int meshId in meshIds) { - groups.Add(new GroupAssignment(meshId, model.GetMesh(meshId).groupId, MMesh.GROUP_NONE)); - } - return new SetMeshGroupsCommand(groups); - } + /// + /// Creates a SetMeshGroupsCommand that ungroups the given meshes. + /// + /// The model. + /// The meshes to ungroup. + /// + public static SetMeshGroupsCommand CreateUngroupMeshesCommand(Model model, IEnumerable meshIds) + { + // Assign each mesh to the MMesh.GROUP_NONE group. + List groups = new List(meshIds.Count()); + foreach (int meshId in meshIds) + { + groups.Add(new GroupAssignment(meshId, model.GetMesh(meshId).groupId, MMesh.GROUP_NONE)); + } + return new SetMeshGroupsCommand(groups); + } - public void ApplyToModel(Model model) { - // Assign each mesh to the prescribed group. - foreach (GroupAssignment assignment in assignments) { - model.SetMeshGroup(assignment.meshId, assignment.toGroupId); - } - } + public void ApplyToModel(Model model) + { + // Assign each mesh to the prescribed group. + foreach (GroupAssignment assignment in assignments) + { + model.SetMeshGroup(assignment.meshId, assignment.toGroupId); + } + } - public Command GetUndoCommand(Model model) { - // To undo the command, we just invert the "to" and "from" groups. - return new SetMeshGroupsCommand(assignments.Select(assignment => assignment.Reversed())); - } + public Command GetUndoCommand(Model model) + { + // To undo the command, we just invert the "to" and "from" groups. + return new SetMeshGroupsCommand(assignments.Select(assignment => assignment.Reversed())); + } - /// - /// Represents each assignment in the command. An assignment represents the fact that we - /// have to assign one particular mesh from one group to another. - /// - public class GroupAssignment { - // The mesh to reassign. - public int meshId; - // The mesh's original group. - public int fromGroupId; - // The mesh's new group. - public int toGroupId; + /// + /// Represents each assignment in the command. An assignment represents the fact that we + /// have to assign one particular mesh from one group to another. + /// + public class GroupAssignment + { + // The mesh to reassign. + public int meshId; + // The mesh's original group. + public int fromGroupId; + // The mesh's new group. + public int toGroupId; - public GroupAssignment(int meshId, int fromGroup, int toGroup) { - this.meshId = meshId; - this.fromGroupId = fromGroup; - this.toGroupId = toGroup; - } + public GroupAssignment(int meshId, int fromGroup, int toGroup) + { + this.meshId = meshId; + this.fromGroupId = fromGroup; + this.toGroupId = toGroup; + } - // Returns the reverse assignment (with from and to flipped). - public GroupAssignment Reversed() { - return new GroupAssignment(meshId, toGroupId, fromGroupId); - } + // Returns the reverse assignment (with from and to flipped). + public GroupAssignment Reversed() + { + return new GroupAssignment(meshId, toGroupId, fromGroupId); + } + } } - } } diff --git a/Assets/Scripts/model/core/ShowVideoViewerCommand.cs b/Assets/Scripts/model/core/ShowVideoViewerCommand.cs index 6db0550c..9478694d 100644 --- a/Assets/Scripts/model/core/ShowVideoViewerCommand.cs +++ b/Assets/Scripts/model/core/ShowVideoViewerCommand.cs @@ -17,20 +17,24 @@ using com.google.apps.peltzer.video; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.core { - /// - /// This command makes the video viewer visible. It does not affect the Model. - /// - public class ShowVideoViewerCommand : Command { - public void ApplyToModel(Model model) { - // Set the video viewer active and set it up to be moveable. - GameObject videoViewer = PeltzerMain.Instance.GetVideoViewer(); - videoViewer.SetActive(true); - videoViewer.GetComponent().Setup(); - } +namespace com.google.apps.peltzer.client.model.core +{ + /// + /// This command makes the video viewer visible. It does not affect the Model. + /// + public class ShowVideoViewerCommand : Command + { + public void ApplyToModel(Model model) + { + // Set the video viewer active and set it up to be moveable. + GameObject videoViewer = PeltzerMain.Instance.GetVideoViewer(); + videoViewer.SetActive(true); + videoViewer.GetComponent().Setup(); + } - public Command GetUndoCommand(Model model) { - return new HideVideoViewerCommand(); + public Command GetUndoCommand(Model model) + { + return new HideVideoViewerCommand(); + } } - } } diff --git a/Assets/Scripts/model/core/SmoothMoves.cs b/Assets/Scripts/model/core/SmoothMoves.cs index 8279a614..46404c20 100644 --- a/Assets/Scripts/model/core/SmoothMoves.cs +++ b/Assets/Scripts/model/core/SmoothMoves.cs @@ -16,357 +16,409 @@ using com.google.apps.peltzer.client.tools.utils; using UnityEngine; -namespace com.google.apps.peltzer.client.model.core { - public class SmoothScale { - public Vector3 scaleFactor; - // If true, we are doing a scaling animation. - public bool scaleAnimActive; - // If scaleAnimActive == true, this indicates the initial scale we are animating from. - public float scaleAnimStartScale; - // If scaleAnimActive == true, this indicates the time when the scaling animation began (as in Time.time). - public float scaleAnimStartTime; - // If scaleAnimActive == true, this indicates the duration of the scale animation. - public float scaleAnimDuration; - // Duration of the scale animation, in seconds. - private const float SCALE_ANIM_DURATION = 0.10f; - - public SmoothScale() { - scaleFactor = Vector3.one; - scaleAnimActive = false; - scaleAnimStartScale = 1f; - scaleAnimStartTime = 0f; - scaleAnimDuration = 0f; +namespace com.google.apps.peltzer.client.model.core +{ + public class SmoothScale + { + public Vector3 scaleFactor; + // If true, we are doing a scaling animation. + public bool scaleAnimActive; + // If scaleAnimActive == true, this indicates the initial scale we are animating from. + public float scaleAnimStartScale; + // If scaleAnimActive == true, this indicates the time when the scaling animation began (as in Time.time). + public float scaleAnimStartTime; + // If scaleAnimActive == true, this indicates the duration of the scale animation. + public float scaleAnimDuration; + // Duration of the scale animation, in seconds. + private const float SCALE_ANIM_DURATION = 0.10f; + + public SmoothScale() + { + scaleFactor = Vector3.one; + scaleAnimActive = false; + scaleAnimStartScale = 1f; + scaleAnimStartTime = 0f; + scaleAnimDuration = 0f; + } + + public SmoothScale(SmoothScale source) + { + scaleFactor = source.scaleFactor; + scaleAnimActive = source.scaleAnimActive; + scaleAnimStartScale = source.scaleAnimStartScale; + scaleAnimStartTime = source.scaleAnimStartTime; + scaleAnimDuration = source.scaleAnimDuration; + } + + public void Update() + { + scaleFactor = Vector3.one; + if (scaleAnimActive) + { + scaleFactor = GetCurrentAnimatedScale(); + scaleAnimActive = scaleAnimActive && (Time.time - scaleAnimStartTime < scaleAnimDuration); + } + } + + public Vector3 GetScale() + { + return scaleFactor; + } + + /// + /// Animates this object's displayed scale from the given scale to the default scale (1.0f). + /// + /// Old scale factor. + public void AnimateScaleFrom(float fromScale) + { + scaleAnimActive = true; + scaleAnimStartScale = fromScale; + scaleAnimDuration = SCALE_ANIM_DURATION; + scaleAnimStartTime = Time.time; + } + + /// + /// Returns the current animated scale, that is, the scale at which this object is currently being displayed + /// for animation purposes. + /// + /// The current animated scale. + private Vector3 GetCurrentAnimatedScale() + { + if (!scaleAnimActive) return Vector3.one; + float elapsed = Time.time - scaleAnimStartTime; + float factor = scaleAnimStartScale + Mathf.Sqrt(Mathf.Clamp01(elapsed / scaleAnimDuration)) * + (1.0f - scaleAnimStartScale); + return factor * Vector3.one; + } } - public SmoothScale(SmoothScale source) { - scaleFactor = source.scaleFactor; - scaleAnimActive = source.scaleAnimActive; - scaleAnimStartScale = source.scaleAnimStartScale; - scaleAnimStartTime = source.scaleAnimStartTime; - scaleAnimDuration = source.scaleAnimDuration; - } - - public void Update() { - scaleFactor = Vector3.one; - if (scaleAnimActive) { - scaleFactor = GetCurrentAnimatedScale(); - scaleAnimActive = scaleAnimActive && (Time.time - scaleAnimStartTime < scaleAnimDuration); - } - } - - public Vector3 GetScale() { - return scaleFactor; - } - - /// - /// Animates this object's displayed scale from the given scale to the default scale (1.0f). - /// - /// Old scale factor. - public void AnimateScaleFrom(float fromScale) { - scaleAnimActive = true; - scaleAnimStartScale = fromScale; - scaleAnimDuration = SCALE_ANIM_DURATION; - scaleAnimStartTime = Time.time; - } - - /// - /// Returns the current animated scale, that is, the scale at which this object is currently being displayed - /// for animation purposes. - /// - /// The current animated scale. - private Vector3 GetCurrentAnimatedScale() { - if (!scaleAnimActive) return Vector3.one; - float elapsed = Time.time - scaleAnimStartTime; - float factor = scaleAnimStartScale + Mathf.Sqrt(Mathf.Clamp01(elapsed / scaleAnimDuration)) * - (1.0f - scaleAnimStartScale); - return factor * Vector3.one; - } - } - - public class SmoothMoves { - // Smoothing settings: - - // How long, in seconds, it takes for the displayed position to catch up with the real position. - private const float DISPLAY_CATCH_UP_TIME = 0.05f; - // Minimum speed at which displayed position catches up with the real position. - private const float MIN_CATCH_UP_SPEED = 0.1f; - // How close the displayed position has to be to the real position for us to end the animation. - private const float DIST_TO_TARGET_EPSILON = 0.001f; - - private Vector3 _positionModelSpace; - /// - /// Used to disable linear interpolation of display position. Primary use case is when a user of - /// MeshWithMaterialRenderer is handling a transform on its own where linear interpolation would result - /// in incorrect output - rotating the mesh by its parent transform, for example. - /// - bool preventDisplayPositionUpdate = false; - - // Optional position in model space. For things that are not parented to game objects (like selections) - // it makes more sense to specify their location in model space. - // Setting this position will result in a hard update (no smoothing). If smoothing is desired, use the - // SetPositionModelSpace method and indicate your desire to have the motion smoothed. - public Vector3 positionModelSpace { - get { - return _positionModelSpace; - } - set { - // Setting this property is a hard update (not smoothed). - SetPositionModelSpace(value, false /* smooth */); - } - } - - private Quaternion _orientationModelSpace; - - // Optional orientation in model space. - // Setting is private to enforce use of SetOrientationModelSpace as it makes intent clearer. - public Quaternion orientationModelSpace { - get { - return _orientationModelSpace; - } - private set { - // Setting this property is a hard update (not smoothed). - SetOrientationModelSpace(value, false /* smooth */); - } - } - - // For smooth animation, this is the position at which we are currently displaying this object. - // We update this each frame to chase after the real position. - private Vector3? displayPositionModelSpace; - - // A wrapper for model space orientation that manages spherical linear interpolation of orientation changes. - // This is updated every frame. - private Slerpee displayOrientationWrapperModelSpace; - - private SmoothScale smoothScale; - - private WorldSpace worldSpace; - - public SmoothMoves(WorldSpace worldSpace, Vector3 startingPositionModel, Quaternion startingOrientationModel) { - this.worldSpace = worldSpace; - positionModelSpace = startingPositionModel; - orientationModelSpace = startingOrientationModel; - smoothScale = new SmoothScale(); - } - - public SmoothMoves(SmoothMoves source) { - this.worldSpace = source.worldSpace; - this.positionModelSpace = source.positionModelSpace; - this.orientationModelSpace = source.orientationModelSpace; - this.smoothScale = new SmoothScale(source.smoothScale); - } - - public void UpdateDisplayPosition() { - if (null == positionModelSpace) return; - if (preventDisplayPositionUpdate) return; - if (null == displayPositionModelSpace) { - // If we don't have a display position yet, use the actual position. - displayPositionModelSpace = positionModelSpace; - return; - } - // Update the display position smoothly. - Vector3 targetPos = positionModelSpace; - Vector3 curPos = displayPositionModelSpace.Value; - float distToTarget = Vector3.Distance(targetPos, curPos); - // Calculate the speed we have to move at in order to hit the target in DISPLAY_CATCH_UP_TIME. - // But don't go any slower than MIN_CATCH_UP_SPEED. - float speed = Mathf.Max(distToTarget / DISPLAY_CATCH_UP_TIME, MIN_CATCH_UP_SPEED); - // Displacement is how far we could move on this frame, given the computed speed. - float displacement = speed * Time.deltaTime; - if (displacement >= distToTarget) { - // Arrived at target. - curPos = targetPos; - } else { - // Didn't arrive at target yet. - // Update curPos to go towards targetPos at the given speed. - curPos += displacement * (targetPos - curPos).normalized; - } - displayPositionModelSpace = curPos; - - smoothScale.Update(); - } - - public Vector3 GetScale() { - return smoothScale.GetScale(); - } - - public Vector3 GetDisplayPositionInWorldSpace() { - return (displayPositionModelSpace != null) ? - worldSpace.ModelToWorld(displayPositionModelSpace.Value) : - GetPositionInWorldSpace(); - } - - - - /// - /// Returns the position in world space. Note that if this IS NOT affected by smoothing. Smoothing is a purely - /// visual effect and does not alter the object's position. - /// - /// - public Vector3 GetPositionInWorldSpace() { - return worldSpace.ModelToWorld(positionModelSpace); - } - - public Vector3 GetPositionInModelSpace() { - return positionModelSpace; - } - - /// - /// Returns the display orientation in world space. - /// - public Quaternion GetDisplayOrientationInWorldSpace() { - if (displayOrientationWrapperModelSpace != null) { - // Retrieve the current slerped orientation... - Quaternion modelOrientation = displayOrientationWrapperModelSpace.UpdateAndGetCurrentOrientation(); - // and transform it into world space. - return worldSpace.ModelOrientationToWorld(modelOrientation); - } else { - // If we don't have a display orientation, just return the actual orientation in world space. - return GetOrientationInWorldSpace(); - } - } - - /// - /// Returns the orientation in world space. - /// - public Quaternion GetOrientationInWorldSpace() { - return worldSpace.ModelOrientationToWorld(orientationModelSpace); - } - - /// - /// Returns the orientation in model space. - /// - public Quaternion GetOrientationInModelSpace() { - return orientationModelSpace; - } - - /// - /// Sets the position in model space, optionally with smoothing. - /// - /// The new position in model space. - /// True if a smoothing effect is desired. Note that smoothing is a purely visual effect. - /// The actual position is instantaneously updated regardless of smoothing. - public void SetPositionModelSpace(Vector3 newPositionModelSpace, bool smooth = false) { - _positionModelSpace = newPositionModelSpace; - - if (!smooth) { - // Immediately update the display position as well. - displayPositionModelSpace = newPositionModelSpace; - } - preventDisplayPositionUpdate = false; - } - - /// - /// Sets the position in world space, optionally with smoothing. - /// - /// The new position in model space. - /// True if a smoothing effect is desired. Note that smoothing is a purely visual effect. - /// The actual position is instantaneously updated regardless of smoothing. - public void SetPositionWorldSpace(Vector3 newPositionModelSpace, bool smooth = false) { - _positionModelSpace = worldSpace.WorldToModel(newPositionModelSpace); - - if (!smooth) { - // Immediately update the display position as well. - displayPositionModelSpace = _positionModelSpace; - } - preventDisplayPositionUpdate = false; - } - - /// - /// Sets the position in model space, overriding smoothing with an override display position. This should primarily - /// be used when an external tool is handling smoothing (smoothing a parent rotation, for example) where lerping - /// position would result in an incorrect display position. - /// Positions will not be linearly interpolated until SetPositionModelSpace is called again. - /// - /// The new position in model space. - /// The override position to display the mesh at. - public void SetPositionWithDisplayOverrideModelSpace(Vector3 newPositionModelSpace, - Vector3 newDisplayPositionModelSpace) { - _positionModelSpace = newPositionModelSpace; - displayPositionModelSpace = newDisplayPositionModelSpace; - preventDisplayPositionUpdate = true; - } - - /// - /// Sets the orientation in model space, optionally with smoothing. - /// - /// The new orientation in model space. - /// True if a smoothing effect is desired. Note that smoothing is a purely visual effect. - /// The actual orientation is instantaneously updated regardless of smoothing. - public void SetOrientationModelSpace(Quaternion newOrientationModelSpace, bool smooth = false) { - _orientationModelSpace = newOrientationModelSpace; - - // Detect bad rotations when in editor, and log. + public class SmoothMoves + { + // Smoothing settings: + + // How long, in seconds, it takes for the displayed position to catch up with the real position. + private const float DISPLAY_CATCH_UP_TIME = 0.05f; + // Minimum speed at which displayed position catches up with the real position. + private const float MIN_CATCH_UP_SPEED = 0.1f; + // How close the displayed position has to be to the real position for us to end the animation. + private const float DIST_TO_TARGET_EPSILON = 0.001f; + + private Vector3 _positionModelSpace; + /// + /// Used to disable linear interpolation of display position. Primary use case is when a user of + /// MeshWithMaterialRenderer is handling a transform on its own where linear interpolation would result + /// in incorrect output - rotating the mesh by its parent transform, for example. + /// + bool preventDisplayPositionUpdate = false; + + // Optional position in model space. For things that are not parented to game objects (like selections) + // it makes more sense to specify their location in model space. + // Setting this position will result in a hard update (no smoothing). If smoothing is desired, use the + // SetPositionModelSpace method and indicate your desire to have the motion smoothed. + public Vector3 positionModelSpace + { + get + { + return _positionModelSpace; + } + set + { + // Setting this property is a hard update (not smoothed). + SetPositionModelSpace(value, false /* smooth */); + } + } + + private Quaternion _orientationModelSpace; + + // Optional orientation in model space. + // Setting is private to enforce use of SetOrientationModelSpace as it makes intent clearer. + public Quaternion orientationModelSpace + { + get + { + return _orientationModelSpace; + } + private set + { + // Setting this property is a hard update (not smoothed). + SetOrientationModelSpace(value, false /* smooth */); + } + } + + // For smooth animation, this is the position at which we are currently displaying this object. + // We update this each frame to chase after the real position. + private Vector3? displayPositionModelSpace; + + // A wrapper for model space orientation that manages spherical linear interpolation of orientation changes. + // This is updated every frame. + private Slerpee displayOrientationWrapperModelSpace; + + private SmoothScale smoothScale; + + private WorldSpace worldSpace; + + public SmoothMoves(WorldSpace worldSpace, Vector3 startingPositionModel, Quaternion startingOrientationModel) + { + this.worldSpace = worldSpace; + positionModelSpace = startingPositionModel; + orientationModelSpace = startingOrientationModel; + smoothScale = new SmoothScale(); + } + + public SmoothMoves(SmoothMoves source) + { + this.worldSpace = source.worldSpace; + this.positionModelSpace = source.positionModelSpace; + this.orientationModelSpace = source.orientationModelSpace; + this.smoothScale = new SmoothScale(source.smoothScale); + } + + public void UpdateDisplayPosition() + { + if (null == positionModelSpace) return; + if (preventDisplayPositionUpdate) return; + if (null == displayPositionModelSpace) + { + // If we don't have a display position yet, use the actual position. + displayPositionModelSpace = positionModelSpace; + return; + } + // Update the display position smoothly. + Vector3 targetPos = positionModelSpace; + Vector3 curPos = displayPositionModelSpace.Value; + float distToTarget = Vector3.Distance(targetPos, curPos); + // Calculate the speed we have to move at in order to hit the target in DISPLAY_CATCH_UP_TIME. + // But don't go any slower than MIN_CATCH_UP_SPEED. + float speed = Mathf.Max(distToTarget / DISPLAY_CATCH_UP_TIME, MIN_CATCH_UP_SPEED); + // Displacement is how far we could move on this frame, given the computed speed. + float displacement = speed * Time.deltaTime; + if (displacement >= distToTarget) + { + // Arrived at target. + curPos = targetPos; + } + else + { + // Didn't arrive at target yet. + // Update curPos to go towards targetPos at the given speed. + curPos += displacement * (targetPos - curPos).normalized; + } + displayPositionModelSpace = curPos; + + smoothScale.Update(); + } + + public Vector3 GetScale() + { + return smoothScale.GetScale(); + } + + public Vector3 GetDisplayPositionInWorldSpace() + { + return (displayPositionModelSpace != null) ? + worldSpace.ModelToWorld(displayPositionModelSpace.Value) : + GetPositionInWorldSpace(); + } + + + + /// + /// Returns the position in world space. Note that if this IS NOT affected by smoothing. Smoothing is a purely + /// visual effect and does not alter the object's position. + /// + /// + public Vector3 GetPositionInWorldSpace() + { + return worldSpace.ModelToWorld(positionModelSpace); + } + + public Vector3 GetPositionInModelSpace() + { + return positionModelSpace; + } + + /// + /// Returns the display orientation in world space. + /// + public Quaternion GetDisplayOrientationInWorldSpace() + { + if (displayOrientationWrapperModelSpace != null) + { + // Retrieve the current slerped orientation... + Quaternion modelOrientation = displayOrientationWrapperModelSpace.UpdateAndGetCurrentOrientation(); + // and transform it into world space. + return worldSpace.ModelOrientationToWorld(modelOrientation); + } + else + { + // If we don't have a display orientation, just return the actual orientation in world space. + return GetOrientationInWorldSpace(); + } + } + + /// + /// Returns the orientation in world space. + /// + public Quaternion GetOrientationInWorldSpace() + { + return worldSpace.ModelOrientationToWorld(orientationModelSpace); + } + + /// + /// Returns the orientation in model space. + /// + public Quaternion GetOrientationInModelSpace() + { + return orientationModelSpace; + } + + /// + /// Sets the position in model space, optionally with smoothing. + /// + /// The new position in model space. + /// True if a smoothing effect is desired. Note that smoothing is a purely visual effect. + /// The actual position is instantaneously updated regardless of smoothing. + public void SetPositionModelSpace(Vector3 newPositionModelSpace, bool smooth = false) + { + _positionModelSpace = newPositionModelSpace; + + if (!smooth) + { + // Immediately update the display position as well. + displayPositionModelSpace = newPositionModelSpace; + } + preventDisplayPositionUpdate = false; + } + + /// + /// Sets the position in world space, optionally with smoothing. + /// + /// The new position in model space. + /// True if a smoothing effect is desired. Note that smoothing is a purely visual effect. + /// The actual position is instantaneously updated regardless of smoothing. + public void SetPositionWorldSpace(Vector3 newPositionModelSpace, bool smooth = false) + { + _positionModelSpace = worldSpace.WorldToModel(newPositionModelSpace); + + if (!smooth) + { + // Immediately update the display position as well. + displayPositionModelSpace = _positionModelSpace; + } + preventDisplayPositionUpdate = false; + } + + /// + /// Sets the position in model space, overriding smoothing with an override display position. This should primarily + /// be used when an external tool is handling smoothing (smoothing a parent rotation, for example) where lerping + /// position would result in an incorrect display position. + /// Positions will not be linearly interpolated until SetPositionModelSpace is called again. + /// + /// The new position in model space. + /// The override position to display the mesh at. + public void SetPositionWithDisplayOverrideModelSpace(Vector3 newPositionModelSpace, + Vector3 newDisplayPositionModelSpace) + { + _positionModelSpace = newPositionModelSpace; + displayPositionModelSpace = newDisplayPositionModelSpace; + preventDisplayPositionUpdate = true; + } + + /// + /// Sets the orientation in model space, optionally with smoothing. + /// + /// The new orientation in model space. + /// True if a smoothing effect is desired. Note that smoothing is a purely visual effect. + /// The actual orientation is instantaneously updated regardless of smoothing. + public void SetOrientationModelSpace(Quaternion newOrientationModelSpace, bool smooth = false) + { + _orientationModelSpace = newOrientationModelSpace; + + // Detect bad rotations when in editor, and log. #if UNITY_EDITOR if (!util.Math3d.QuaternionIsValidRotation(newOrientationModelSpace)) { Debug.Log("Bad orientation in set orientation: " + newOrientationModelSpace); } #endif - // Create Slerpee for this mesh to manage interpolation if it doesn't exist. - if (displayOrientationWrapperModelSpace == null) { - displayOrientationWrapperModelSpace = new Slerpee(newOrientationModelSpace); - } - if (!smooth) { - // Immediately update the display position as well. - displayOrientationWrapperModelSpace.UpdateOrientationInstantly(newOrientationModelSpace); - } else { - // Otherwise set the target orientation for the slerp. - displayOrientationWrapperModelSpace.StartOrUpdateSlerp(newOrientationModelSpace); - } - } - - public void SetOrientationWorldSpace(Quaternion newOrientationWorldSpace, bool smooth = false) { - SetOrientationModelSpace(worldSpace.WorldOrientationToModel(newOrientationWorldSpace), smooth); - } - - /// - /// Sets the orientation in model space, with a display override (for when a tool is managing its own smoothing, - /// ie, when the smoothing is being done on a parent transform). - /// - /// The new orientation in model space. - /// The orientation to display. - /// Whether to smooth transitions to and from the display orientation. - /// This option is here primarily to smooth a transition into an override mode. - public void SetOrientationWithDisplayOverrideModelSpace(Quaternion newOrientationModelSpace, - Quaternion newDisplayOrientationModelSpace, bool smooth) { - _orientationModelSpace = newOrientationModelSpace; - if (newOrientationModelSpace == null) { - return; - } - - // Detect bad rotations when in editor, and log. + // Create Slerpee for this mesh to manage interpolation if it doesn't exist. + if (displayOrientationWrapperModelSpace == null) + { + displayOrientationWrapperModelSpace = new Slerpee(newOrientationModelSpace); + } + if (!smooth) + { + // Immediately update the display position as well. + displayOrientationWrapperModelSpace.UpdateOrientationInstantly(newOrientationModelSpace); + } + else + { + // Otherwise set the target orientation for the slerp. + displayOrientationWrapperModelSpace.StartOrUpdateSlerp(newOrientationModelSpace); + } + } + + public void SetOrientationWorldSpace(Quaternion newOrientationWorldSpace, bool smooth = false) + { + SetOrientationModelSpace(worldSpace.WorldOrientationToModel(newOrientationWorldSpace), smooth); + } + + /// + /// Sets the orientation in model space, with a display override (for when a tool is managing its own smoothing, + /// ie, when the smoothing is being done on a parent transform). + /// + /// The new orientation in model space. + /// The orientation to display. + /// Whether to smooth transitions to and from the display orientation. + /// This option is here primarily to smooth a transition into an override mode. + public void SetOrientationWithDisplayOverrideModelSpace(Quaternion newOrientationModelSpace, + Quaternion newDisplayOrientationModelSpace, bool smooth) + { + _orientationModelSpace = newOrientationModelSpace; + if (newOrientationModelSpace == null) + { + return; + } + + // Detect bad rotations when in editor, and log. #if UNITY_EDITOR if (!util.Math3d.QuaternionIsValidRotation(newOrientationModelSpace)) { Debug.Log("Bad orientation in set orientation: " + newOrientationModelSpace); } #endif - // Create Slerpee for this mesh to manage interpolation if it doesn't exist. - if (displayOrientationWrapperModelSpace == null) { - displayOrientationWrapperModelSpace = new Slerpee(newDisplayOrientationModelSpace); - } - if (smooth) { - displayOrientationWrapperModelSpace.StartOrUpdateSlerp(newDisplayOrientationModelSpace); - } else { - displayOrientationWrapperModelSpace.UpdateOrientationInstantly(newDisplayOrientationModelSpace); - } - } - - /// - /// Animates this object's displayed position from the given position to the current one. - /// - /// Old position, in the model space. - public void AnimatePositionFrom(Vector3 oldPosModelSpace) { - displayPositionModelSpace = oldPosModelSpace; - } + // Create Slerpee for this mesh to manage interpolation if it doesn't exist. + if (displayOrientationWrapperModelSpace == null) + { + displayOrientationWrapperModelSpace = new Slerpee(newDisplayOrientationModelSpace); + } + if (smooth) + { + displayOrientationWrapperModelSpace.StartOrUpdateSlerp(newDisplayOrientationModelSpace); + } + else + { + displayOrientationWrapperModelSpace.UpdateOrientationInstantly(newDisplayOrientationModelSpace); + } + } + + /// + /// Animates this object's displayed position from the given position to the current one. + /// + /// Old position, in the model space. + public void AnimatePositionFrom(Vector3 oldPosModelSpace) + { + displayPositionModelSpace = oldPosModelSpace; + } + + /// + /// Animates this object's displayed scale from the given scale to the default scale (1.0f). + /// + /// Old scale factor. + public void AnimateScaleFrom(float fromScale) + { + smoothScale.AnimateScaleFrom(fromScale); + } - /// - /// Animates this object's displayed scale from the given scale to the default scale (1.0f). - /// - /// Old scale factor. - public void AnimateScaleFrom(float fromScale) { - smoothScale.AnimateScaleFrom(fromScale); } - - } } \ No newline at end of file diff --git a/Assets/Scripts/model/core/SnapDetector.cs b/Assets/Scripts/model/core/SnapDetector.cs index 8d5fec74..ec3a2b34 100644 --- a/Assets/Scripts/model/core/SnapDetector.cs +++ b/Assets/Scripts/model/core/SnapDetector.cs @@ -20,612 +20,658 @@ using UnityEngine; using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.model.core { - /// - /// Detects what type of snap should occur and what should be snapped given the current state. We can either - /// mesh --> mesh snap, face --> face snap or mesh --> universe snap and we decide this by: - /// 1) Checking if there is a target mesh to snap to. - /// - A target mesh can be snapped to if the source meshes offset is within the bounding box of the target - /// mesh and the delta between their offsets is within a threshold. Note bug is still in progress and - /// we are still improving mesh snapping. - /// 2) If not check if there are faces to snap together. - /// - A source face can be snapped onto a target face if they point towards each other and the physical - /// distance between the faces is within a threshold. - /// 3) If not default to snapping the source mesh to the universe. - /// - /// While detecting the snap we will maintain as much relevant information as possible to avoid extraneous - /// calculations when the snap is performed. - /// - public class SnapDetector { - // Defining thresholds in world space accounts for a user's available precision and their perspective. When a user - // can see less they are more likely to snap or stick but when a user is zoomed in and working in detail they need - // to be more precise to snap or stick because they can visualize what they are working on better. - - // The threshold for searching for any nearby meshes in the spatialIndex that could be snapped to. - private static readonly float MESH_DETECTION_THRESHOLD_WORLDSPACE = 0.02f; - // The threshold for searching for any nearby faces in the spatialIndex that could be snapped to. - private static readonly float FACE_DETECTION_THRESHOLD_WORLDSPACE = 0.06f; - // The threshold that a source mesh and target mesh must be within to be snapped together. - private static readonly float MESH_SNAP_THRESHOLD_WORLDSPACE = 0.04f; - // The percentage of the inside of a mesh's bounds that should be considered the mesh's center for detecting - // interesections with other meshes. - private static readonly float CENTER_INTERSECTION_PERCENTAGE = 0.65f; - - private Rope rope; - private ContinuousFaceSnapEffect continuousFaceSnapEffect; - private ContinuousPointStickEffect continuousSourcePointSnapEffect; - private ContinuousPointStickEffect continuousTargetPointSnapEffect; - private ContinuousMeshSnapEffect continuousMeshSnapEffect; - private SnapSpace snapSpace; - - public SnapDetector() { - // Instantiate the snap detection rope. This is only updated and shown to the user if Features.showSnappingGuides - // is enabled but we'll instantiate regardless so that the flag can be flipped mid session. - rope = new Rope(); - rope.Hide(); - continuousFaceSnapEffect = new ContinuousFaceSnapEffect(); - continuousSourcePointSnapEffect = new ContinuousPointStickEffect(); - continuousTargetPointSnapEffect = new ContinuousPointStickEffect(); - continuousMeshSnapEffect = new ContinuousMeshSnapEffect(); - snapSpace = null; - } - +namespace com.google.apps.peltzer.client.model.core +{ /// - /// Detects what type of snap should be performed when required and populates snap with all the correct info - /// for maximum performance and reusability. + /// Detects what type of snap should occur and what should be snapped given the current state. We can either + /// mesh --> mesh snap, face --> face snap or mesh --> universe snap and we decide this by: + /// 1) Checking if there is a target mesh to snap to. + /// - A target mesh can be snapped to if the source meshes offset is within the bounding box of the target + /// mesh and the delta between their offsets is within a threshold. Note bug is still in progress and + /// we are still improving mesh snapping. + /// 2) If not check if there are faces to snap together. + /// - A source face can be snapped onto a target face if they point towards each other and the physical + /// distance between the faces is within a threshold. + /// 3) If not default to snapping the source mesh to the universe. + /// + /// While detecting the snap we will maintain as much relevant information as possible to avoid extraneous + /// calculations when the snap is performed. /// - /// The mesh being snapped. This is the preview or held mesh. - /// The actual position in model space for the sourceMesh. - /// The actual model space rotation for the sourceMesh. - public void DetectSnap(MMesh sourceMesh, Vector3 sourceMeshOffset, Quaternion sourceMeshRotation) { - if (!PeltzerMain.Instance.restrictionManager.snappingAllowed) { - return; - } - // Calculate the radius of the sphere needed to encapsulate the sourceMesh. This is used for searching - // the spatial index and calculated once here to avoid calculating it multiple times. - float sourceRadius = - Mathf.Max(sourceMesh.bounds.extents.x, sourceMesh.bounds.extents.y, sourceMesh.bounds.extents.z); - - // Detect what type of snap should be performed. - // This is either mesh --> mesh, face --> face or mesh --> universe by: - // 1) Check if there is a mesh to snap to. - // 2) If not check if there are faces to snap together. - // 3) If not default to snapping to the universe. - if (!DetectRelationalSnap(sourceMesh, sourceMeshOffset, sourceMeshRotation, sourceRadius)) { - ChangeSnapSpace(new UniversalSnapSpace(sourceMesh.bounds)); - } - } + public class SnapDetector + { + // Defining thresholds in world space accounts for a user's available precision and their perspective. When a user + // can see less they are more likely to snap or stick but when a user is zoomed in and working in detail they need + // to be more precise to snap or stick because they can visualize what they are working on better. + + // The threshold for searching for any nearby meshes in the spatialIndex that could be snapped to. + private static readonly float MESH_DETECTION_THRESHOLD_WORLDSPACE = 0.02f; + // The threshold for searching for any nearby faces in the spatialIndex that could be snapped to. + private static readonly float FACE_DETECTION_THRESHOLD_WORLDSPACE = 0.06f; + // The threshold that a source mesh and target mesh must be within to be snapped together. + private static readonly float MESH_SNAP_THRESHOLD_WORLDSPACE = 0.04f; + // The percentage of the inside of a mesh's bounds that should be considered the mesh's center for detecting + // interesections with other meshes. + private static readonly float CENTER_INTERSECTION_PERCENTAGE = 0.65f; + + private Rope rope; + private ContinuousFaceSnapEffect continuousFaceSnapEffect; + private ContinuousPointStickEffect continuousSourcePointSnapEffect; + private ContinuousPointStickEffect continuousTargetPointSnapEffect; + private ContinuousMeshSnapEffect continuousMeshSnapEffect; + private SnapSpace snapSpace; + + public SnapDetector() + { + // Instantiate the snap detection rope. This is only updated and shown to the user if Features.showSnappingGuides + // is enabled but we'll instantiate regardless so that the flag can be flipped mid session. + rope = new Rope(); + rope.Hide(); + continuousFaceSnapEffect = new ContinuousFaceSnapEffect(); + continuousSourcePointSnapEffect = new ContinuousPointStickEffect(); + continuousTargetPointSnapEffect = new ContinuousPointStickEffect(); + continuousMeshSnapEffect = new ContinuousMeshSnapEffect(); + snapSpace = null; + } - /// - /// Executes the last detected snap and returns the SnapSpace to the tool that is snapping. - /// - /// The mesh being snapped. This is the preview or held mesh. - /// The actual position in model space for the sourceMesh. - /// The actual model space rotation for the sourceMesh. - /// The fully setup SnapSpace. - public SnapSpace ExecuteSnap(MMesh sourceMesh, Vector3 sourceMeshOffset, Quaternion sourceMeshRotation) { - if (snapSpace == null || !snapSpace.IsValid()) { - // No snapSpace has been detected. We'll have to detect before executing. This happens if the user skips over - // the detection step by just pulling the full alt-trigger. - DetectSnap(sourceMesh, sourceMeshOffset, sourceMeshRotation); - } - - snapSpace.Execute(); - - // Relinquish ownership of this snapSpace. Its belongs to the tool now. - SnapSpace passedOffSnapSpace = snapSpace; - - // Clear out any detection UI. - Reset(); - return passedOffSnapSpace; - } + /// + /// Detects what type of snap should be performed when required and populates snap with all the correct info + /// for maximum performance and reusability. + /// + /// The mesh being snapped. This is the preview or held mesh. + /// The actual position in model space for the sourceMesh. + /// The actual model space rotation for the sourceMesh. + public void DetectSnap(MMesh sourceMesh, Vector3 sourceMeshOffset, Quaternion sourceMeshRotation) + { + if (!PeltzerMain.Instance.restrictionManager.snappingAllowed) + { + return; + } + // Calculate the radius of the sphere needed to encapsulate the sourceMesh. This is used for searching + // the spatial index and calculated once here to avoid calculating it multiple times. + float sourceRadius = + Mathf.Max(sourceMesh.bounds.extents.x, sourceMesh.bounds.extents.y, sourceMesh.bounds.extents.z); + + // Detect what type of snap should be performed. + // This is either mesh --> mesh, face --> face or mesh --> universe by: + // 1) Check if there is a mesh to snap to. + // 2) If not check if there are faces to snap together. + // 3) If not default to snapping to the universe. + if (!DetectRelationalSnap(sourceMesh, sourceMeshOffset, sourceMeshRotation, sourceRadius)) + { + ChangeSnapSpace(new UniversalSnapSpace(sourceMesh.bounds)); + } + } - /// - /// Resets SnapDetector by clearing out the previous detected SnapSpace and turning off any existing highlights. - /// - public void Reset() { - HideGuides(); - snapSpace = null; - } + /// + /// Executes the last detected snap and returns the SnapSpace to the tool that is snapping. + /// + /// The mesh being snapped. This is the preview or held mesh. + /// The actual position in model space for the sourceMesh. + /// The actual model space rotation for the sourceMesh. + /// The fully setup SnapSpace. + public SnapSpace ExecuteSnap(MMesh sourceMesh, Vector3 sourceMeshOffset, Quaternion sourceMeshRotation) + { + if (snapSpace == null || !snapSpace.IsValid()) + { + // No snapSpace has been detected. We'll have to detect before executing. This happens if the user skips over + // the detection step by just pulling the full alt-trigger. + DetectSnap(sourceMesh, sourceMeshOffset, sourceMeshRotation); + } - public void UpdateHints(SnapSpace snapSpace, MMesh sourceMesh, Vector3 sourceMeshOffset, Quaternion sourceMeshRotation) { - if (snapSpace.SnapType == SnapType.FACE) { - continuousFaceSnapEffect.UpdateFromSnapSpace(snapSpace as FaceSnapSpace); - FaceSnapSpace tempSpace = (FaceSnapSpace)snapSpace; - continuousSourcePointSnapEffect.UpdateFromPoint(tempSpace.sourceFaceCenter); - continuousTargetPointSnapEffect.UpdateFromPoint(tempSpace.snapPoint); - } - } + snapSpace.Execute(); - /// - /// We will try to detect a relational snap (Mesh or Face snap) following this algorithm. We combine Face and Mesh - /// snap detection to avoid doing two passes of calculations on the target and source faces. - - /// 1) Find all the the meshes that interesect a search radius larger than the sourceMesh. This will ensure - /// that we'll find any meshes that the faces of the sourceMesh intersect as well. - - /// 2) MESH SNAP if any source face is inside any possible target mesh. This indicates that the source mesh is - /// overlapping or intersecting another mesh in the scene. We will use a heuristic to check if the source mesh is - /// intersecting any target mesh by checking if the center of a source face is inside a target mesh. This doesn't - /// detect full geometric overlap but seems good enough. - - /// 3) Account for two edge cases missed by the intersection check in step two: - /// 1. If the source mesh is inside a hole of the target mesh it won't intersect (A cylinder inside a torus) - /// 2. If the source mesh is larger than a target mesh and overlaps it entirely no face center will intersect - /// the smaller target mesh. - /// We'll use a pretty simple heuristic to catch these two cases. Given all the meshes we found nearby in step one - /// just check to see if the offset of a nearby mesh is inside the source mesh bounds. Bounds and offset are just - /// a proxy for mesh geometry and can easily be fooled by anything more complex than a cube. To try to avoid false - /// positives for mesh snapping here we will check if the target mesh offset is some threshold from the offset of - /// the source mesh. But this is just a heuristic. It's possible we'll mesh snap when we should face snap and that - /// there may be a dead zone in the source mesh where we don't mesh snap to a target mesh thats inside the - /// geometry but outside the threshold. We want to favor face snapping though so we'll keep the threshold small. - - /// 4) FACE SNAP based on information gathered in step two. Step two requires us to compare every source face to - /// every target face to determine if we should mesh snap so we simultaneously calculate the separation between - /// the faces and can then face snap the closest pair if we don't mesh snap. - /// - /// - /// To be as efficient as possible we will actually: - /// 1) Find all nearby meshes ordered by nearness of offsets. - /// 2) Check if the offset of the nearest mesh is within x% of the sourceMesh radius from the sourceMesh offset and - /// break before comparing any faces. We only need to compare the nearest mesh, if the nearest mesh is not in - /// the threshold, no mesh is. - /// 3) Compare the set of sourceMesh faces against every face of target meshes one mesh at time. If we determine - /// that a source face is inside a target mesh we will break early and mesh snap. We know the first mesh we - /// detect a source face is inside is the closest mesh because the nearby meshes were sorted by nearness. - /// 4) We compare each pair of faces by: - /// Doing Mesh Snap checks: - /// 1. Checking if the center of the source face is behind the plane of the target face. This helps us determine - /// if the source face is intersecting the target mesh. - /// - /// Doing Face Snap checks: - /// 1. Check if the center of the source face when projected onto the target face is actually within the target - /// face boundaries. If it's not we won't snap the faces together since we can't tell for sure they overlap. - /// Using the center is only a heuristic and makes its hard to snap large source faces to small target faces - /// since. - /// 2. Check if the angle between the source face and target face point towards each other. We can do this by - /// checking that the angle between the normals is greater than 90 degrees. - /// 3. Calculate separation. This is a combination of the physical distance between the faces and how flush they - /// are. See bug for a diagram on how we calculate separation. - /// 4. Check that the separation is within a threshold. The threshold used to find nearby meshes isn't strict - /// enough because we just check that a nearby mesh intersects that threshold. Its possible a face on that - /// mesh is far out of reach. - /// 4. If this is the closest separation detected so far we store it. - /// 5) If we compare all faces and don't break early to mesh snap we will try to snap the pair of faces we - /// determined have the smallest separation. - /// 6) If we never found an eligible pair of faces to snap we return false. - /// - private bool DetectRelationalSnap(MMesh sourceMesh, Vector3 sourceMeshOffset, Quaternion sourceMeshRotation, - float sourceRadius) { - // We use a worldspace thresholds so that the user has to move the same amount despite zoom level to get within - // the mesh snap threshold. This means when they zoom out and meshes are small they are more likely to mesh snap - // which makes sense given faces are so small they shouldn't be trying to snap them together. - - // Find the threshold used as a search radius around the sourceMesh to find intersecting target meshes. - float meshClosenessThresholdModelSpace = - MESH_DETECTION_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale - + sourceRadius; - - // We use another threshold to determine if two faces are close enough to snap together. - float faceClosenessThresholdModelSpace = - FACE_DETECTION_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale - + sourceRadius; - - // Find all the meshes that the meshClosenessThreshold intersects ordered by nearness. - List> nearestMeshes; - bool hasNearbyMeshes = PeltzerMain.Instance.GetSpatialIndex().FindNearestMeshesToNotIncludingPoint( - sourceMeshOffset, - meshClosenessThresholdModelSpace, - out nearestMeshes, - /*ignoreHiddenMeshes*/ true); - - // If there aren't any nearby meshes we can't mesh or face snap. - if (hasNearbyMeshes) { - - // Run a heuristic check to see if the nearest mesh is "inside" the source mesh by seeing if their offsets are - // within a threshold apart. The threshold is some fraction of the sourceMesh bounds so we use a proxy for - // "inside". - MMesh nearestMesh = PeltzerMain.Instance.model.GetMesh(nearestMeshes[0].value); - if (Vector3.Distance(nearestMesh.bounds.center, sourceMeshOffset) - < sourceRadius * CENTER_INTERSECTION_PERCENTAGE) { - MeshSnapSpace newMeshSnapSpace = new MeshSnapSpace(nearestMesh.id); - newMeshSnapSpace.unsnappedPosition = sourceMeshOffset; - newMeshSnapSpace.snappedPosition = nearestMesh.bounds.center; - ChangeSnapSpace(newMeshSnapSpace); - return true; + // Relinquish ownership of this snapSpace. Its belongs to the tool now. + SnapSpace passedOffSnapSpace = snapSpace; + + // Clear out any detection UI. + Reset(); + return passedOffSnapSpace; } - // We determine mesh and face snapping by comparing every face pair in one pass so we set up variables to keep - // track of what should snap. - - // Values to track the pair of faces that should snap. - FaceSnapSpace tempFaceSnapSpace = null; - float closestSeparation = Mathf.Infinity; - - // Compare every face from the source mesh to every possible target mesh and the mesh's faces. - foreach (Face sourceFace in sourceMesh.GetFaces()) { - // Find the position of the vertices in model space for the sourceMeshFace so we can calculate the face's - // center and normal. - List sourceMeshFaceVerticesInModelSpace = MeshMath.CalculateVertexPositions( - sourceFace.vertexIds, - sourceMesh, - sourceMeshOffset, - sourceMeshRotation); - - Vector3 sourceFaceCenter = MeshMath.CalculateGeometricCenter(sourceMeshFaceVerticesInModelSpace); - Vector3 sourceFaceNormal = MeshMath.CalculateNormal(sourceMeshFaceVerticesInModelSpace); - - // Compare every source face to every possible target mesh and then the faces of the target mesh. We can - // break early on a target mesh if we should mesh snap but use the first pass comparing the faces to keep - // track of the closest pair of faces that might be snapped if we don't mesh snap. - foreach (DistancePair targetMeshId in nearestMeshes) { - // Grab the targetMesh. - MMesh targetMesh = PeltzerMain.Instance.model.GetMesh(targetMeshId.value); - - // Start tracking if the source face is inside this targetMesh. As we iterate over every target face in the - // target mesh we'll check if the source face is behind the target face. If at any point this is false we - // stop checking because we know that when the source face is not behind one target face then it is not - // within the mesh. If sourceFaceBehindEachTargetFace is still true when we have compared the source face - // to every face on the target mesh we know the source face intersects the target mesh and we should mesh - // snap. - bool sourceFaceBehindEachTargetFace = true; - foreach (Face targetFace in targetMesh.GetFaces()) { - FaceInfo targetFaceInfo; - FaceKey targetFaceKey = new FaceKey(targetMesh.id, targetFace.id); - if (PeltzerMain.Instance.GetSpatialIndex().TryGetFaceInfo(targetFaceKey, out targetFaceInfo)) { - // Up to this point we were just getting all the information we needed as efficiently as possible. Now - // we start the detection algorithm. - - // 1) Check if the source face is "behind" the target face. If the source face is behind every target - // face on the target mesh then we can say that the source mesh intersects the target mesh and we - // should mesh snap. As soon as the source face is not "behind" one target face then it does not - // intersect and we can stop checking. - if (sourceFaceBehindEachTargetFace) { - // GetSide() returns false if the sourceFaceCenter is behind the targetFace. - sourceFaceBehindEachTargetFace = !targetFaceInfo.plane.GetSide(sourceFaceCenter); - } + /// + /// Resets SnapDetector by clearing out the previous detected SnapSpace and turning off any existing highlights. + /// + public void Reset() + { + HideGuides(); + snapSpace = null; + } - // Compare the source face to target face and find their separation. We might mesh snap instead but - // if we don't want to have to compare all the faces again so we just do that calculation now. This is - // a combination of the physical distance between the faces and how flush they are. See bug for - // a diagram on how we calculate separation. + public void UpdateHints(SnapSpace snapSpace, MMesh sourceMesh, Vector3 sourceMeshOffset, Quaternion sourceMeshRotation) + { + if (snapSpace.SnapType == SnapType.FACE) + { + continuousFaceSnapEffect.UpdateFromSnapSpace(snapSpace as FaceSnapSpace); + FaceSnapSpace tempSpace = (FaceSnapSpace)snapSpace; + continuousSourcePointSnapEffect.UpdateFromPoint(tempSpace.sourceFaceCenter); + continuousTargetPointSnapEffect.UpdateFromPoint(tempSpace.snapPoint); + } + } - // We only want to snap faces that are pointing towards each other so we can breakout by checking the - // angle of the normals. If its 90 degrees or less the faces point away from each other. - if (Vector3.Angle(sourceFaceNormal, targetFaceInfo.plane.normal) <= 90f) { - continue; + /// + /// We will try to detect a relational snap (Mesh or Face snap) following this algorithm. We combine Face and Mesh + /// snap detection to avoid doing two passes of calculations on the target and source faces. + + /// 1) Find all the the meshes that interesect a search radius larger than the sourceMesh. This will ensure + /// that we'll find any meshes that the faces of the sourceMesh intersect as well. + + /// 2) MESH SNAP if any source face is inside any possible target mesh. This indicates that the source mesh is + /// overlapping or intersecting another mesh in the scene. We will use a heuristic to check if the source mesh is + /// intersecting any target mesh by checking if the center of a source face is inside a target mesh. This doesn't + /// detect full geometric overlap but seems good enough. + + /// 3) Account for two edge cases missed by the intersection check in step two: + /// 1. If the source mesh is inside a hole of the target mesh it won't intersect (A cylinder inside a torus) + /// 2. If the source mesh is larger than a target mesh and overlaps it entirely no face center will intersect + /// the smaller target mesh. + /// We'll use a pretty simple heuristic to catch these two cases. Given all the meshes we found nearby in step one + /// just check to see if the offset of a nearby mesh is inside the source mesh bounds. Bounds and offset are just + /// a proxy for mesh geometry and can easily be fooled by anything more complex than a cube. To try to avoid false + /// positives for mesh snapping here we will check if the target mesh offset is some threshold from the offset of + /// the source mesh. But this is just a heuristic. It's possible we'll mesh snap when we should face snap and that + /// there may be a dead zone in the source mesh where we don't mesh snap to a target mesh thats inside the + /// geometry but outside the threshold. We want to favor face snapping though so we'll keep the threshold small. + + /// 4) FACE SNAP based on information gathered in step two. Step two requires us to compare every source face to + /// every target face to determine if we should mesh snap so we simultaneously calculate the separation between + /// the faces and can then face snap the closest pair if we don't mesh snap. + /// + /// + /// To be as efficient as possible we will actually: + /// 1) Find all nearby meshes ordered by nearness of offsets. + /// 2) Check if the offset of the nearest mesh is within x% of the sourceMesh radius from the sourceMesh offset and + /// break before comparing any faces. We only need to compare the nearest mesh, if the nearest mesh is not in + /// the threshold, no mesh is. + /// 3) Compare the set of sourceMesh faces against every face of target meshes one mesh at time. If we determine + /// that a source face is inside a target mesh we will break early and mesh snap. We know the first mesh we + /// detect a source face is inside is the closest mesh because the nearby meshes were sorted by nearness. + /// 4) We compare each pair of faces by: + /// Doing Mesh Snap checks: + /// 1. Checking if the center of the source face is behind the plane of the target face. This helps us determine + /// if the source face is intersecting the target mesh. + /// + /// Doing Face Snap checks: + /// 1. Check if the center of the source face when projected onto the target face is actually within the target + /// face boundaries. If it's not we won't snap the faces together since we can't tell for sure they overlap. + /// Using the center is only a heuristic and makes its hard to snap large source faces to small target faces + /// since. + /// 2. Check if the angle between the source face and target face point towards each other. We can do this by + /// checking that the angle between the normals is greater than 90 degrees. + /// 3. Calculate separation. This is a combination of the physical distance between the faces and how flush they + /// are. See bug for a diagram on how we calculate separation. + /// 4. Check that the separation is within a threshold. The threshold used to find nearby meshes isn't strict + /// enough because we just check that a nearby mesh intersects that threshold. Its possible a face on that + /// mesh is far out of reach. + /// 4. If this is the closest separation detected so far we store it. + /// 5) If we compare all faces and don't break early to mesh snap we will try to snap the pair of faces we + /// determined have the smallest separation. + /// 6) If we never found an eligible pair of faces to snap we return false. + /// + private bool DetectRelationalSnap(MMesh sourceMesh, Vector3 sourceMeshOffset, Quaternion sourceMeshRotation, + float sourceRadius) + { + // We use a worldspace thresholds so that the user has to move the same amount despite zoom level to get within + // the mesh snap threshold. This means when they zoom out and meshes are small they are more likely to mesh snap + // which makes sense given faces are so small they shouldn't be trying to snap them together. + + // Find the threshold used as a search radius around the sourceMesh to find intersecting target meshes. + float meshClosenessThresholdModelSpace = + MESH_DETECTION_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale + + sourceRadius; + + // We use another threshold to determine if two faces are close enough to snap together. + float faceClosenessThresholdModelSpace = + FACE_DETECTION_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale + + sourceRadius; + + // Find all the meshes that the meshClosenessThreshold intersects ordered by nearness. + List> nearestMeshes; + bool hasNearbyMeshes = PeltzerMain.Instance.GetSpatialIndex().FindNearestMeshesToNotIncludingPoint( + sourceMeshOffset, + meshClosenessThresholdModelSpace, + out nearestMeshes, + /*ignoreHiddenMeshes*/ true); + + // If there aren't any nearby meshes we can't mesh or face snap. + if (hasNearbyMeshes) + { + + // Run a heuristic check to see if the nearest mesh is "inside" the source mesh by seeing if their offsets are + // within a threshold apart. The threshold is some fraction of the sourceMesh bounds so we use a proxy for + // "inside". + MMesh nearestMesh = PeltzerMain.Instance.model.GetMesh(nearestMeshes[0].value); + if (Vector3.Distance(nearestMesh.bounds.center, sourceMeshOffset) + < sourceRadius * CENTER_INTERSECTION_PERCENTAGE) + { + MeshSnapSpace newMeshSnapSpace = new MeshSnapSpace(nearestMesh.id); + newMeshSnapSpace.unsnappedPosition = sourceMeshOffset; + newMeshSnapSpace.snappedPosition = nearestMesh.bounds.center; + ChangeSnapSpace(newMeshSnapSpace); + return true; } - // A ray out of the source face center along the inverse of the target face's normal. This is the - // "straight down" projection of the source center onto the target face. - Ray projectionRay = new Ray(sourceFaceCenter, -targetFaceInfo.plane.normal); - // The projectionLength is a measure of closeness. Small distances along the projection mean the faces - // are physically close to each other. - float projectionLength; - targetFaceInfo.plane.Raycast(projectionRay, out projectionLength); - - // Check to see if the sourceFaceCenter projected onto the plane of the targetFace is actually within - // the boundary of the face. This projected point is called the snap point. If the snap point is not - // within the target face we won't try to snap the faces together and don't have to find the normalRay. - Vector3 snapPoint = projectionRay.GetPoint(projectionLength); - if (!Math3d.IsInside(targetFaceInfo.border, snapPoint)) { - continue; + // We determine mesh and face snapping by comparing every face pair in one pass so we set up variables to keep + // track of what should snap. + + // Values to track the pair of faces that should snap. + FaceSnapSpace tempFaceSnapSpace = null; + float closestSeparation = Mathf.Infinity; + + // Compare every face from the source mesh to every possible target mesh and the mesh's faces. + foreach (Face sourceFace in sourceMesh.GetFaces()) + { + // Find the position of the vertices in model space for the sourceMeshFace so we can calculate the face's + // center and normal. + List sourceMeshFaceVerticesInModelSpace = MeshMath.CalculateVertexPositions( + sourceFace.vertexIds, + sourceMesh, + sourceMeshOffset, + sourceMeshRotation); + + Vector3 sourceFaceCenter = MeshMath.CalculateGeometricCenter(sourceMeshFaceVerticesInModelSpace); + Vector3 sourceFaceNormal = MeshMath.CalculateNormal(sourceMeshFaceVerticesInModelSpace); + + // Compare every source face to every possible target mesh and then the faces of the target mesh. We can + // break early on a target mesh if we should mesh snap but use the first pass comparing the faces to keep + // track of the closest pair of faces that might be snapped if we don't mesh snap. + foreach (DistancePair targetMeshId in nearestMeshes) + { + // Grab the targetMesh. + MMesh targetMesh = PeltzerMain.Instance.model.GetMesh(targetMeshId.value); + + // Start tracking if the source face is inside this targetMesh. As we iterate over every target face in the + // target mesh we'll check if the source face is behind the target face. If at any point this is false we + // stop checking because we know that when the source face is not behind one target face then it is not + // within the mesh. If sourceFaceBehindEachTargetFace is still true when we have compared the source face + // to every face on the target mesh we know the source face intersects the target mesh and we should mesh + // snap. + bool sourceFaceBehindEachTargetFace = true; + foreach (Face targetFace in targetMesh.GetFaces()) + { + FaceInfo targetFaceInfo; + FaceKey targetFaceKey = new FaceKey(targetMesh.id, targetFace.id); + if (PeltzerMain.Instance.GetSpatialIndex().TryGetFaceInfo(targetFaceKey, out targetFaceInfo)) + { + // Up to this point we were just getting all the information we needed as efficiently as possible. Now + // we start the detection algorithm. + + // 1) Check if the source face is "behind" the target face. If the source face is behind every target + // face on the target mesh then we can say that the source mesh intersects the target mesh and we + // should mesh snap. As soon as the source face is not "behind" one target face then it does not + // intersect and we can stop checking. + if (sourceFaceBehindEachTargetFace) + { + // GetSide() returns false if the sourceFaceCenter is behind the targetFace. + sourceFaceBehindEachTargetFace = !targetFaceInfo.plane.GetSide(sourceFaceCenter); + } + + // Compare the source face to target face and find their separation. We might mesh snap instead but + // if we don't want to have to compare all the faces again so we just do that calculation now. This is + // a combination of the physical distance between the faces and how flush they are. See bug for + // a diagram on how we calculate separation. + + // We only want to snap faces that are pointing towards each other so we can breakout by checking the + // angle of the normals. If its 90 degrees or less the faces point away from each other. + if (Vector3.Angle(sourceFaceNormal, targetFaceInfo.plane.normal) <= 90f) + { + continue; + } + + // A ray out of the source face center along the inverse of the target face's normal. This is the + // "straight down" projection of the source center onto the target face. + Ray projectionRay = new Ray(sourceFaceCenter, -targetFaceInfo.plane.normal); + // The projectionLength is a measure of closeness. Small distances along the projection mean the faces + // are physically close to each other. + float projectionLength; + targetFaceInfo.plane.Raycast(projectionRay, out projectionLength); + + // Check to see if the sourceFaceCenter projected onto the plane of the targetFace is actually within + // the boundary of the face. This projected point is called the snap point. If the snap point is not + // within the target face we won't try to snap the faces together and don't have to find the normalRay. + Vector3 snapPoint = projectionRay.GetPoint(projectionLength); + if (!Math3d.IsInside(targetFaceInfo.border, snapPoint)) + { + continue; + } + + // A ray out of the source face center along the normal of the source face. + Ray normalRay = new Ray(sourceFaceCenter, sourceFaceNormal); + // Find the distance from the source face center to the target face plane along the normal and + // projection. The normalLength is a measure of flushness. Small distances along the normal mean the + // faces point towards each other and are flush. + float normalLength; + targetFaceInfo.plane.Raycast(normalRay, out normalLength); + + // Calculate the separation which is the sum of these lengths. Not taking the average favours faces + // that are close and flush. + float separation = Mathf.Abs(normalLength) + Mathf.Abs(projectionLength); + + if (separation < faceClosenessThresholdModelSpace && separation < closestSeparation) + { + closestSeparation = separation; + // Calculate the snapPoint on the target face while we have already done all the heavy calculations. + //Vector3 snapPoint = projectionRay.GetPoint(projectionLength); + FaceKey sourceFaceKey = new FaceKey(sourceMesh.id, sourceFace.id); + tempFaceSnapSpace = new FaceSnapSpace(sourceMesh, sourceFaceKey, targetFaceKey, sourceFaceCenter, + targetFaceInfo.baryCenter, snapPoint); + tempFaceSnapSpace.sourceMeshOffset = sourceMeshOffset; + tempFaceSnapSpace.sourceMeshRotation = sourceMeshRotation; + } + } + else + { + // Failed to get the faceInfo from the spatialIndex. + continue; + } + } + + // We've checked every target face for this target mesh against the sourceFace. Check to see if we've + // determined if the source face is inside the target mesh and we can break out early and mesh snap. + if (sourceFaceBehindEachTargetFace) + { + MeshSnapSpace newMeshSnapSpace = new MeshSnapSpace(targetMesh.id); + newMeshSnapSpace.unsnappedPosition = sourceMeshOffset; + newMeshSnapSpace.snappedPosition = targetMesh.bounds.center; + ChangeSnapSpace(newMeshSnapSpace); + return true; + } + } } - // A ray out of the source face center along the normal of the source face. - Ray normalRay = new Ray(sourceFaceCenter, sourceFaceNormal); - // Find the distance from the source face center to the target face plane along the normal and - // projection. The normalLength is a measure of flushness. Small distances along the normal mean the - // faces point towards each other and are flush. - float normalLength; - targetFaceInfo.plane.Raycast(normalRay, out normalLength); - - // Calculate the separation which is the sum of these lengths. Not taking the average favours faces - // that are close and flush. - float separation = Mathf.Abs(normalLength) + Mathf.Abs(projectionLength); - - if (separation < faceClosenessThresholdModelSpace && separation < closestSeparation) { - closestSeparation = separation; - // Calculate the snapPoint on the target face while we have already done all the heavy calculations. - //Vector3 snapPoint = projectionRay.GetPoint(projectionLength); - FaceKey sourceFaceKey = new FaceKey(sourceMesh.id, sourceFace.id); - tempFaceSnapSpace = new FaceSnapSpace(sourceMesh, sourceFaceKey, targetFaceKey, sourceFaceCenter, - targetFaceInfo.baryCenter, snapPoint); - tempFaceSnapSpace.sourceMeshOffset = sourceMeshOffset; - tempFaceSnapSpace.sourceMeshRotation = sourceMeshRotation; + // We've compared all the source faces to all the nearby target mesh faces. If we've reached this point we + // already know we should mesh snap so if there is a pair of faces to snap together we'll do that. + if (tempFaceSnapSpace != null) + { + // But first see if we should be sticking the initial snapPoint of the FaceSnapSpace. We didn't do this + // while comparing faces to avoid extraneous calculations on faces we didn't end up using to snap. + + // Check to see if the initialSnapPoint is close enough to the center of the target face. If it is we should + // stick to the center by overriding snapPosition with the target face's center. + float distanceFromCenter = + Vector3.Distance(tempFaceSnapSpace.initialSnapPoint, tempFaceSnapSpace.targetFaceCenter); + tempFaceSnapSpace.initialSnapPoint = distanceFromCenter + < CoordinateSystem.STICK_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale + ? tempFaceSnapSpace.targetFaceCenter : tempFaceSnapSpace.initialSnapPoint; + + ChangeSnapSpace(tempFaceSnapSpace); + return true; } - } else { - // Failed to get the faceInfo from the spatialIndex. - continue; - } } - // We've checked every target face for this target mesh against the sourceFace. Check to see if we've - // determined if the source face is inside the target mesh and we can break out early and mesh snap. - if (sourceFaceBehindEachTargetFace) { - MeshSnapSpace newMeshSnapSpace = new MeshSnapSpace(targetMesh.id); - newMeshSnapSpace.unsnappedPosition = sourceMeshOffset; - newMeshSnapSpace.snappedPosition = targetMesh.bounds.center; - ChangeSnapSpace(newMeshSnapSpace); - return true; - } - } + return false; } - // We've compared all the source faces to all the nearby target mesh faces. If we've reached this point we - // already know we should mesh snap so if there is a pair of faces to snap together we'll do that. - if (tempFaceSnapSpace != null) { - // But first see if we should be sticking the initial snapPoint of the FaceSnapSpace. We didn't do this - // while comparing faces to avoid extraneous calculations on faces we didn't end up using to snap. - - // Check to see if the initialSnapPoint is close enough to the center of the target face. If it is we should - // stick to the center by overriding snapPosition with the target face's center. - float distanceFromCenter = - Vector3.Distance(tempFaceSnapSpace.initialSnapPoint, tempFaceSnapSpace.targetFaceCenter); - tempFaceSnapSpace.initialSnapPoint = distanceFromCenter - < CoordinateSystem.STICK_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale - ? tempFaceSnapSpace.targetFaceCenter : tempFaceSnapSpace.initialSnapPoint; - - ChangeSnapSpace(tempFaceSnapSpace); - return true; - } - } - - return false; - } + /// + /// Determines if there is a mesh nearby that should be snapped to and maintains all the snapInfo from detecting + /// the mesh snap to be reused when the snap is actually performed. + /// + /// The model space radius required to encapsulate the entire source mesh. + /// Whether there is a mesh to snap to. + private bool DetectMeshSnap(MMesh sourceMesh, Vector3 sourceMeshOffset, Quaternion sourceMeshRotation, + float sourceRadius) + { + // We use a worldspace threshold so that the user has to move the same amount despite zoom level to get within + // the mesh snap threshold. This means when they zoom out and meshes are small they are more likely to mesh snap + // which makes sense given faces are so small they shouldn't be trying to snap them together. + float meshClosenessThresholdModelSpace = + MESH_DETECTION_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale + + sourceRadius; + + // Use the spatial index to find the nearest mesh. + int? nearestMeshId; + bool hasNearbyMesh = PeltzerMain.Instance.GetSpatialIndex().FindNearestMeshToNotIncludingPoint( + sourceMeshOffset, + meshClosenessThresholdModelSpace, + out nearestMeshId, + /*ignoreHiddenMeshes*/ true); + + if (hasNearbyMesh) + { + MMesh nearestMesh = PeltzerMain.Instance.model.GetMesh(nearestMeshId.Value); + if (Vector3.Distance(nearestMesh.bounds.center, sourceMeshOffset) + < MESH_SNAP_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale) + { + + MeshSnapSpace newMeshSnapSpace = new MeshSnapSpace(nearestMeshId.Value); + newMeshSnapSpace.unsnappedPosition = sourceMeshOffset; + newMeshSnapSpace.snappedPosition = nearestMesh.bounds.center; + ChangeSnapSpace(newMeshSnapSpace); + return true; + } + } - /// - /// Determines if there is a mesh nearby that should be snapped to and maintains all the snapInfo from detecting - /// the mesh snap to be reused when the snap is actually performed. - /// - /// The model space radius required to encapsulate the entire source mesh. - /// Whether there is a mesh to snap to. - private bool DetectMeshSnap(MMesh sourceMesh, Vector3 sourceMeshOffset, Quaternion sourceMeshRotation, - float sourceRadius) { - // We use a worldspace threshold so that the user has to move the same amount despite zoom level to get within - // the mesh snap threshold. This means when they zoom out and meshes are small they are more likely to mesh snap - // which makes sense given faces are so small they shouldn't be trying to snap them together. - float meshClosenessThresholdModelSpace = - MESH_DETECTION_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale - + sourceRadius; - - // Use the spatial index to find the nearest mesh. - int? nearestMeshId; - bool hasNearbyMesh = PeltzerMain.Instance.GetSpatialIndex().FindNearestMeshToNotIncludingPoint( - sourceMeshOffset, - meshClosenessThresholdModelSpace, - out nearestMeshId, - /*ignoreHiddenMeshes*/ true); - - if (hasNearbyMesh) { - MMesh nearestMesh = PeltzerMain.Instance.model.GetMesh(nearestMeshId.Value); - if (Vector3.Distance(nearestMesh.bounds.center, sourceMeshOffset) - < MESH_SNAP_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale) { - - MeshSnapSpace newMeshSnapSpace = new MeshSnapSpace(nearestMeshId.Value); - newMeshSnapSpace.unsnappedPosition = sourceMeshOffset; - newMeshSnapSpace.snappedPosition = nearestMesh.bounds.center; - ChangeSnapSpace(newMeshSnapSpace); - return true; + return false; } - } - return false; - } + public void HideGuides() + { + rope.Hide(); + UXEffectManager.GetEffectManager().EndEffect(continuousFaceSnapEffect); + UXEffectManager.GetEffectManager().EndEffect(continuousSourcePointSnapEffect); + UXEffectManager.GetEffectManager().EndEffect(continuousTargetPointSnapEffect); + continuousMeshSnapEffect.Finish(); + } - public void HideGuides() { - rope.Hide(); - UXEffectManager.GetEffectManager().EndEffect(continuousFaceSnapEffect); - UXEffectManager.GetEffectManager().EndEffect(continuousSourcePointSnapEffect); - UXEffectManager.GetEffectManager().EndEffect(continuousTargetPointSnapEffect); - continuousMeshSnapEffect.Finish(); - } + /// + /// Determines if there is a pair of appropriate faces that should be snapped together and maintains all the + /// snapInfo from detecting the snap to be reused when the snap is actually performed. + /// + /// The model space radius required to encapsulate the source mesh. + /// Whether there are faces to snap together. + private bool DetectFaceSnap(MMesh sourceMesh, Vector3 sourceMeshOffset, Quaternion sourceMeshRotation, + float sourceRadius) + { + // Calculate the search radius for finding nearby faces. This is the radius of the sphere needed to encapsulate + // the sourceMesh plus a world space threshold. Defining the threshold in world space accounts for a user's + // available precision and their perspective. + float faceClosenessThresholdModelSpace = + FACE_DETECTION_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale + + sourceRadius; + + // Grab a set of faces that are within the search radius. The spatialIndex will return faces in a given radius + // from a point. This isn't useful for face to face snapping because the face closest to the search point isn't + // necessarily close to any other face. Instead we grab a dump of all the faces nearby and use + // FindClosestFaces() to check each source face against every nearby face to determine which pair of faces are + // closest. + List> targetFaces; + // Increasing the number of nearby faces to 300 allows us to snap from a greater distance. + PeltzerMain.Instance.GetSpatialIndex().FindFacesClosestTo(sourceMeshOffset, faceClosenessThresholdModelSpace, + /*ignoreInFace*/ false, out targetFaces, /*limit*/ 300); + + // If there are any nearby faces to the mesh parse through the faces and compare them to find the closest pair. + // Just because there are target faces doesn't mean that they are valid canditates for snapping. Faces can only + // be snapped together if they point towards each other or the physical separation from face to face is below + // a threshold. + if (targetFaces.Count > 0) + { + FaceSnapSpace tempSnapSpace = null; + float closestSeparation = Mathf.Infinity; + + // Iterate through every pair and compare the faces. + foreach (Face sourceFace in sourceMesh.GetFaces()) + { + // Find the position of the vertices in model space for the sourceMeshFace. + List sourceMeshFaceVerticesInModelSpace = MeshMath.CalculateVertexPositions( + sourceFace.vertexIds, + sourceMesh, + sourceMeshOffset, + sourceMeshRotation); + + Vector3 sourceFaceNormal = MeshMath.CalculateNormal(sourceMeshFaceVerticesInModelSpace); + Vector3 sourceFaceCenter = MeshMath.CalculateGeometricCenter(sourceMeshFaceVerticesInModelSpace); + + foreach (DistancePair distancePair in targetFaces) + { + FaceKey targetFaceKey = distancePair.value; + + // Don't try to snap to a hidden mesh. + if (PeltzerMain.Instance.GetModel().IsMeshHidden(targetFaceKey.meshId)) + { + continue; + } + + FaceInfo targetFaceInfo = PeltzerMain.Instance.GetSpatialIndex().GetFaceInfo(targetFaceKey); + + // Now we can calculate the separation between the faces. This is a combination of the physical distance + // between the faces and how flush they are. See bug for a diagram on how we calculate separation. + + // We only want to snap faces that are pointing towards each other so we can breakout by checking the + // angle of the normals. If its 90 degrees or less the faces point away from each other. + if (Vector3.Angle(sourceFaceNormal, targetFaceInfo.plane.normal) <= 90f) + { + continue; + } + + // A ray out of the source face center along the normal of the source face. + Ray normalRay = new Ray(sourceFaceCenter, sourceFaceNormal); + + // A ray out of the source face center along the inverse of the target face's normal. This is the + // "straight down" projection of the source center onto the target face. + Ray projectionRay = new Ray(sourceFaceCenter, -targetFaceInfo.plane.normal); + // Find the distance from the source face center to the target face plane along the normal and projection. + // The normalLength is a measure of flushness. Small distances along the normal mean the faces point + // towards each other and are flush. + float normalLength; + // The projectionLength is a measure of closeness. Small distances along the projection mean the faces are + // physically close to each other. + float projectionLength; + targetFaceInfo.plane.Raycast(normalRay, out normalLength); + targetFaceInfo.plane.Raycast(projectionRay, out projectionLength); + + // Calculate the separation which is the sum of these lengths. Not taking the average favours faces that + // are close and flush. + float separation = Mathf.Abs(normalLength) + Mathf.Abs(projectionLength); + + if (separation < closestSeparation) + { + closestSeparation = separation; + // Calculate the snapPoint on the target face while we have already done all the heavy calculations. + Vector3 snapPoint = projectionRay.GetPoint(projectionLength); + FaceKey sourceFaceKey = new FaceKey(sourceMesh.id, sourceFace.id); + tempSnapSpace = new FaceSnapSpace(sourceMesh, sourceFaceKey, targetFaceKey, sourceFaceCenter, + targetFaceInfo.baryCenter, snapPoint); + tempSnapSpace.sourceMeshOffset = sourceMeshOffset; + tempSnapSpace.sourceMeshRotation = sourceMeshRotation; + } + } + } - /// - /// Determines if there is a pair of appropriate faces that should be snapped together and maintains all the - /// snapInfo from detecting the snap to be reused when the snap is actually performed. - /// - /// The model space radius required to encapsulate the source mesh. - /// Whether there are faces to snap together. - private bool DetectFaceSnap(MMesh sourceMesh, Vector3 sourceMeshOffset, Quaternion sourceMeshRotation, - float sourceRadius) { - // Calculate the search radius for finding nearby faces. This is the radius of the sphere needed to encapsulate - // the sourceMesh plus a world space threshold. Defining the threshold in world space accounts for a user's - // available precision and their perspective. - float faceClosenessThresholdModelSpace = - FACE_DETECTION_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale - + sourceRadius; - - // Grab a set of faces that are within the search radius. The spatialIndex will return faces in a given radius - // from a point. This isn't useful for face to face snapping because the face closest to the search point isn't - // necessarily close to any other face. Instead we grab a dump of all the faces nearby and use - // FindClosestFaces() to check each source face against every nearby face to determine which pair of faces are - // closest. - List> targetFaces; - // Increasing the number of nearby faces to 300 allows us to snap from a greater distance. - PeltzerMain.Instance.GetSpatialIndex().FindFacesClosestTo(sourceMeshOffset, faceClosenessThresholdModelSpace, - /*ignoreInFace*/ false, out targetFaces, /*limit*/ 300); - - // If there are any nearby faces to the mesh parse through the faces and compare them to find the closest pair. - // Just because there are target faces doesn't mean that they are valid canditates for snapping. Faces can only - // be snapped together if they point towards each other or the physical separation from face to face is below - // a threshold. - if (targetFaces.Count > 0) { - FaceSnapSpace tempSnapSpace = null; - float closestSeparation = Mathf.Infinity; - - // Iterate through every pair and compare the faces. - foreach (Face sourceFace in sourceMesh.GetFaces()) { - // Find the position of the vertices in model space for the sourceMeshFace. - List sourceMeshFaceVerticesInModelSpace = MeshMath.CalculateVertexPositions( - sourceFace.vertexIds, - sourceMesh, - sourceMeshOffset, - sourceMeshRotation); - - Vector3 sourceFaceNormal = MeshMath.CalculateNormal(sourceMeshFaceVerticesInModelSpace); - Vector3 sourceFaceCenter = MeshMath.CalculateGeometricCenter(sourceMeshFaceVerticesInModelSpace); - - foreach (DistancePair distancePair in targetFaces) { - FaceKey targetFaceKey = distancePair.value; - - // Don't try to snap to a hidden mesh. - if (PeltzerMain.Instance.GetModel().IsMeshHidden(targetFaceKey.meshId)) { - continue; + // We found a face to snap to! + if (tempSnapSpace != null) + { + // But first see if we should be sticking the initial snapPoint of the FaceSnapSpace. We didn't do this + // while comparing faces to avoid extraneous calculations on faces we didn't end up using to snap. + + // Check to see if the initialSnapPoint is close enough to the center of the target face. If it is we should + // stick to the center by overriding snapPosition with the target face's center. + float distanceFromCenter = Vector3.Distance(tempSnapSpace.initialSnapPoint, tempSnapSpace.targetFaceCenter); + tempSnapSpace.initialSnapPoint = distanceFromCenter + < CoordinateSystem.STICK_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale + ? tempSnapSpace.targetFaceCenter : tempSnapSpace.initialSnapPoint; + + ChangeSnapSpace(tempSnapSpace); + return true; + } } - FaceInfo targetFaceInfo = PeltzerMain.Instance.GetSpatialIndex().GetFaceInfo(targetFaceKey); - - // Now we can calculate the separation between the faces. This is a combination of the physical distance - // between the faces and how flush they are. See bug for a diagram on how we calculate separation. + return false; + } - // We only want to snap faces that are pointing towards each other so we can breakout by checking the - // angle of the normals. If its 90 degrees or less the faces point away from each other. - if (Vector3.Angle(sourceFaceNormal, targetFaceInfo.plane.normal) <= 90f) { - continue; + /// + /// Changes the currently detected snapSpace to a new one if there are any changes. Will toggle off old UI hints + /// and turn on any new ones. + /// + /// The newly detected SnapSpace. + private void ChangeSnapSpace(SnapSpace newSnapSpace) + { + SnapSpace previousSnapSpace = snapSpace; + + // Check to see if the snapSpace hasn't actually changed since the last detection. If it hasn't we can avoid + // updating UI elements. + if (newSnapSpace.Equals(previousSnapSpace)) + { + switch (newSnapSpace.SnapType) + { + case SnapType.FACE: + // TODO (bug): Update the snap line for the new initialSnapPoint. FaceSnapSpace.Equals ignores initialSnapPoint. + FaceSnapSpace tempFaceSpace = (FaceSnapSpace)newSnapSpace; + if (Features.showSnappingGuides) + { + continuousFaceSnapEffect.UpdateFromSnapSpace(newSnapSpace as FaceSnapSpace); + continuousSourcePointSnapEffect.UpdateFromPoint(tempFaceSpace.sourceFaceCenter); + continuousTargetPointSnapEffect.UpdateFromPoint(tempFaceSpace.snapPoint); + //rope.UpdatePosition(PeltzerMain.Instance.worldSpace.ModelToWorld(tempFaceSpace.sourceFaceCenter), + // PeltzerMain.Instance.worldSpace.ModelToWorld(tempFaceSpace.initialSnapPoint)); + } + // Overwrite snapSpace so that it has the new initialSnapPoint. + snapSpace = newSnapSpace; + break; + case SnapType.MESH: + MeshSnapSpace tempMeshSpace = (MeshSnapSpace)newSnapSpace; + continuousMeshSnapEffect.UpdateFromSnapSpace(tempMeshSpace); + continuousSourcePointSnapEffect.UpdateFromPoint(tempMeshSpace.unsnappedPosition); + continuousTargetPointSnapEffect.UpdateFromPoint(tempMeshSpace.snappedPosition); + break; + default: + break; + } + return; } - // A ray out of the source face center along the normal of the source face. - Ray normalRay = new Ray(sourceFaceCenter, sourceFaceNormal); - - // A ray out of the source face center along the inverse of the target face's normal. This is the - // "straight down" projection of the source center onto the target face. - Ray projectionRay = new Ray(sourceFaceCenter, -targetFaceInfo.plane.normal); - // Find the distance from the source face center to the target face plane along the normal and projection. - // The normalLength is a measure of flushness. Small distances along the normal mean the faces point - // towards each other and are flush. - float normalLength; - // The projectionLength is a measure of closeness. Small distances along the projection mean the faces are - // physically close to each other. - float projectionLength; - targetFaceInfo.plane.Raycast(normalRay, out normalLength); - targetFaceInfo.plane.Raycast(projectionRay, out projectionLength); - - // Calculate the separation which is the sum of these lengths. Not taking the average favours faces that - // are close and flush. - float separation = Mathf.Abs(normalLength) + Mathf.Abs(projectionLength); - - if (separation < closestSeparation) { - closestSeparation = separation; - // Calculate the snapPoint on the target face while we have already done all the heavy calculations. - Vector3 snapPoint = projectionRay.GetPoint(projectionLength); - FaceKey sourceFaceKey = new FaceKey(sourceMesh.id, sourceFace.id); - tempSnapSpace = new FaceSnapSpace(sourceMesh, sourceFaceKey, targetFaceKey, sourceFaceCenter, - targetFaceInfo.baryCenter, snapPoint); - tempSnapSpace.sourceMeshOffset = sourceMeshOffset; - tempSnapSpace.sourceMeshRotation = sourceMeshRotation; + if (previousSnapSpace != null) + { + HideGuides(); + // Turn off any previous highlights. + switch (previousSnapSpace.SnapType) + { + case SnapType.UNIVERSAL: + // There are currently no Universal snap hints. + break; + case SnapType.MESH: + continuousMeshSnapEffect.Finish(); + UXEffectManager.GetEffectManager().EndEffect(continuousSourcePointSnapEffect); + UXEffectManager.GetEffectManager().EndEffect(continuousTargetPointSnapEffect); + break; + case SnapType.FACE: + UXEffectManager.GetEffectManager().EndEffect(continuousFaceSnapEffect); + UXEffectManager.GetEffectManager().EndEffect(continuousSourcePointSnapEffect); + UXEffectManager.GetEffectManager().EndEffect(continuousTargetPointSnapEffect); + break; + } } - } - } - - // We found a face to snap to! - if (tempSnapSpace != null) { - // But first see if we should be sticking the initial snapPoint of the FaceSnapSpace. We didn't do this - // while comparing faces to avoid extraneous calculations on faces we didn't end up using to snap. - // Check to see if the initialSnapPoint is close enough to the center of the target face. If it is we should - // stick to the center by overriding snapPosition with the target face's center. - float distanceFromCenter = Vector3.Distance(tempSnapSpace.initialSnapPoint, tempSnapSpace.targetFaceCenter); - tempSnapSpace.initialSnapPoint = distanceFromCenter - < CoordinateSystem.STICK_THRESHOLD_WORLDSPACE / PeltzerMain.Instance.worldSpace.scale - ? tempSnapSpace.targetFaceCenter : tempSnapSpace.initialSnapPoint; - - ChangeSnapSpace(tempSnapSpace); - return true; - } - } - - return false; - } - - /// - /// Changes the currently detected snapSpace to a new one if there are any changes. Will toggle off old UI hints - /// and turn on any new ones. - /// - /// The newly detected SnapSpace. - private void ChangeSnapSpace(SnapSpace newSnapSpace) { - SnapSpace previousSnapSpace = snapSpace; - - // Check to see if the snapSpace hasn't actually changed since the last detection. If it hasn't we can avoid - // updating UI elements. - if (newSnapSpace.Equals(previousSnapSpace)) { - switch (newSnapSpace.SnapType) { - case SnapType.FACE: - // TODO (bug): Update the snap line for the new initialSnapPoint. FaceSnapSpace.Equals ignores initialSnapPoint. - FaceSnapSpace tempFaceSpace = (FaceSnapSpace)newSnapSpace; - if (Features.showSnappingGuides) { - continuousFaceSnapEffect.UpdateFromSnapSpace(newSnapSpace as FaceSnapSpace); - continuousSourcePointSnapEffect.UpdateFromPoint(tempFaceSpace.sourceFaceCenter); - continuousTargetPointSnapEffect.UpdateFromPoint(tempFaceSpace.snapPoint); - //rope.UpdatePosition(PeltzerMain.Instance.worldSpace.ModelToWorld(tempFaceSpace.sourceFaceCenter), - // PeltzerMain.Instance.worldSpace.ModelToWorld(tempFaceSpace.initialSnapPoint)); + // Turn on any new highlights. + switch (newSnapSpace.SnapType) + { + case SnapType.UNIVERSAL: + // There are currently no Universal snap hints. + HideGuides(); + break; + case SnapType.MESH: + MeshSnapSpace tempMeshSpace = (MeshSnapSpace)newSnapSpace; + continuousMeshSnapEffect.UpdateFromSnapSpace(tempMeshSpace); + UXEffectManager.GetEffectManager().StartEffect(continuousSourcePointSnapEffect); + continuousSourcePointSnapEffect.UpdateFromPoint(tempMeshSpace.unsnappedPosition); + UXEffectManager.GetEffectManager().StartEffect(continuousTargetPointSnapEffect); + continuousTargetPointSnapEffect.UpdateFromPoint(tempMeshSpace.snappedPosition); + // TODO (bug): Turn on the mesh snap hint for newSnapSpace.targetMeshId. + break; + case SnapType.FACE: + FaceSnapSpace tempFaceSpace = (FaceSnapSpace)newSnapSpace; + // TODO (bug): Turn on the hints for the new faces. + if (Features.showSnappingGuides) + { + UXEffectManager.GetEffectManager().StartEffect(continuousFaceSnapEffect); + continuousFaceSnapEffect.UpdateFromSnapSpace(tempFaceSpace); + UXEffectManager.GetEffectManager().StartEffect(continuousSourcePointSnapEffect); + continuousSourcePointSnapEffect.UpdateFromPoint(tempFaceSpace.sourceFaceCenter); + UXEffectManager.GetEffectManager().StartEffect(continuousTargetPointSnapEffect); + continuousTargetPointSnapEffect.UpdateFromPoint(tempFaceSpace.snapPoint); + } + break; } - // Overwrite snapSpace so that it has the new initialSnapPoint. + snapSpace = newSnapSpace; - break; - case SnapType.MESH: - MeshSnapSpace tempMeshSpace = (MeshSnapSpace)newSnapSpace; - continuousMeshSnapEffect.UpdateFromSnapSpace(tempMeshSpace); - continuousSourcePointSnapEffect.UpdateFromPoint(tempMeshSpace.unsnappedPosition); - continuousTargetPointSnapEffect.UpdateFromPoint(tempMeshSpace.snappedPosition); - break; - default: - break; - } - return; - } - - if (previousSnapSpace != null) { - HideGuides(); - // Turn off any previous highlights. - switch (previousSnapSpace.SnapType) { - case SnapType.UNIVERSAL: - // There are currently no Universal snap hints. - break; - case SnapType.MESH: - continuousMeshSnapEffect.Finish(); - UXEffectManager.GetEffectManager().EndEffect(continuousSourcePointSnapEffect); - UXEffectManager.GetEffectManager().EndEffect(continuousTargetPointSnapEffect); - break; - case SnapType.FACE: - UXEffectManager.GetEffectManager().EndEffect(continuousFaceSnapEffect); - UXEffectManager.GetEffectManager().EndEffect(continuousSourcePointSnapEffect); - UXEffectManager.GetEffectManager().EndEffect(continuousTargetPointSnapEffect); - break; } - } - - // Turn on any new highlights. - switch (newSnapSpace.SnapType) { - case SnapType.UNIVERSAL: - // There are currently no Universal snap hints. - HideGuides(); - break; - case SnapType.MESH: - MeshSnapSpace tempMeshSpace = (MeshSnapSpace) newSnapSpace; - continuousMeshSnapEffect.UpdateFromSnapSpace(tempMeshSpace); - UXEffectManager.GetEffectManager().StartEffect(continuousSourcePointSnapEffect); - continuousSourcePointSnapEffect.UpdateFromPoint(tempMeshSpace.unsnappedPosition); - UXEffectManager.GetEffectManager().StartEffect(continuousTargetPointSnapEffect); - continuousTargetPointSnapEffect.UpdateFromPoint(tempMeshSpace.snappedPosition); - // TODO (bug): Turn on the mesh snap hint for newSnapSpace.targetMeshId. - break; - case SnapType.FACE: - FaceSnapSpace tempFaceSpace = (FaceSnapSpace)newSnapSpace; - // TODO (bug): Turn on the hints for the new faces. - if (Features.showSnappingGuides) { - UXEffectManager.GetEffectManager().StartEffect(continuousFaceSnapEffect); - continuousFaceSnapEffect.UpdateFromSnapSpace(tempFaceSpace); - UXEffectManager.GetEffectManager().StartEffect(continuousSourcePointSnapEffect); - continuousSourcePointSnapEffect.UpdateFromPoint(tempFaceSpace.sourceFaceCenter); - UXEffectManager.GetEffectManager().StartEffect(continuousTargetPointSnapEffect); - continuousTargetPointSnapEffect.UpdateFromPoint(tempFaceSpace.snapPoint); - } - break; - } - - snapSpace = newSnapSpace; } - } } diff --git a/Assets/Scripts/model/core/SnapGrid.cs b/Assets/Scripts/model/core/SnapGrid.cs index b8b5e307..5616e889 100644 --- a/Assets/Scripts/model/core/SnapGrid.cs +++ b/Assets/Scripts/model/core/SnapGrid.cs @@ -21,897 +21,977 @@ using com.google.apps.peltzer.client.alignment; using System.Collections.ObjectModel; -namespace com.google.apps.peltzer.client.model.core { - /// - /// Representation of an Axes which is made of three vectors. Each vector represents the right, up, and forward - /// orientation of space. - /// - public class Axes { - public static Axes identity = new Axes(Vector3.right, Vector3.up, Vector3.forward); - public enum Axis { RIGHT, UP, FORWARD }; - - public Vector3 right; - public Vector3 up; - public Vector3 forward; - - public Axes(Vector3 right, Vector3 up, Vector3 forward) { - this.right = right; - this.up = up; - this.forward = forward; - } +namespace com.google.apps.peltzer.client.model.core +{ + /// + /// Representation of an Axes which is made of three vectors. Each vector represents the right, up, and forward + /// orientation of space. + /// + public class Axes + { + public static Axes identity = new Axes(Vector3.right, Vector3.up, Vector3.forward); + public enum Axis { RIGHT, UP, FORWARD }; + + public Vector3 right; + public Vector3 up; + public Vector3 forward; + + public Axes(Vector3 right, Vector3 up, Vector3 forward) + { + this.right = right; + this.up = up; + this.forward = forward; + } - /// - /// Finds the rotation from one Axes to another. - /// - /// The starting Axes. - /// The final Axes. - /// The rotational difference between the two Axes'. - public static Quaternion FromToRotation(Axes from, Axes to) { - // To find the rotation between two Axes we only need to find the difference between two of the axes since a - // pair of axes must move together to maintain the 90 degree angles between them. So we will start by moving - // one arbitrary Axes into place. Then applying this change to a second Axes and finding the remaining - // difference between this Axes and the universal grid axes. - - // Start by finding the rotation difference between the to.up axis and the from.up. - Quaternion yRotation = Quaternion.FromToRotation(from.up, to.up); - - // Apply the yRotation to the from.right then find the difference between the partially rotated from.right axis - // and the to.right axis. - Quaternion residualRotation = Quaternion.FromToRotation(yRotation * from.right, to.right); - - // Combine the rotations to find the rotational difference. - return residualRotation * yRotation; - } + /// + /// Finds the rotation from one Axes to another. + /// + /// The starting Axes. + /// The final Axes. + /// The rotational difference between the two Axes'. + public static Quaternion FromToRotation(Axes from, Axes to) + { + // To find the rotation between two Axes we only need to find the difference between two of the axes since a + // pair of axes must move together to maintain the 90 degree angles between them. So we will start by moving + // one arbitrary Axes into place. Then applying this change to a second Axes and finding the remaining + // difference between this Axes and the universal grid axes. + + // Start by finding the rotation difference between the to.up axis and the from.up. + Quaternion yRotation = Quaternion.FromToRotation(from.up, to.up); + + // Apply the yRotation to the from.right then find the difference between the partially rotated from.right axis + // and the to.right axis. + Quaternion residualRotation = Quaternion.FromToRotation(yRotation * from.right, to.right); + + // Combine the rotations to find the rotational difference. + return residualRotation * yRotation; + } - /// - /// Given the coplanarVertices of face determine the axes for the face. - /// - /// Vertices of the face. - /// The axes that define the face. - public static Axes FindAxesForAFace(List coplanarVertices) { - Vector3 forward = FindForwardAxis(coplanarVertices); - Vector3 right = FindRightAxis(coplanarVertices); - Vector3 up = FindUpAxis(forward, right); - - return new Axes(right, up, forward); - } + /// + /// Given the coplanarVertices of face determine the axes for the face. + /// + /// Vertices of the face. + /// The axes that define the face. + public static Axes FindAxesForAFace(List coplanarVertices) + { + Vector3 forward = FindForwardAxis(coplanarVertices); + Vector3 right = FindRightAxis(coplanarVertices); + Vector3 up = FindUpAxis(forward, right); + + return new Axes(right, up, forward); + } - /// - /// Finds the forward axis given the veritces of a face, which is just the normal out of the origin. - /// - /// The vertices of a face. - /// The forward axis. - public static Vector3 FindForwardAxis(List coplanarVertices) { - return MeshMath.CalculateNormal(coplanarVertices); - } + /// + /// Finds the forward axis given the veritces of a face, which is just the normal out of the origin. + /// + /// The vertices of a face. + /// The forward axis. + public static Vector3 FindForwardAxis(List coplanarVertices) + { + return MeshMath.CalculateNormal(coplanarVertices); + } - /// - /// Finds the right axis of a face by comparing all the edges in the face and choosing an edge for the right axis - /// that is the most representative of the other edges. Essentially we are trying to find an edge that is - /// perpendicular to as many edges as possible so that we can rotate the preview to align to the greatest number - /// of edges. Since the forward axis is the face normal any edge is guaranteed to be perpendicular to the normal. - /// - /// The vertices of a face. - /// The right axis. - public static Vector3 FindRightAxis(List coplanarVertices) { - EdgeInfo mostRepresentativeEdge = MeshMath.FindMostRepresentativeEdge(coplanarVertices); - return mostRepresentativeEdge.edgeVector.normalized; - } + /// + /// Finds the right axis of a face by comparing all the edges in the face and choosing an edge for the right axis + /// that is the most representative of the other edges. Essentially we are trying to find an edge that is + /// perpendicular to as many edges as possible so that we can rotate the preview to align to the greatest number + /// of edges. Since the forward axis is the face normal any edge is guaranteed to be perpendicular to the normal. + /// + /// The vertices of a face. + /// The right axis. + public static Vector3 FindRightAxis(List coplanarVertices) + { + EdgeInfo mostRepresentativeEdge = MeshMath.FindMostRepresentativeEdge(coplanarVertices); + return mostRepresentativeEdge.edgeVector.normalized; + } - /// - /// Finds the up axis which is just the axis perpendicular to the right and forward axis. We use the left hand - /// rule to make sure the up axis points the right way so snapGrid.forward, up and right are related the same - /// as the universal Vector3.forward, up and right. - /// - /// The right axis. - /// The forward axis. - /// The cross product of the right and forward axis. - public static Vector3 FindUpAxis(Vector3 right, Vector3 forward) { - return Vector3.Cross(forward, right).normalized; - } - } - - /// - /// Holds a snaptransform as well as any additional information needed to render UX effects related to Snapping. - /// - public struct SnapInfo { - // The transform needed to snap the object to the grid - internal SnapTransform transform; - // The point on the surface that we're snapping to in face snap - internal Vector3 snapPoint; - // The normal of that point - internal Vector3 snapNormal; - // The position of the face that is being snapped - internal Vector3 snappingFacePosition; - // Whether we're within the threshhold to snap directly to the surface - internal bool inSurfaceThreshhold; - } - - public class SnapGrid { - public enum SnapType { NONE, VERTEX, FACE, MESH, UNIVERSAL }; - // TODO (bug) These thresholds are not optimized and should be calibrated following more testing. - /// - /// Floating point error threshold for comparing angles. - /// - private const float angleThreshold = 0.01f; - /// - /// A threshold above which a face is considered too far away to select. - /// This is in Unity units, where 1.0f = 1 meter by default and were chosen by testing and iterating. - /// - private const float FACE_CLOSENESS_THRESHOLD_DEFAULT = 0.1f; - /// - /// A threshold above which a vertex is considered too far away to select. - /// This is in Unity units, where 1.0f = 1 meter by default and were chosen by testing and iterating. - /// - private const float VERTEX_CLOSENESS_THRESHOLD_DEFAULT = 0.005f; - /// - /// A threshold above which the angle (in degrees) between two faces is too great to snap them together. The - /// angle between two faces is defined as the rotational difference to make them flush. - /// Chosen to be smaller than the angle between any two faces on a primitive. - /// - private const float FACE_ANGLE_THRESHOLD_DEFAULT = 25.0f; - /// - /// Threshold for surface snapping. - /// - private float surfaceThreshold; - /// - /// Threshold for center snapping. - /// - private float centerThreshold; - /// - /// Threshold for edge snapping. - /// - private float edgeThreshold; - /// - /// The anchor type that generated this snapGrid. - /// - public SnapType snapType; - /// - /// The origin of the snapGrid. - /// - private Vector3 origin; - /// - /// The positional difference between the snapGrid and the universal grid. - /// - private Vector3 offset; - /// - /// The rotation of the snapGrid. - /// - private Quaternion rotation = Quaternion.identity; - /// - /// The SnapGrid normal. This is either the normal of the snapFace, the vertexNormal or zero if - /// snapType = MESH or UNIVERSAL. - /// - private Vector3 normal; - /// - /// The three vectors that represent the Axes of the snapGrid. - /// - private Axes snapAxes; - /// - /// A position used as the center for snapping. Not necessarily the same as the origin. - /// - public Vector3 snapCenter; - /// - /// The clockwise, coplanar vertices that make up the snapFace. - /// - private List coplanarSnapFaceVertices; - private static EdgeInfo mostRepresentativeEdge; - private bool hasComparableFace; - - public SnapGrid(MMesh previewMesh, Vector3 previewMeshOffset, Quaternion previewMeshRotation, Model model, - SpatialIndex spatialIndex, WorldSpace worldSpace, bool isSubtract, out Quaternion finalPreviewMeshRotation, - out Face previewFace, out List coplanarPreviewFaceVerticesAtOrigin, out int targetMeshId) { - // Setup the thresholds for surface, center and edge snapping relative to the previewMesh size. - // TODO (bug): These thresholds are still being tested and iterated on so the repetition is being left for - // clarity. - surfaceThreshold = - Mathf.Min(previewMesh.bounds.size.x, previewMesh.bounds.size.y, previewMesh.bounds.size.z) * 2.0f; - centerThreshold = - Mathf.Min(previewMesh.bounds.size.x, previewMesh.bounds.size.y, previewMesh.bounds.size.z) * 0.75f; - edgeThreshold = - Mathf.Min(previewMesh.bounds.extents.x, previewMesh.bounds.extents.y, previewMesh.bounds.extents.z); - - // We decide whether to MeshSnap, FaceSnap, VertexSnap or UniversalSnap using the following method. - // First find all nearbyFaces, nearbyVertices, closestFace and closestVertex and cache the results then: - - // 1) If there are more than six faces on the same mesh in nearbyFaces: MeshSnap - // 2) If there are any nearbyFaces and the closestFace is closer than the closestVertex: FaceSnap - // 3) If we didn't face or mesh snap because there were no faces or closestVertex is closer than closestFace and - // there are nearbyVertices: VertexSnap - // 4) Default to the universalGrid. - - // Set a radius for selecting a face to the radius of the previewMesh plus a small default threshold. Using - // this radius we can call directly to the spatialIndex to find all nearbyFaces within the selection radius. - float faceClosenessThreshold = FACE_CLOSENESS_THRESHOLD_DEFAULT - + Mathf.Max(previewMesh.bounds.extents.x, previewMesh.bounds.extents.y, previewMesh.bounds.extents.z); - List> nearbyFacePairs; - spatialIndex.FindFacesClosestTo(previewMeshOffset, faceClosenessThreshold, /*ignoreInFace*/ false, - out nearbyFacePairs); - - // Set a radius for selecting a vertex to the radius of the previewMesh plus a small default threshold. Using - // this radius we can call directly to the spatialIndex to find all nearbyVertices within the selection radius. - float vertexClosenessThreshold = VERTEX_CLOSENESS_THRESHOLD_DEFAULT - + Mathf.Max(previewMesh.bounds.extents.x, previewMesh.bounds.extents.y, previewMesh.bounds.extents.z); - List> nearbyVertexPairs; - spatialIndex.FindVerticesClosestTo(previewMeshOffset, vertexClosenessThreshold, out nearbyVertexPairs); - - FacePair closestFace = new FacePair(); - if (nearbyFacePairs.Count > 0) { - hasComparableFace = MeshMath.FindClosestFace(nearbyFacePairs, previewMesh, previewMeshOffset, previewMeshRotation, - model, FACE_ANGLE_THRESHOLD_DEFAULT, out closestFace); - } - - FaceVertexPair closestVertex = new FaceVertexPair(); - if (nearbyVertexPairs.Count > 0) { - closestVertex = MeshMath.FindClosestVertex(nearbyVertexPairs, previewMesh, previewMeshOffset, previewMeshRotation, - model); - } - - // First try snapping to a mesh. If there are more than 3 nearby faces belonging to the same mesh the user - // probably wants to mesh snap instead of face snapping. - if (nearbyFacePairs.Count > 0) { - // Try finding a mesh to snap to. - int nearestMeshId; - if (MeshMath.TryFindingNearestMeshGivenNearbyFaces(nearbyFacePairs, 3, out nearestMeshId) || isSubtract) { - // Snap to a mesh. - MMesh nearestMesh = model.GetMesh(nearestMeshId); - - // Setup the snapGrid for mesh snapping. - SetupMeshSnapGrid(previewMeshOffset, nearestMesh); - - // Snapping to a mesh doesn't require any knowledge of the previewFace so we can pass back null. - previewFace = null; - coplanarPreviewFaceVerticesAtOrigin = null; - - // The previewMesh should copy the snapMeshes rotation to the nearest 90 degrees. - finalPreviewMeshRotation = GridUtils.SnapToNearest(previewMeshRotation, nearestMesh.rotation, 90f); - targetMeshId = nearestMeshId; - return; - } - - // If we didn't mesh snap there are still faces we could snap to. We will snap to the closetFace calculated - // early unless the closetVertex is closer than the closestFace or there are no nearby vertices. - if (hasComparableFace && (nearbyVertexPairs.Count == 0 || closestVertex.separation > closestFace.separation)) { - // First, calculate the center of the face being snapped to in model space, for comparison across meshes. - FaceKey snapFaceKey = closestFace.toFaceKey; - MMesh snapMesh = model.GetMesh(snapFaceKey.meshId); - Face snapFace = snapMesh.GetFace(snapFaceKey.faceId); - List coplanarSnapFaceVertices = new List(snapFace.vertexIds.Count); - foreach (int vertexId in snapFace.vertexIds) { - coplanarSnapFaceVertices.Add(snapMesh.VertexPositionInModelCoords(vertexId)); - } - Vector3 snapFaceCenter = MeshMath.CalculateGeometricCenter(coplanarSnapFaceVertices); - - // Next, calculate properties of the held mesh that is being snapped. - // We obtain the vertex positions at 'origin' -- ignoring the mesh's transform -- such that this - // information can be re-used without recaulcation as the mesh's transform is modified. - // We further obtain the vertex positions including the mesh's transform, to find a snap target. - FaceKey previewFaceKey = closestFace.fromFaceKey; - previewFace = previewMesh.GetFace(previewFaceKey.faceId); - coplanarPreviewFaceVerticesAtOrigin = new List(previewFace.vertexIds.Count); - List coplanarPreviewFaceVertices = new List(coplanarPreviewFaceVerticesAtOrigin.Count); - for (int i = 0; i < previewFace.vertexIds.Count; i++) { - Vector3 positionModelSpaceBeforeTransform = - previewMesh.VertexPositionInModelCoords(previewFace.vertexIds[i]); - coplanarPreviewFaceVerticesAtOrigin.Add(positionModelSpaceBeforeTransform); - coplanarPreviewFaceVertices.Add( - (previewMeshRotation * positionModelSpaceBeforeTransform) - + previewMeshOffset); - } - - // Setup the snapGrid for face snapping. - SetupFaceSnapGrid(snapFace, snapMesh, MeshMath.CalculateGeometricCenter(coplanarPreviewFaceVertices)); - - // Calculate the new rotation for the previewMesh so that the previewFace and snapFace are flush. - finalPreviewMeshRotation = - FindPreviewMeshRotationForFaceSnap(coplanarPreviewFaceVertices, previewMeshRotation); - targetMeshId = snapMesh.id; - return; - } - } - - // We haven't mesh or face snapped so either there were no faces to snap to or the closestVertex was closer - // than the closestFace and we should be vertex snapping. - if (nearbyVertexPairs.Count > 0) { - // Find the nearest vertex called the snapVertex and the mesh it belongs to called the snapMesh. - VertexKey snapVertexKey = closestVertex.vertexKey; - MMesh snapMesh = model.GetMesh(snapVertexKey.meshId); - - // Find the position of the vertex being snapped to and setup the snapGrid for vertex snapping. - Vector3 snapVertex = snapMesh.VertexPositionInModelCoords(snapVertexKey.vertexId); - SetupVertexSnapGrid(snapVertex, snapMesh); - - // Find the face on the previewMesh closest to the snapVertex. This will be returned to the tool and used on - // update for snapping. - previewFace = previewMesh.GetFace(closestVertex.faceKey.faceId); - - // Find the list of coplanar vertices on the previewFace. Finding them each update loop is expensive so we - // cache them. The position of the previewMesh is never updated which is why these vertices are marked as - // "at origin" to use them in calculations their positions will have to be updated. - coplanarPreviewFaceVerticesAtOrigin = new List(previewFace.vertexIds.Count); - for (int i = 0; i < previewFace.vertexIds.Count; i++) { - int id = previewFace.vertexIds[i]; - coplanarPreviewFaceVerticesAtOrigin.Add(previewMesh.VertexPositionInModelCoords(id)); - } - - // We need the actual position of the coplanarPreviewFaceVerticesAtOrigin so we apply the previewMesh - // rotation and offset to each Vector3. - List coplanarPreviewFaceVertices = new List(coplanarPreviewFaceVerticesAtOrigin.Count); - for (int i = 0; i < coplanarPreviewFaceVerticesAtOrigin.Count; i++) { - Vector3 vertex = coplanarPreviewFaceVerticesAtOrigin[i]; - coplanarPreviewFaceVertices.Add((previewMeshRotation * vertex) + previewMeshOffset); - } - - // Calculate the new rotation for the previewMesh so that it is balanced on the vertex. - finalPreviewMeshRotation = FindPreviewMeshRotationForVertexSnap( - MeshMath.CalculateNormal(coplanarPreviewFaceVertices), previewMeshRotation); - targetMeshId = snapMesh.id; - return; - } - targetMeshId = -1; - // If there was no vertex, face or mesh to snap to, snap to the universal grid. - SetupUniversalSnapGrid(); - - // Snapping to the universal grid doesn't require any knowledge of the previewFace so we can pass back null. - previewFace = null; - coplanarPreviewFaceVerticesAtOrigin = null; - - // The previewMesh should rotate to the nearest 90 degrees on the universal grid which has the identity - // rotation. - finalPreviewMeshRotation = GridUtils.SnapToNearest(previewMeshRotation, Quaternion.identity, 90f); + /// + /// Finds the up axis which is just the axis perpendicular to the right and forward axis. We use the left hand + /// rule to make sure the up axis points the right way so snapGrid.forward, up and right are related the same + /// as the universal Vector3.forward, up and right. + /// + /// The right axis. + /// The forward axis. + /// The cross product of the right and forward axis. + public static Vector3 FindUpAxis(Vector3 right, Vector3 forward) + { + return Vector3.Cross(forward, right).normalized; + } } /// - /// Creates a snapGrid anchored on a vertex. - /// - /// The vertex being snapped to. - /// The mesh the vertex is on. - private void SetupVertexSnapGrid(Vector3 snapVertex, MMesh mesh) { - snapType = SnapType.VERTEX; - - origin = snapVertex; - snapCenter = origin; - - // The vertex normal can be imagined as a vector from the mesh center pointing out through the vertex. - normal = snapVertex - mesh.offset; - - // The offset and rotation aren't important for snapType = VERTEX. The user can only positionally snap along - // the normal. - offset = Vector3.zero; - rotation = Quaternion.identity; - } + /// Holds a snaptransform as well as any additional information needed to render UX effects related to Snapping. + /// + public struct SnapInfo + { + // The transform needed to snap the object to the grid + internal SnapTransform transform; + // The point on the surface that we're snapping to in face snap + internal Vector3 snapPoint; + // The normal of that point + internal Vector3 snapNormal; + // The position of the face that is being snapped + internal Vector3 snappingFacePosition; + // Whether we're within the threshhold to snap directly to the surface + internal bool inSurfaceThreshhold; + } + + public class SnapGrid + { + public enum SnapType { NONE, VERTEX, FACE, MESH, UNIVERSAL }; + // TODO (bug) These thresholds are not optimized and should be calibrated following more testing. + /// + /// Floating point error threshold for comparing angles. + /// + private const float angleThreshold = 0.01f; + /// + /// A threshold above which a face is considered too far away to select. + /// This is in Unity units, where 1.0f = 1 meter by default and were chosen by testing and iterating. + /// + private const float FACE_CLOSENESS_THRESHOLD_DEFAULT = 0.1f; + /// + /// A threshold above which a vertex is considered too far away to select. + /// This is in Unity units, where 1.0f = 1 meter by default and were chosen by testing and iterating. + /// + private const float VERTEX_CLOSENESS_THRESHOLD_DEFAULT = 0.005f; + /// + /// A threshold above which the angle (in degrees) between two faces is too great to snap them together. The + /// angle between two faces is defined as the rotational difference to make them flush. + /// Chosen to be smaller than the angle between any two faces on a primitive. + /// + private const float FACE_ANGLE_THRESHOLD_DEFAULT = 25.0f; + /// + /// Threshold for surface snapping. + /// + private float surfaceThreshold; + /// + /// Threshold for center snapping. + /// + private float centerThreshold; + /// + /// Threshold for edge snapping. + /// + private float edgeThreshold; + /// + /// The anchor type that generated this snapGrid. + /// + public SnapType snapType; + /// + /// The origin of the snapGrid. + /// + private Vector3 origin; + /// + /// The positional difference between the snapGrid and the universal grid. + /// + private Vector3 offset; + /// + /// The rotation of the snapGrid. + /// + private Quaternion rotation = Quaternion.identity; + /// + /// The SnapGrid normal. This is either the normal of the snapFace, the vertexNormal or zero if + /// snapType = MESH or UNIVERSAL. + /// + private Vector3 normal; + /// + /// The three vectors that represent the Axes of the snapGrid. + /// + private Axes snapAxes; + /// + /// A position used as the center for snapping. Not necessarily the same as the origin. + /// + public Vector3 snapCenter; + /// + /// The clockwise, coplanar vertices that make up the snapFace. + /// + private List coplanarSnapFaceVertices; + private static EdgeInfo mostRepresentativeEdge; + private bool hasComparableFace; + + public SnapGrid(MMesh previewMesh, Vector3 previewMeshOffset, Quaternion previewMeshRotation, Model model, + SpatialIndex spatialIndex, WorldSpace worldSpace, bool isSubtract, out Quaternion finalPreviewMeshRotation, + out Face previewFace, out List coplanarPreviewFaceVerticesAtOrigin, out int targetMeshId) + { + // Setup the thresholds for surface, center and edge snapping relative to the previewMesh size. + // TODO (bug): These thresholds are still being tested and iterated on so the repetition is being left for + // clarity. + surfaceThreshold = + Mathf.Min(previewMesh.bounds.size.x, previewMesh.bounds.size.y, previewMesh.bounds.size.z) * 2.0f; + centerThreshold = + Mathf.Min(previewMesh.bounds.size.x, previewMesh.bounds.size.y, previewMesh.bounds.size.z) * 0.75f; + edgeThreshold = + Mathf.Min(previewMesh.bounds.extents.x, previewMesh.bounds.extents.y, previewMesh.bounds.extents.z); + + // We decide whether to MeshSnap, FaceSnap, VertexSnap or UniversalSnap using the following method. + // First find all nearbyFaces, nearbyVertices, closestFace and closestVertex and cache the results then: + + // 1) If there are more than six faces on the same mesh in nearbyFaces: MeshSnap + // 2) If there are any nearbyFaces and the closestFace is closer than the closestVertex: FaceSnap + // 3) If we didn't face or mesh snap because there were no faces or closestVertex is closer than closestFace and + // there are nearbyVertices: VertexSnap + // 4) Default to the universalGrid. + + // Set a radius for selecting a face to the radius of the previewMesh plus a small default threshold. Using + // this radius we can call directly to the spatialIndex to find all nearbyFaces within the selection radius. + float faceClosenessThreshold = FACE_CLOSENESS_THRESHOLD_DEFAULT + + Mathf.Max(previewMesh.bounds.extents.x, previewMesh.bounds.extents.y, previewMesh.bounds.extents.z); + List> nearbyFacePairs; + spatialIndex.FindFacesClosestTo(previewMeshOffset, faceClosenessThreshold, /*ignoreInFace*/ false, + out nearbyFacePairs); + + // Set a radius for selecting a vertex to the radius of the previewMesh plus a small default threshold. Using + // this radius we can call directly to the spatialIndex to find all nearbyVertices within the selection radius. + float vertexClosenessThreshold = VERTEX_CLOSENESS_THRESHOLD_DEFAULT + + Mathf.Max(previewMesh.bounds.extents.x, previewMesh.bounds.extents.y, previewMesh.bounds.extents.z); + List> nearbyVertexPairs; + spatialIndex.FindVerticesClosestTo(previewMeshOffset, vertexClosenessThreshold, out nearbyVertexPairs); + + FacePair closestFace = new FacePair(); + if (nearbyFacePairs.Count > 0) + { + hasComparableFace = MeshMath.FindClosestFace(nearbyFacePairs, previewMesh, previewMeshOffset, previewMeshRotation, + model, FACE_ANGLE_THRESHOLD_DEFAULT, out closestFace); + } + + FaceVertexPair closestVertex = new FaceVertexPair(); + if (nearbyVertexPairs.Count > 0) + { + closestVertex = MeshMath.FindClosestVertex(nearbyVertexPairs, previewMesh, previewMeshOffset, previewMeshRotation, + model); + } + + // First try snapping to a mesh. If there are more than 3 nearby faces belonging to the same mesh the user + // probably wants to mesh snap instead of face snapping. + if (nearbyFacePairs.Count > 0) + { + // Try finding a mesh to snap to. + int nearestMeshId; + if (MeshMath.TryFindingNearestMeshGivenNearbyFaces(nearbyFacePairs, 3, out nearestMeshId) || isSubtract) + { + // Snap to a mesh. + MMesh nearestMesh = model.GetMesh(nearestMeshId); + + // Setup the snapGrid for mesh snapping. + SetupMeshSnapGrid(previewMeshOffset, nearestMesh); + + // Snapping to a mesh doesn't require any knowledge of the previewFace so we can pass back null. + previewFace = null; + coplanarPreviewFaceVerticesAtOrigin = null; + + // The previewMesh should copy the snapMeshes rotation to the nearest 90 degrees. + finalPreviewMeshRotation = GridUtils.SnapToNearest(previewMeshRotation, nearestMesh.rotation, 90f); + targetMeshId = nearestMeshId; + return; + } + + // If we didn't mesh snap there are still faces we could snap to. We will snap to the closetFace calculated + // early unless the closetVertex is closer than the closestFace or there are no nearby vertices. + if (hasComparableFace && (nearbyVertexPairs.Count == 0 || closestVertex.separation > closestFace.separation)) + { + // First, calculate the center of the face being snapped to in model space, for comparison across meshes. + FaceKey snapFaceKey = closestFace.toFaceKey; + MMesh snapMesh = model.GetMesh(snapFaceKey.meshId); + Face snapFace = snapMesh.GetFace(snapFaceKey.faceId); + List coplanarSnapFaceVertices = new List(snapFace.vertexIds.Count); + foreach (int vertexId in snapFace.vertexIds) + { + coplanarSnapFaceVertices.Add(snapMesh.VertexPositionInModelCoords(vertexId)); + } + Vector3 snapFaceCenter = MeshMath.CalculateGeometricCenter(coplanarSnapFaceVertices); + + // Next, calculate properties of the held mesh that is being snapped. + // We obtain the vertex positions at 'origin' -- ignoring the mesh's transform -- such that this + // information can be re-used without recaulcation as the mesh's transform is modified. + // We further obtain the vertex positions including the mesh's transform, to find a snap target. + FaceKey previewFaceKey = closestFace.fromFaceKey; + previewFace = previewMesh.GetFace(previewFaceKey.faceId); + coplanarPreviewFaceVerticesAtOrigin = new List(previewFace.vertexIds.Count); + List coplanarPreviewFaceVertices = new List(coplanarPreviewFaceVerticesAtOrigin.Count); + for (int i = 0; i < previewFace.vertexIds.Count; i++) + { + Vector3 positionModelSpaceBeforeTransform = + previewMesh.VertexPositionInModelCoords(previewFace.vertexIds[i]); + coplanarPreviewFaceVerticesAtOrigin.Add(positionModelSpaceBeforeTransform); + coplanarPreviewFaceVertices.Add( + (previewMeshRotation * positionModelSpaceBeforeTransform) + + previewMeshOffset); + } + + // Setup the snapGrid for face snapping. + SetupFaceSnapGrid(snapFace, snapMesh, MeshMath.CalculateGeometricCenter(coplanarPreviewFaceVertices)); + + // Calculate the new rotation for the previewMesh so that the previewFace and snapFace are flush. + finalPreviewMeshRotation = + FindPreviewMeshRotationForFaceSnap(coplanarPreviewFaceVertices, previewMeshRotation); + targetMeshId = snapMesh.id; + return; + } + } + + // We haven't mesh or face snapped so either there were no faces to snap to or the closestVertex was closer + // than the closestFace and we should be vertex snapping. + if (nearbyVertexPairs.Count > 0) + { + // Find the nearest vertex called the snapVertex and the mesh it belongs to called the snapMesh. + VertexKey snapVertexKey = closestVertex.vertexKey; + MMesh snapMesh = model.GetMesh(snapVertexKey.meshId); + + // Find the position of the vertex being snapped to and setup the snapGrid for vertex snapping. + Vector3 snapVertex = snapMesh.VertexPositionInModelCoords(snapVertexKey.vertexId); + SetupVertexSnapGrid(snapVertex, snapMesh); + + // Find the face on the previewMesh closest to the snapVertex. This will be returned to the tool and used on + // update for snapping. + previewFace = previewMesh.GetFace(closestVertex.faceKey.faceId); + + // Find the list of coplanar vertices on the previewFace. Finding them each update loop is expensive so we + // cache them. The position of the previewMesh is never updated which is why these vertices are marked as + // "at origin" to use them in calculations their positions will have to be updated. + coplanarPreviewFaceVerticesAtOrigin = new List(previewFace.vertexIds.Count); + for (int i = 0; i < previewFace.vertexIds.Count; i++) + { + int id = previewFace.vertexIds[i]; + coplanarPreviewFaceVerticesAtOrigin.Add(previewMesh.VertexPositionInModelCoords(id)); + } + + // We need the actual position of the coplanarPreviewFaceVerticesAtOrigin so we apply the previewMesh + // rotation and offset to each Vector3. + List coplanarPreviewFaceVertices = new List(coplanarPreviewFaceVerticesAtOrigin.Count); + for (int i = 0; i < coplanarPreviewFaceVerticesAtOrigin.Count; i++) + { + Vector3 vertex = coplanarPreviewFaceVerticesAtOrigin[i]; + coplanarPreviewFaceVertices.Add((previewMeshRotation * vertex) + previewMeshOffset); + } + + // Calculate the new rotation for the previewMesh so that it is balanced on the vertex. + finalPreviewMeshRotation = FindPreviewMeshRotationForVertexSnap( + MeshMath.CalculateNormal(coplanarPreviewFaceVertices), previewMeshRotation); + targetMeshId = snapMesh.id; + return; + } + targetMeshId = -1; + // If there was no vertex, face or mesh to snap to, snap to the universal grid. + SetupUniversalSnapGrid(); + + // Snapping to the universal grid doesn't require any knowledge of the previewFace so we can pass back null. + previewFace = null; + coplanarPreviewFaceVerticesAtOrigin = null; + + // The previewMesh should rotate to the nearest 90 degrees on the universal grid which has the identity + // rotation. + finalPreviewMeshRotation = GridUtils.SnapToNearest(previewMeshRotation, Quaternion.identity, 90f); + } - /// - /// Creates a snapGrid anchored on a face. - /// - /// The face being snapped to. - /// The mesh the face belongs to. - /// The position of the snap. - private void SetupFaceSnapGrid(Face snapFace, MMesh mesh, Vector3 previewFacePosition) { - snapType = SnapType.FACE; - - // Find the clockwise positions of the vertices that make up the face. - coplanarSnapFaceVertices = new List(snapFace.vertexIds.Count); - for (int i = 0; i < snapFace.vertexIds.Count; i++) { - int id = snapFace.vertexIds[i]; - coplanarSnapFaceVertices.Add(mesh.VertexPositionInModelCoords(id)); - } - - // Find the two vertices that make up the closest edge. The grid will be anchored on this edge. - KeyValuePair closestEdgeEndPoints = - MeshMath.FindClosestEdgeInFace(previewFacePosition, coplanarSnapFaceVertices); - - origin = FindFaceOrigin(previewFacePosition, closestEdgeEndPoints); - offset = FindOffset(origin); - - // Find the three face axes that will represent the snapGrid. - Vector3 forward = FindForwardAxis(coplanarSnapFaceVertices); - Vector3 right = FindBestRightAxis(coplanarSnapFaceVertices); - Vector3 up = FindUpAxis(right, forward); - snapAxes = new Axes(right, up, forward); - - // Find the rotational difference from the universal axes to the axes of the face. - rotation = FromToRotation(new Axes(Vector3.right, Vector3.up, Vector3.forward), snapAxes); - normal = forward; - snapCenter = MeshMath.CalculateGeometricCenter(coplanarSnapFaceVertices); - } + /// + /// Creates a snapGrid anchored on a vertex. + /// + /// The vertex being snapped to. + /// The mesh the vertex is on. + private void SetupVertexSnapGrid(Vector3 snapVertex, MMesh mesh) + { + snapType = SnapType.VERTEX; + + origin = snapVertex; + snapCenter = origin; + + // The vertex normal can be imagined as a vector from the mesh center pointing out through the vertex. + normal = snapVertex - mesh.offset; + + // The offset and rotation aren't important for snapType = VERTEX. The user can only positionally snap along + // the normal. + offset = Vector3.zero; + rotation = Quaternion.identity; + } - /// - /// Creates a snapGrid anchored to a mesh. - /// - /// The position of the preview at start of snap. - /// The mesh being snapped to. - private void SetupMeshSnapGrid(Vector3 positionToSnap, MMesh mesh) { - snapType = SnapType.MESH; - - origin = mesh.offset; - offset = FindOffset(origin); - rotation = mesh.rotation; - snapCenter = origin; - normal = GridUtils.FindNearestLocalMeshAxis(positionToSnap - snapCenter, rotation); - } + /// + /// Creates a snapGrid anchored on a face. + /// + /// The face being snapped to. + /// The mesh the face belongs to. + /// The position of the snap. + private void SetupFaceSnapGrid(Face snapFace, MMesh mesh, Vector3 previewFacePosition) + { + snapType = SnapType.FACE; + + // Find the clockwise positions of the vertices that make up the face. + coplanarSnapFaceVertices = new List(snapFace.vertexIds.Count); + for (int i = 0; i < snapFace.vertexIds.Count; i++) + { + int id = snapFace.vertexIds[i]; + coplanarSnapFaceVertices.Add(mesh.VertexPositionInModelCoords(id)); + } + + // Find the two vertices that make up the closest edge. The grid will be anchored on this edge. + KeyValuePair closestEdgeEndPoints = + MeshMath.FindClosestEdgeInFace(previewFacePosition, coplanarSnapFaceVertices); + + origin = FindFaceOrigin(previewFacePosition, closestEdgeEndPoints); + offset = FindOffset(origin); + + // Find the three face axes that will represent the snapGrid. + Vector3 forward = FindForwardAxis(coplanarSnapFaceVertices); + Vector3 right = FindBestRightAxis(coplanarSnapFaceVertices); + Vector3 up = FindUpAxis(right, forward); + snapAxes = new Axes(right, up, forward); + + // Find the rotational difference from the universal axes to the axes of the face. + rotation = FromToRotation(new Axes(Vector3.right, Vector3.up, Vector3.forward), snapAxes); + normal = forward; + snapCenter = MeshMath.CalculateGeometricCenter(coplanarSnapFaceVertices); + } - /// - /// Creates a snapGrid representation of the universal grid. - /// - private void SetupUniversalSnapGrid() { - snapType = SnapType.UNIVERSAL; - - origin = Vector3.zero; - offset = Vector3.zero; - rotation = Quaternion.identity; - // The normal isn't important for universal snapping. We can do snapping calculations without it. - normal = Vector3.zero; - } + /// + /// Creates a snapGrid anchored to a mesh. + /// + /// The position of the preview at start of snap. + /// The mesh being snapped to. + private void SetupMeshSnapGrid(Vector3 positionToSnap, MMesh mesh) + { + snapType = SnapType.MESH; + + origin = mesh.offset; + offset = FindOffset(origin); + rotation = mesh.rotation; + snapCenter = origin; + normal = GridUtils.FindNearestLocalMeshAxis(positionToSnap - snapCenter, rotation); + } - /// - /// Finds the rotation of the previewMesh given the vertex being snapped to. It does this by creating a vertex - /// normal which is a vector from the mesh center to the vertex and finding the rotational difference between - /// the vertex normal and the previewFace normal. - /// - /// The normal of the previewFace. - /// The rotation of the previewMesh. - /// The new rotation for the previewMesh such that it is balanced on the snapVertex. - public Quaternion FindPreviewMeshRotationForVertexSnap(Vector3 previewFaceNormal, Quaternion previewMeshRotation) { - Quaternion normalRotDelta = Quaternion.FromToRotation(previewFaceNormal, -normal); + /// + /// Creates a snapGrid representation of the universal grid. + /// + private void SetupUniversalSnapGrid() + { + snapType = SnapType.UNIVERSAL; + + origin = Vector3.zero; + offset = Vector3.zero; + rotation = Quaternion.identity; + // The normal isn't important for universal snapping. We can do snapping calculations without it. + normal = Vector3.zero; + } - return normalRotDelta * previewMeshRotation; - } + /// + /// Finds the rotation of the previewMesh given the vertex being snapped to. It does this by creating a vertex + /// normal which is a vector from the mesh center to the vertex and finding the rotational difference between + /// the vertex normal and the previewFace normal. + /// + /// The normal of the previewFace. + /// The rotation of the previewMesh. + /// The new rotation for the previewMesh such that it is balanced on the snapVertex. + public Quaternion FindPreviewMeshRotationForVertexSnap(Vector3 previewFaceNormal, Quaternion previewMeshRotation) + { + Quaternion normalRotDelta = Quaternion.FromToRotation(previewFaceNormal, -normal); + + return normalRotDelta * previewMeshRotation; + } - /// - /// Finds the rotation of the previewMesh given the snapGrid properties. It does this by finding the Axes - /// representing the previewFace, then the rotation from the previewFace to the snapFace and applying the - /// rotational delta to the rotation of the previewMesh. - /// - /// - /// The vertices representing the previewFace which is being rotated to be flush with the snapFace. - /// - /// The rotation of the previewMesh the previewFace belongs to. - /// The new rotation for the previewMesh such that the previewFace and snapFace are flush. - public Quaternion FindPreviewMeshRotationForFaceSnap(List coplanarPreviewFaceVertices, - Quaternion previewMeshRotation) { - // We want to rotate to line up with the snapFaceAxes, except we want the forwards to point in different - // directions. So what we really want is to flip around the snapFaceAxes and rotate to match up with that. - Axes invertedFaceAxes = new Axes(snapAxes.right, -snapAxes.up, snapAxes.forward); - - // Find the axes that represent the previewFace. - Vector3 previewForward = FindForwardAxis(coplanarPreviewFaceVertices); - - // Choose the right axes that is closest to the right axes of the snapFace to minimize the effect of the - // rotation. We will find the edge that is closest to the edge we used to define snapAxes.right but we also - // want to invert this edge so that it goes in the same direction as snapAxes.right. They point in different - // directions to start with because the winding order of the faces are in opposite directions since the normals - // of the face point toward each other. - Vector3 previewRight = - -MeshMath.ClosestEdgeToEdge(coplanarPreviewFaceVertices, mostRepresentativeEdge).normalized; - Vector3 previewUp = FindUpAxis(previewRight, previewForward); - Axes previewFaceAxes = new Axes(previewRight, previewUp, previewForward); - - Quaternion previewRotDelta = FromToRotation(previewFaceAxes, invertedFaceAxes); - - return previewRotDelta * previewMeshRotation; - } + /// + /// Finds the rotation of the previewMesh given the snapGrid properties. It does this by finding the Axes + /// representing the previewFace, then the rotation from the previewFace to the snapFace and applying the + /// rotational delta to the rotation of the previewMesh. + /// + /// + /// The vertices representing the previewFace which is being rotated to be flush with the snapFace. + /// + /// The rotation of the previewMesh the previewFace belongs to. + /// The new rotation for the previewMesh such that the previewFace and snapFace are flush. + public Quaternion FindPreviewMeshRotationForFaceSnap(List coplanarPreviewFaceVertices, + Quaternion previewMeshRotation) + { + // We want to rotate to line up with the snapFaceAxes, except we want the forwards to point in different + // directions. So what we really want is to flip around the snapFaceAxes and rotate to match up with that. + Axes invertedFaceAxes = new Axes(snapAxes.right, -snapAxes.up, snapAxes.forward); + + // Find the axes that represent the previewFace. + Vector3 previewForward = FindForwardAxis(coplanarPreviewFaceVertices); + + // Choose the right axes that is closest to the right axes of the snapFace to minimize the effect of the + // rotation. We will find the edge that is closest to the edge we used to define snapAxes.right but we also + // want to invert this edge so that it goes in the same direction as snapAxes.right. They point in different + // directions to start with because the winding order of the faces are in opposite directions since the normals + // of the face point toward each other. + Vector3 previewRight = + -MeshMath.ClosestEdgeToEdge(coplanarPreviewFaceVertices, mostRepresentativeEdge).normalized; + Vector3 previewUp = FindUpAxis(previewRight, previewForward); + Axes previewFaceAxes = new Axes(previewRight, previewUp, previewForward); + + Quaternion previewRotDelta = FromToRotation(previewFaceAxes, invertedFaceAxes); + + return previewRotDelta * previewMeshRotation; + } - /// - /// Takes a position on a mesh and snaps it to the grid then updates the mesh position. - /// - /// The position being snapped to the grid. - /// The id of the face being snapped if there is a face being snapped. - /// The offset of the mesh being snapped. - /// The rotation of the mesh being snapped. - /// The mesh being snapped. - /// The snap transform for the previewMesh after snapping, as well as other data about the snap necessary - /// for UX shading. - public SnapInfo snapToGrid(Vector3 positionToSnap, int previewFaceId, Vector3 previewMeshOffset, - Quaternion previewMeshRotation, MMesh previewMesh) { - // Depending on the snapType use different methods to positionally snap. - // Note that we could write all these functions into one generalized function but to avoid unneccesary calls and - // to increase readability they have been split into separate functions. - SnapInfo snapInfo = new SnapInfo(); - - bool relationalSnapped = false; - if (snapType == SnapType.VERTEX) { - Vector3 snappedPosition = previewMeshOffset + (SnapPositionToVertexSnapGrid(positionToSnap) - positionToSnap); - Quaternion snappedRotation = previewMeshRotation; - snapInfo.transform = new SnapTransform(snappedPosition, snappedRotation); - relationalSnapped = true; - } else if (snapType == SnapType.FACE) { - snapInfo = SnapPositionToFaceSnapGrid(positionToSnap, previewFaceId, previewMeshOffset, - previewMeshRotation, previewMesh); - relationalSnapped = true; - } else if (snapType == SnapType.MESH) { - Vector3 snappedPosition = SnapPositionToMeshSnapGrid(previewMeshOffset); - Quaternion snappedRotation = previewMeshRotation; - snapInfo.transform = new SnapTransform(snappedPosition, snappedRotation); - relationalSnapped = true; - } else if (snapType == SnapType.UNIVERSAL) { - Vector3 snappedPosition = SnapPositionToUniversalSnapGrid(previewMeshOffset, previewMesh.bounds); - Quaternion snappedRotation = previewMeshRotation; - snapInfo.transform = new SnapTransform(snappedPosition, snappedRotation); - } - - if (relationalSnapped) { - if (PeltzerMain.Instance.peltzerController.mode == controller.ControllerMode.insertVolume) { - PeltzerMain.Instance.snappedInVolumeInserter = true; - } else if (PeltzerMain.Instance.peltzerController.mode == controller.ControllerMode.move) { - PeltzerMain.Instance.snappedInMover = true; - } - } - return snapInfo; - } + /// + /// Takes a position on a mesh and snaps it to the grid then updates the mesh position. + /// + /// The position being snapped to the grid. + /// The id of the face being snapped if there is a face being snapped. + /// The offset of the mesh being snapped. + /// The rotation of the mesh being snapped. + /// The mesh being snapped. + /// The snap transform for the previewMesh after snapping, as well as other data about the snap necessary + /// for UX shading. + public SnapInfo snapToGrid(Vector3 positionToSnap, int previewFaceId, Vector3 previewMeshOffset, + Quaternion previewMeshRotation, MMesh previewMesh) + { + // Depending on the snapType use different methods to positionally snap. + // Note that we could write all these functions into one generalized function but to avoid unneccesary calls and + // to increase readability they have been split into separate functions. + SnapInfo snapInfo = new SnapInfo(); + + bool relationalSnapped = false; + if (snapType == SnapType.VERTEX) + { + Vector3 snappedPosition = previewMeshOffset + (SnapPositionToVertexSnapGrid(positionToSnap) - positionToSnap); + Quaternion snappedRotation = previewMeshRotation; + snapInfo.transform = new SnapTransform(snappedPosition, snappedRotation); + relationalSnapped = true; + } + else if (snapType == SnapType.FACE) + { + snapInfo = SnapPositionToFaceSnapGrid(positionToSnap, previewFaceId, previewMeshOffset, + previewMeshRotation, previewMesh); + relationalSnapped = true; + } + else if (snapType == SnapType.MESH) + { + Vector3 snappedPosition = SnapPositionToMeshSnapGrid(previewMeshOffset); + Quaternion snappedRotation = previewMeshRotation; + snapInfo.transform = new SnapTransform(snappedPosition, snappedRotation); + relationalSnapped = true; + } + else if (snapType == SnapType.UNIVERSAL) + { + Vector3 snappedPosition = SnapPositionToUniversalSnapGrid(previewMeshOffset, previewMesh.bounds); + Quaternion snappedRotation = previewMeshRotation; + snapInfo.transform = new SnapTransform(snappedPosition, snappedRotation); + } + + if (relationalSnapped) + { + if (PeltzerMain.Instance.peltzerController.mode == controller.ControllerMode.insertVolume) + { + PeltzerMain.Instance.snappedInVolumeInserter = true; + } + else if (PeltzerMain.Instance.peltzerController.mode == controller.ControllerMode.move) + { + PeltzerMain.Instance.snappedInMover = true; + } + } + return snapInfo; + } - /// - /// Snaps a position along the vertexNormal. - /// - /// Position to snap along the normal. - /// The snapped position. - private Vector3 SnapPositionToVertexSnapGrid(Vector3 positionToSnap) { - if (WithinSurfaceThreshold(positionToSnap)) { - return origin; - } else { - // Project the position onto the normal then snap it to the nearest position on the normal. - return GridUtils.ProjectPointOntoLine(positionToSnap, normal, origin); - } - } + /// + /// Snaps a position along the vertexNormal. + /// + /// Position to snap along the normal. + /// The snapped position. + private Vector3 SnapPositionToVertexSnapGrid(Vector3 positionToSnap) + { + if (WithinSurfaceThreshold(positionToSnap)) + { + return origin; + } + else + { + // Project the position onto the normal then snap it to the nearest position on the normal. + return GridUtils.ProjectPointOntoLine(positionToSnap, normal, origin); + } + } - /// - /// Snaps a position to a grid defined by a face. - /// - /// The position being snapped to the grid. - /// The idea of the face being snapped if there is a face being snapped. - /// The offset of the mesh being snapped. - /// The rotation of the mesh being snapped. - /// The mesh being snapped. - /// The new position and rotation for the previewMesh after snapping. - private SnapInfo SnapPositionToFaceSnapGrid(Vector3 positionToSnap, int previewFaceId, - Vector3 previewMeshOffset, Quaternion previewMeshRotation, MMesh previewMesh) { - SnapInfo snapInfo = new SnapInfo(); - Vector3 snappedPosition; - Quaternion snappedRotation; - bool snappedToCenter = false; - - // A point can center and/or surface snap or neither. So first we either centerSnap or snap to the grid. - // This can be thought of as snapping in the X and Y plane (we also snap in the Z but we can override the Z later - // without affecting the X and Y). - if (WithinCenterThreshold(positionToSnap)) { - // If center snapping, snap onto a line represented by the normal centered on the snapCenter. - snappedPosition = GridUtils.ProjectPointOntoLine(positionToSnap, normal, snapCenter); - snappedToCenter = true; - } else { - // Just snap to the nearest snapGrid point. - snappedPosition = SnapPositionToSnapGrid(positionToSnap); - } - - // Position of point projected onto the surface plane. The point has already been snapped in the X and Y planes - // so just project it down onto the surface (snap on the Z plane). - // We need this for shader effects even if we're not snapping to the surface. - Vector3 surfaceSnappedPosition = Math3d.ProjectPointOnPlane(normal, origin, snappedPosition); - - // Now check to see if we should be snapping to the surface in addition to the above snap. - if (WithinSurfaceThreshold(positionToSnap)) { - snapInfo.inSurfaceThreshhold = true; - // The point has already been snapped in the X and Y planes so just project it down onto the surface (snap on - // the Z plane). - snappedPosition = surfaceSnappedPosition; - - // Recalculate the position of the previewFaceVertices. - Vector3 delta = snappedPosition - positionToSnap; - ReadOnlyCollection vertexIds = previewMesh.GetFace(previewFaceId).vertexIds; - List coplanarPreviewFaceVertices = new List(vertexIds.Count); - foreach (int vertexId in vertexIds) { - coplanarPreviewFaceVertices.Add( - previewMeshRotation * previewMesh.VertexPositionInMeshCoords(vertexId) + (previewMeshOffset + delta)); - } - - // Check to see if we are close enough to edge snap but only if we aren't center snapped. Center snap supercedes - // edge snapping. - EdgeInfo previewFaceEdge; - EdgeInfo snapFaceEdge; - bool withinCornerThreshold; - Vector3 corner; - - if (!snappedToCenter && WithinEdgeThreshold(coplanarPreviewFaceVertices, out previewFaceEdge, out snapFaceEdge, - out withinCornerThreshold, out corner)) { - return SnapToEdgeOrCorner( - previewMeshOffset + delta, - previewMeshRotation, - previewFaceEdge, - snapFaceEdge, - withinCornerThreshold, - corner, - positionToSnap); - } else { - snappedPosition = previewMeshOffset + (snappedPosition - positionToSnap); - snappedRotation = previewMeshRotation; - } - } else { - snappedPosition = previewMeshOffset + (snappedPosition - positionToSnap); - snappedRotation = previewMeshRotation; - } - snapInfo.transform = new SnapTransform(snappedPosition, snappedRotation); - snapInfo.snapPoint = surfaceSnappedPosition; - snapInfo.snapNormal = normal; - snapInfo.snappingFacePosition = positionToSnap; - return snapInfo; - } + /// + /// Snaps a position to a grid defined by a face. + /// + /// The position being snapped to the grid. + /// The idea of the face being snapped if there is a face being snapped. + /// The offset of the mesh being snapped. + /// The rotation of the mesh being snapped. + /// The mesh being snapped. + /// The new position and rotation for the previewMesh after snapping. + private SnapInfo SnapPositionToFaceSnapGrid(Vector3 positionToSnap, int previewFaceId, + Vector3 previewMeshOffset, Quaternion previewMeshRotation, MMesh previewMesh) + { + SnapInfo snapInfo = new SnapInfo(); + Vector3 snappedPosition; + Quaternion snappedRotation; + bool snappedToCenter = false; + + // A point can center and/or surface snap or neither. So first we either centerSnap or snap to the grid. + // This can be thought of as snapping in the X and Y plane (we also snap in the Z but we can override the Z later + // without affecting the X and Y). + if (WithinCenterThreshold(positionToSnap)) + { + // If center snapping, snap onto a line represented by the normal centered on the snapCenter. + snappedPosition = GridUtils.ProjectPointOntoLine(positionToSnap, normal, snapCenter); + snappedToCenter = true; + } + else + { + // Just snap to the nearest snapGrid point. + snappedPosition = SnapPositionToSnapGrid(positionToSnap); + } + + // Position of point projected onto the surface plane. The point has already been snapped in the X and Y planes + // so just project it down onto the surface (snap on the Z plane). + // We need this for shader effects even if we're not snapping to the surface. + Vector3 surfaceSnappedPosition = Math3d.ProjectPointOnPlane(normal, origin, snappedPosition); + + // Now check to see if we should be snapping to the surface in addition to the above snap. + if (WithinSurfaceThreshold(positionToSnap)) + { + snapInfo.inSurfaceThreshhold = true; + // The point has already been snapped in the X and Y planes so just project it down onto the surface (snap on + // the Z plane). + snappedPosition = surfaceSnappedPosition; + + // Recalculate the position of the previewFaceVertices. + Vector3 delta = snappedPosition - positionToSnap; + ReadOnlyCollection vertexIds = previewMesh.GetFace(previewFaceId).vertexIds; + List coplanarPreviewFaceVertices = new List(vertexIds.Count); + foreach (int vertexId in vertexIds) + { + coplanarPreviewFaceVertices.Add( + previewMeshRotation * previewMesh.VertexPositionInMeshCoords(vertexId) + (previewMeshOffset + delta)); + } + + // Check to see if we are close enough to edge snap but only if we aren't center snapped. Center snap supercedes + // edge snapping. + EdgeInfo previewFaceEdge; + EdgeInfo snapFaceEdge; + bool withinCornerThreshold; + Vector3 corner; + + if (!snappedToCenter && WithinEdgeThreshold(coplanarPreviewFaceVertices, out previewFaceEdge, out snapFaceEdge, + out withinCornerThreshold, out corner)) + { + return SnapToEdgeOrCorner( + previewMeshOffset + delta, + previewMeshRotation, + previewFaceEdge, + snapFaceEdge, + withinCornerThreshold, + corner, + positionToSnap); + } + else + { + snappedPosition = previewMeshOffset + (snappedPosition - positionToSnap); + snappedRotation = previewMeshRotation; + } + } + else + { + snappedPosition = previewMeshOffset + (snappedPosition - positionToSnap); + snappedRotation = previewMeshRotation; + } + snapInfo.transform = new SnapTransform(snappedPosition, snappedRotation); + snapInfo.snapPoint = surfaceSnappedPosition; + snapInfo.snapNormal = normal; + snapInfo.snappingFacePosition = positionToSnap; + return snapInfo; + } - /// - /// Snaps a position to a grid defined by a mesh. - /// - /// Position being snapped to the snapGrid. - /// The snapped position. - private Vector3 SnapPositionToMeshSnapGrid(Vector3 positionToSnap) { - if (WithinCenterThreshold(positionToSnap)) { - // If center snapping, snap onto a line represented by the normal centered on the snapCenter. - return GridUtils.ProjectPointOntoLine(positionToSnap, normal, snapCenter); - } else { - // Just snap to the nearest grid point. - return SnapPositionToSnapGrid(positionToSnap); - } - } + /// + /// Snaps a position to a grid defined by a mesh. + /// + /// Position being snapped to the snapGrid. + /// The snapped position. + private Vector3 SnapPositionToMeshSnapGrid(Vector3 positionToSnap) + { + if (WithinCenterThreshold(positionToSnap)) + { + // If center snapping, snap onto a line represented by the normal centered on the snapCenter. + return GridUtils.ProjectPointOntoLine(positionToSnap, normal, snapCenter); + } + else + { + // Just snap to the nearest grid point. + return SnapPositionToSnapGrid(positionToSnap); + } + } - /// - /// Snaps a position to a snapGrid. - /// - /// Position being snapped to the snapGrid. - /// The snapped position. - private Vector3 SnapPositionToSnapGrid(Vector3 positionToSnap) { - // Draw a vector from the origin of the snapGrid to the position being snapped. - Vector3 positionalVector = positionToSnap - origin; + /// + /// Snaps a position to a snapGrid. + /// + /// Position being snapped to the snapGrid. + /// The snapped position. + private Vector3 SnapPositionToSnapGrid(Vector3 positionToSnap) + { + // Draw a vector from the origin of the snapGrid to the position being snapped. + Vector3 positionalVector = positionToSnap - origin; - // Unrotate the positionalVector so we are working on the universal grid. - Vector3 universalVector = Quaternion.Inverse(rotation) * positionalVector; + // Unrotate the positionalVector so we are working on the universal grid. + Vector3 universalVector = Quaternion.Inverse(rotation) * positionalVector; - // Get the unrotated position from the end point of the universalVector. - Vector3 universalPosition = origin + (universalVector.normalized * Vector3.Distance(origin, positionToSnap)); + // Get the unrotated position from the end point of the universalVector. + Vector3 universalPosition = origin + (universalVector.normalized * Vector3.Distance(origin, positionToSnap)); - // Snap the unrotatedPosition to UniversalGrid + offset. - Vector3 universalSnappedPosition = GridUtils.SnapToGrid(universalPosition, offset); + // Snap the unrotatedPosition to UniversalGrid + offset. + Vector3 universalSnappedPosition = GridUtils.SnapToGrid(universalPosition, offset); - // Draw a new vector from the origin to the unrotatedSnappedPosition. - Vector3 universalPositionalSnappedVector = universalSnappedPosition - origin; + // Draw a new vector from the origin to the unrotatedSnappedPosition. + Vector3 universalPositionalSnappedVector = universalSnappedPosition - origin; - // Rotate back. - Vector3 positionalSnappedVector = rotation * universalPositionalSnappedVector; + // Rotate back. + Vector3 positionalSnappedVector = rotation * universalPositionalSnappedVector; - // Get snapped position - which is the end point from the positionalSnappedVector. - return origin + (positionalSnappedVector.normalized * Vector3.Distance(origin, universalSnappedPosition)); - } + // Get snapped position - which is the end point from the positionalSnappedVector. + return origin + (positionalSnappedVector.normalized * Vector3.Distance(origin, universalSnappedPosition)); + } - /// - /// Snaps a mesh to an edge by rotating the mesh so its nearest edge aligns with the edge and moves it - /// positionally to align with the edge. - /// - /// The offset of the mesh being snapped. - /// The rotation of the mesh being snapped. - /// The edgeInfo for the edge on the previewMesh being snapped. - /// The edgeInfo for the edge on the snapMesh being snapped to. - /// Whether there is a corner to snap to. - /// The position of the corner or Vector3.zero if there isn't a close enough corner. - /// The new position and rotation for the previewMesh after snapping. - private SnapInfo SnapToEdgeOrCorner(Vector3 previewMeshOffset, Quaternion previewMeshRotation, - EdgeInfo previewFaceEdge, EdgeInfo snapFaceEdge, bool withinCornerThreshold, Vector3 corner, - Vector3 positionToSnap) { - // These were found in WithinEdgeThreshold and recorded to avoid looping through both faces again. - Vector3 previewEdge = previewFaceEdge.edgeVector; - Vector3 snapEdge = snapFaceEdge.edgeVector; - SnapInfo snapInfo = new SnapInfo(); - - // Switch the direction of snapEdge to minimize the angle between previewFaceEdge and snapFaceEdge. - // The angle between snapEdge and -snapEdge is 180 degrees. So if the angle between previewEdge and snapEdge is - // greater than 90 the angle between previewEdge and -snapEdge will be less than 90 and therefore the minimized - // angle. - if (90.0f - Vector3.Angle(previewEdge, snapEdge) < angleThreshold) - snapEdge = -snapEdge; - - // Find the rotational difference between the two edges. - Quaternion edgeRotDelta = Quaternion.FromToRotation(previewEdge, snapEdge); - - // Find the position of an edge point if snapped onto the line. - Vector3 snappedEdgeStartPoint = GridUtils.ProjectPointOntoLine(previewFaceEdge.edgeStart, snapEdge, - snapFaceEdge.edgeStart); - - // Find the difference and apply it to positionToSnap. - Vector3 snappedPosition = previewMeshOffset + (snappedEdgeStartPoint - previewFaceEdge.edgeStart); - - // Slide the position over so it snaps to the corner. - if (withinCornerThreshold) { - Vector3 snappedEdgeEndPoint = snappedEdgeStartPoint + previewFaceEdge.edgeVector; - Vector3 distanceToCorner = - Vector3.Distance(corner, snappedEdgeEndPoint) < Vector3.Distance(corner, snappedEdgeStartPoint) ? - corner - snappedEdgeEndPoint : corner - snappedEdgeStartPoint; - snappedPosition = snappedPosition + distanceToCorner; - } - - // Find the rotational difference to align the edges. - Quaternion snappedRotation = edgeRotDelta * previewMeshRotation; - snapInfo.transform = new SnapTransform(snappedPosition, snappedRotation); - snapInfo.inSurfaceThreshhold = true; - snapInfo.snapPoint = snappedPosition; - snapInfo.snapNormal = normal; - snapInfo.snappingFacePosition = positionToSnap; - return snapInfo; - } + /// + /// Snaps a mesh to an edge by rotating the mesh so its nearest edge aligns with the edge and moves it + /// positionally to align with the edge. + /// + /// The offset of the mesh being snapped. + /// The rotation of the mesh being snapped. + /// The edgeInfo for the edge on the previewMesh being snapped. + /// The edgeInfo for the edge on the snapMesh being snapped to. + /// Whether there is a corner to snap to. + /// The position of the corner or Vector3.zero if there isn't a close enough corner. + /// The new position and rotation for the previewMesh after snapping. + private SnapInfo SnapToEdgeOrCorner(Vector3 previewMeshOffset, Quaternion previewMeshRotation, + EdgeInfo previewFaceEdge, EdgeInfo snapFaceEdge, bool withinCornerThreshold, Vector3 corner, + Vector3 positionToSnap) + { + // These were found in WithinEdgeThreshold and recorded to avoid looping through both faces again. + Vector3 previewEdge = previewFaceEdge.edgeVector; + Vector3 snapEdge = snapFaceEdge.edgeVector; + SnapInfo snapInfo = new SnapInfo(); + + // Switch the direction of snapEdge to minimize the angle between previewFaceEdge and snapFaceEdge. + // The angle between snapEdge and -snapEdge is 180 degrees. So if the angle between previewEdge and snapEdge is + // greater than 90 the angle between previewEdge and -snapEdge will be less than 90 and therefore the minimized + // angle. + if (90.0f - Vector3.Angle(previewEdge, snapEdge) < angleThreshold) + snapEdge = -snapEdge; + + // Find the rotational difference between the two edges. + Quaternion edgeRotDelta = Quaternion.FromToRotation(previewEdge, snapEdge); + + // Find the position of an edge point if snapped onto the line. + Vector3 snappedEdgeStartPoint = GridUtils.ProjectPointOntoLine(previewFaceEdge.edgeStart, snapEdge, + snapFaceEdge.edgeStart); + + // Find the difference and apply it to positionToSnap. + Vector3 snappedPosition = previewMeshOffset + (snappedEdgeStartPoint - previewFaceEdge.edgeStart); + + // Slide the position over so it snaps to the corner. + if (withinCornerThreshold) + { + Vector3 snappedEdgeEndPoint = snappedEdgeStartPoint + previewFaceEdge.edgeVector; + Vector3 distanceToCorner = + Vector3.Distance(corner, snappedEdgeEndPoint) < Vector3.Distance(corner, snappedEdgeStartPoint) ? + corner - snappedEdgeEndPoint : corner - snappedEdgeStartPoint; + snappedPosition = snappedPosition + distanceToCorner; + } + + // Find the rotational difference to align the edges. + Quaternion snappedRotation = edgeRotDelta * previewMeshRotation; + snapInfo.transform = new SnapTransform(snappedPosition, snappedRotation); + snapInfo.inSurfaceThreshhold = true; + snapInfo.snapPoint = snappedPosition; + snapInfo.snapNormal = normal; + snapInfo.snappingFacePosition = positionToSnap; + return snapInfo; + } - /// - /// Snaps a position to the universal grid. - /// - /// Position to snap. - /// The bounds of the mesh being snapped to the universal grid. - /// The snapped position. - private Vector3 SnapPositionToUniversalSnapGrid(Vector3 positionToSnap, Bounds bounds) { - // We don't have to do anything fancy to snap to the universal grid. - return GridUtils.SnapToGrid(positionToSnap, bounds); - } + /// + /// Snaps a position to the universal grid. + /// + /// Position to snap. + /// The bounds of the mesh being snapped to the universal grid. + /// The snapped position. + private Vector3 SnapPositionToUniversalSnapGrid(Vector3 positionToSnap, Bounds bounds) + { + // We don't have to do anything fancy to snap to the universal grid. + return GridUtils.SnapToGrid(positionToSnap, bounds); + } - /// - /// Checks to see if a position is within the threshold for snapping to the surface. - /// - /// The position. - /// Whether the position is in the threshold. - private bool WithinSurfaceThreshold(Vector3 positionToSnap) { - return Mathf.Abs(Math3d.SignedDistancePlanePoint(normal, origin, positionToSnap)) < surfaceThreshold; - } + /// + /// Checks to see if a position is within the threshold for snapping to the surface. + /// + /// The position. + /// Whether the position is in the threshold. + private bool WithinSurfaceThreshold(Vector3 positionToSnap) + { + return Mathf.Abs(Math3d.SignedDistancePlanePoint(normal, origin, positionToSnap)) < surfaceThreshold; + } - /// - /// Checks to see if a position is within the threshold for snapping to the center. - /// - /// The position. - /// Whether the position is in the threhold. - private bool WithinCenterThreshold(Vector3 positionToSnap) { - // Project the position onto a plane defined by the normal and the snapCenter. - Vector3 projectedPosition = Math3d.ProjectPointOnPlane(normal, snapCenter, positionToSnap); - // Check to see if the planar distance is within the threshold. - return Mathf.Abs(Vector3.Distance(projectedPosition, snapCenter)) < centerThreshold; - } + /// + /// Checks to see if a position is within the threshold for snapping to the center. + /// + /// The position. + /// Whether the position is in the threhold. + private bool WithinCenterThreshold(Vector3 positionToSnap) + { + // Project the position onto a plane defined by the normal and the snapCenter. + Vector3 projectedPosition = Math3d.ProjectPointOnPlane(normal, snapCenter, positionToSnap); + // Check to see if the planar distance is within the threshold. + return Mathf.Abs(Vector3.Distance(projectedPosition, snapCenter)) < centerThreshold; + } - /// - /// Checks to see if any edge in the previewFace is within the threshold to snap to any edge in the snapFace. - /// - /// The coplanar vertices representing the previewFace. - /// The edgeInfo for the previewFaceEdge. - /// The edgeInfo for the snapFaceEdge. - /// Whether the position is close enough to a corner to snap to. - /// The position of the corner or Vector3.zero if there isn't a close enough corner. - /// Whether there is a pair of edges within the edge threshold of each other. - private bool WithinEdgeThreshold(IEnumerable coplanarPreviewFaceVertices, out EdgeInfo previewFaceEdge, - out EdgeInfo snapFaceEdge, out bool withinCornerThreshold, out Vector3 corner) { - IEnumerable closestEdgePairs = - MeshMath.FindClosestEdgePairs(coplanarPreviewFaceVertices, coplanarSnapFaceVertices); - - // Set defaults for out parameters. - previewFaceEdge = new EdgeInfo(); - snapFaceEdge = new EdgeInfo(); - withinCornerThreshold = false; - corner = Vector3.zero; - - // Find if there is at least one edge to be snapped to. - if (closestEdgePairs.First().separation < edgeThreshold) { - previewFaceEdge = closestEdgePairs.First().fromEdge; - snapFaceEdge = closestEdgePairs.First().toEdge; - - // Check to see if there are two perpendicular intersecting nearbyEdges in which case we should corner snap. - if (closestEdgePairs.Count() > 1) { - EdgePair secondEdgePair = closestEdgePairs.ElementAt(1); - EdgeInfo secondEdge = secondEdgePair.toEdge; - if (secondEdgePair.separation < edgeThreshold - && Mathf.Abs(90.0f - Vector3.Angle(snapFaceEdge.edgeVector, secondEdge.edgeVector)) < 0.01f) { - withinCornerThreshold = true; - corner = - Math3d.ProjectPointOntoLine(snapFaceEdge.edgeStart, secondEdge.edgeVector, secondEdge.edgeStart); - } - } - - return true; - } - - return false; - } + /// + /// Checks to see if any edge in the previewFace is within the threshold to snap to any edge in the snapFace. + /// + /// The coplanar vertices representing the previewFace. + /// The edgeInfo for the previewFaceEdge. + /// The edgeInfo for the snapFaceEdge. + /// Whether the position is close enough to a corner to snap to. + /// The position of the corner or Vector3.zero if there isn't a close enough corner. + /// Whether there is a pair of edges within the edge threshold of each other. + private bool WithinEdgeThreshold(IEnumerable coplanarPreviewFaceVertices, out EdgeInfo previewFaceEdge, + out EdgeInfo snapFaceEdge, out bool withinCornerThreshold, out Vector3 corner) + { + IEnumerable closestEdgePairs = + MeshMath.FindClosestEdgePairs(coplanarPreviewFaceVertices, coplanarSnapFaceVertices); + + // Set defaults for out parameters. + previewFaceEdge = new EdgeInfo(); + snapFaceEdge = new EdgeInfo(); + withinCornerThreshold = false; + corner = Vector3.zero; + + // Find if there is at least one edge to be snapped to. + if (closestEdgePairs.First().separation < edgeThreshold) + { + previewFaceEdge = closestEdgePairs.First().fromEdge; + snapFaceEdge = closestEdgePairs.First().toEdge; + + // Check to see if there are two perpendicular intersecting nearbyEdges in which case we should corner snap. + if (closestEdgePairs.Count() > 1) + { + EdgePair secondEdgePair = closestEdgePairs.ElementAt(1); + EdgeInfo secondEdge = secondEdgePair.toEdge; + if (secondEdgePair.separation < edgeThreshold + && Mathf.Abs(90.0f - Vector3.Angle(snapFaceEdge.edgeVector, secondEdge.edgeVector)) < 0.01f) + { + withinCornerThreshold = true; + corner = + Math3d.ProjectPointOntoLine(snapFaceEdge.edgeStart, secondEdge.edgeVector, secondEdge.edgeStart); + } + } + + return true; + } + + return false; + } - /// - /// Finds the origin of the snapGrid by choosing the closest vertex on the closest edge. - /// - /// The position at the start of the snap. - /// The vertices of the closest edge. - /// The closest vertex from the closest edge. - private static Vector3 FindFaceOrigin(Vector3 previewFacePosition, - KeyValuePair closestEdgeEndPoints) { - Vector3 v1 = closestEdgeEndPoints.Key; - Vector3 v2 = closestEdgeEndPoints.Value; - - // Let the origin be which ever vertex in the edge is closest. - return Vector3.Distance(v1, previewFacePosition) < Vector3.Distance(v2, previewFacePosition) ? v1 : v2; - } + /// + /// Finds the origin of the snapGrid by choosing the closest vertex on the closest edge. + /// + /// The position at the start of the snap. + /// The vertices of the closest edge. + /// The closest vertex from the closest edge. + private static Vector3 FindFaceOrigin(Vector3 previewFacePosition, + KeyValuePair closestEdgeEndPoints) + { + Vector3 v1 = closestEdgeEndPoints.Key; + Vector3 v2 = closestEdgeEndPoints.Value; + + // Let the origin be which ever vertex in the edge is closest. + return Vector3.Distance(v1, previewFacePosition) < Vector3.Distance(v2, previewFacePosition) ? v1 : v2; + } - /// - /// Finds the snapGrid offset which is the distance from the universal grid. - /// - private static Vector3 FindOffset(Vector3 origin) { - return origin - GridUtils.SnapToGrid(origin); - } + /// + /// Finds the snapGrid offset which is the distance from the universal grid. + /// + private static Vector3 FindOffset(Vector3 origin) + { + return origin - GridUtils.SnapToGrid(origin); + } - /// - /// Finds the forward axis which is just the normal out of the origin. - /// - private static Vector3 FindForwardAxis(List coplanarVertices) { - return MeshMath.CalculateNormal(coplanarVertices); - } + /// + /// Finds the forward axis which is just the normal out of the origin. + /// + private static Vector3 FindForwardAxis(List coplanarVertices) + { + return MeshMath.CalculateNormal(coplanarVertices); + } - /// - /// Finds the right axis by comparing all the edges in the face and choosing an edge for the right axis that is - /// the most representative of the other edges. Essentially we are trying to find an edge that is perpendicular - /// to as many edges as possible so that we can rotate the preview to align to the greatest number of edges. - /// - /// The vertices representing the snapFace. - /// The right axis as a normalized vector. - private static Vector3 FindBestRightAxis(List coplanarSnapFaceVertices) { - mostRepresentativeEdge = MeshMath.FindMostRepresentativeEdge(coplanarSnapFaceVertices); - return mostRepresentativeEdge.edgeVector.normalized; - } + /// + /// Finds the right axis by comparing all the edges in the face and choosing an edge for the right axis that is + /// the most representative of the other edges. Essentially we are trying to find an edge that is perpendicular + /// to as many edges as possible so that we can rotate the preview to align to the greatest number of edges. + /// + /// The vertices representing the snapFace. + /// The right axis as a normalized vector. + private static Vector3 FindBestRightAxis(List coplanarSnapFaceVertices) + { + mostRepresentativeEdge = MeshMath.FindMostRepresentativeEdge(coplanarSnapFaceVertices); + return mostRepresentativeEdge.edgeVector.normalized; + } - /// - /// Finds the right axis of the snapGrid by using the closest edge in the face. Since the forward axis is the - /// face normal any edge is guaranteed to be perpendicular to the normal. - /// - /// The closest edge. - /// A normalized vector representing the right axis of the snapGrid. - private static Vector3 FindRightAxis(KeyValuePair closestEdge) { - return (closestEdge.Key - closestEdge.Value).normalized; - } + /// + /// Finds the right axis of the snapGrid by using the closest edge in the face. Since the forward axis is the + /// face normal any edge is guaranteed to be perpendicular to the normal. + /// + /// The closest edge. + /// A normalized vector representing the right axis of the snapGrid. + private static Vector3 FindRightAxis(KeyValuePair closestEdge) + { + return (closestEdge.Key - closestEdge.Value).normalized; + } - /// - /// Finds the up axis which is just the axis perpendicular to the right and forward axis. We use the left hand - /// rule to make sure the up axis points the right way so snapGrid.forward, up and right are related the same - /// as the universal Vector3.forward, up and right. - /// - /// The right axis. - /// The forward axis. - /// The cross product of the right and forward axis. - private static Vector3 FindUpAxis(Vector3 right, Vector3 forward) { - return Vector3.Cross(forward, right).normalized; - } + /// + /// Finds the up axis which is just the axis perpendicular to the right and forward axis. We use the left hand + /// rule to make sure the up axis points the right way so snapGrid.forward, up and right are related the same + /// as the universal Vector3.forward, up and right. + /// + /// The right axis. + /// The forward axis. + /// The cross product of the right and forward axis. + private static Vector3 FindUpAxis(Vector3 right, Vector3 forward) + { + return Vector3.Cross(forward, right).normalized; + } - /// - /// Finds the rotation from one Axes to another. - /// - /// The starting Axes. - /// The final Axes. - /// The rotational difference between the two Axes'. - private static Quaternion FromToRotation(Axes from, Axes to) { - // To find the rotation between two Axes we only need to find the difference between two of the axes since a - // pair of axes must move together to maintain the 90 degree angles between them. So we will start by moving - // one arbitrary Axes into place. Then applying this change to a second Axes and finding the remaining - // difference between this Axes and the universal grid axes. - - // Start by finding the rotation difference between the to.up axis and the from.up. - Quaternion yRotation = Quaternion.FromToRotation(from.up, to.up); - - // Apply the yRotation to the from.right then find the difference between the partially rotated from.right axis - // and the to.right axis. - Quaternion residualRotation = Quaternion.FromToRotation(yRotation * from.right, to.right); - - // Combine the rotations to find the rotational difference. - return residualRotation * yRotation; - } + /// + /// Finds the rotation from one Axes to another. + /// + /// The starting Axes. + /// The final Axes. + /// The rotational difference between the two Axes'. + private static Quaternion FromToRotation(Axes from, Axes to) + { + // To find the rotation between two Axes we only need to find the difference between two of the axes since a + // pair of axes must move together to maintain the 90 degree angles between them. So we will start by moving + // one arbitrary Axes into place. Then applying this change to a second Axes and finding the remaining + // difference between this Axes and the universal grid axes. + + // Start by finding the rotation difference between the to.up axis and the from.up. + Quaternion yRotation = Quaternion.FromToRotation(from.up, to.up); + + // Apply the yRotation to the from.right then find the difference between the partially rotated from.right axis + // and the to.right axis. + Quaternion residualRotation = Quaternion.FromToRotation(yRotation * from.right, to.right); + + // Combine the rotations to find the rotational difference. + return residualRotation * yRotation; + } - // Public for testing. - public static Vector3 FindForwardAxisForTest(List coplanarVertices) { - return FindForwardAxis(coplanarVertices); - } + // Public for testing. + public static Vector3 FindForwardAxisForTest(List coplanarVertices) + { + return FindForwardAxis(coplanarVertices); + } - // Public for testing. - public static Vector3 FindRightAxisForTest(KeyValuePair closestEdge) { - return FindRightAxis(closestEdge); - } + // Public for testing. + public static Vector3 FindRightAxisForTest(KeyValuePair closestEdge) + { + return FindRightAxis(closestEdge); + } - // Public for testing. - public static Vector3 FindUpAxisForTest(Vector3 right, Vector3 forward) { - return FindUpAxis(right, forward); - } + // Public for testing. + public static Vector3 FindUpAxisForTest(Vector3 right, Vector3 forward) + { + return FindUpAxis(right, forward); + } - // Public for testing. - public static Quaternion FindFaceRotationForTest(Vector3 right, Vector3 up, Vector3 forward) { - return FromToRotation(new Axes(Vector3.right, Vector3.up, Vector3.forward), new Axes(right, up, forward)); + // Public for testing. + public static Quaternion FindFaceRotationForTest(Vector3 right, Vector3 up, Vector3 forward) + { + return FromToRotation(new Axes(Vector3.right, Vector3.up, Vector3.forward), new Axes(right, up, forward)); + } } - } } diff --git a/Assets/Scripts/model/core/SpatialIndex.cs b/Assets/Scripts/model/core/SpatialIndex.cs index 50c6f0fe..8f295307 100644 --- a/Assets/Scripts/model/core/SpatialIndex.cs +++ b/Assets/Scripts/model/core/SpatialIndex.cs @@ -17,732 +17,856 @@ using com.google.apps.peltzer.client.model.util; using UnityEngine; -namespace com.google.apps.peltzer.client.model.core { - /// - /// Holder for calculated information about a Face. - /// - public struct FaceInfo { - internal Bounds bounds; - internal Plane plane; - internal Vector3 baryCenter; - internal List border; - } - - /// - /// Holder for calculated information about an edge. - /// - public struct EdgeInfo { - internal Bounds bounds; - internal float length; - internal Vector3 edgeStart; - internal Vector3 edgeVector; - } - - /// - /// Holder for an object along with a distance. Makes it easy to determine distance once for - /// a set of candidates and then sort on that. - /// - public struct DistancePair { - public float distance; - public T value; - - internal DistancePair(float distance, T value) { - this.distance = distance; - this.value = value; - } - } - - /// - /// Comparator for sorting DistancePairs. - /// - internal class DistancePairComparer : IComparer> { - public int Compare(DistancePair left, DistancePair right) { - return left.distance.CompareTo(right.distance); - } - } - - public class SpatialIndex { - public const int MAX_INTERSECT_RESULTS = 100000; - - public CollisionSystem meshes { get; private set; } - private CollisionSystem faces; - private CollisionSystem edges; - private CollisionSystem vertices; - private CollisionSystem meshBounds; - private Dictionary faceInfo; - private Dictionary edgeInfo; - - // A reference to the model, which is the single point of truth as to whether an item exists, despite the fact - // that this spatial index contains collections of meshes and other items. - // The reason we wish to treat the model as a single point of truth is that changes to the model happen on the - // main thread, whereas the spatial index is updated on a background thread. The major worry is that something - // is removed from the model, but returned from the spatial index to a tool. See bug for discussion. - private Model model; - +namespace com.google.apps.peltzer.client.model.core +{ /// - /// Meshes that are declared to be invalid and pending removal. These meshes may still exist in the index - /// but will be removed soon, so we behave as if they didn't exist. This is used for performance reasons, - /// so that the main thread can immediately mark meshes for deletion while leaving the actual cleanup - /// to the background task. + /// Holder for calculated information about a Face. /// - private HashSet condemnedMeshes = new HashSet(); - private object condemnedMeshesLock = new object(); // lock this while accessing condemnedMeshes. - // IMPORTANT: never call a synchronized method of this class while holding condemnedMeshesLock. - // It might deadlock (because another thread might be holding the monitor lock and waiting for - // condemnedMeshesLock). - - public SpatialIndex(Model model, Bounds bounds) { - this.model = model; - Setup(bounds); - } - - private void Setup(Bounds bounds) { - meshes = new NativeSpatial(); - faces = new NativeSpatial(); - edges = new NativeSpatial(); - vertices = new NativeSpatial(); - meshBounds = new NativeSpatial(); - - faceInfo = new Dictionary(); - edgeInfo = new Dictionary(); + public struct FaceInfo + { + internal Bounds bounds; + internal Plane plane; + internal Vector3 baryCenter; + internal List border; } /// - /// Add a Mesh to the index. Will index all faces and vertices. This method is not synchronized. It should not - /// touch any indexes directly. (Currently it calculates the values it will store and then stores them via a - /// synchronized method.) + /// Holder for calculated information about an edge. /// - public void AddMesh(MMesh mesh) { - // Calc all the face and edge info before we lock the index: - Dictionary faceInfos = new Dictionary(); - Dictionary edgeInfos = new Dictionary(); - foreach (Face face in mesh.GetFaces()) { - faceInfos[new FaceKey(mesh.id, face.id)] = CalculateFaceInfo(mesh, face); - for (int i = 0; i < face.vertexIds.Count; i++) { - int start = face.vertexIds[i]; - int end = face.vertexIds[(i + 1) % face.vertexIds.Count]; - // Edges will show up twice, since two faces always share an edge in reverse order. - // Don't do anything if it is already in edgeInfos. - EdgeKey edgeKey = new EdgeKey(mesh.id, start, end); - if (!edgeInfos.ContainsKey(edgeKey)) { - edgeInfos[edgeKey] = CalculateEdgeInfo( - mesh.VertexPositionInModelCoords(start), mesh.VertexPositionInModelCoords(end)); - } - } - } - // Lock the index and add everything: - LoadMeshIntoIndex(mesh, faceInfos, edgeInfos); + public struct EdgeInfo + { + internal Bounds bounds; + internal float length; + internal Vector3 edgeStart; + internal Vector3 edgeVector; } /// - /// Mark a mesh as condemned and pending deletion. + /// Holder for an object along with a distance. Makes it easy to determine distance once for + /// a set of candidates and then sort on that. /// - public void CondemnMesh(int meshId) { - lock (condemnedMeshesLock) { - condemnedMeshes.Add(meshId); - } + public struct DistancePair + { + public float distance; + public T value; + + internal DistancePair(float distance, T value) + { + this.distance = distance; + this.value = value; + } } /// - /// Remove a mesh from the index. Expects all the same face and vertex ids from when - /// the mesh was inserted. + /// Comparator for sorting DistancePairs. /// - [MethodImplAttribute(MethodImplOptions.Synchronized)] - public void RemoveMesh(MMesh mesh) { - lock (condemnedMeshesLock) { - condemnedMeshes.Remove(mesh.id); - } - if (meshBounds.HasItem(mesh.id)) { - meshBounds.Remove(mesh.id); - } - if (meshes.HasItem(mesh.id)) { - meshes.Remove(mesh.id); - foreach (Face face in mesh.GetFaces()) { - FaceKey faceKey = new FaceKey(mesh.id, face.id); - faces.Remove(faceKey); - faceInfo.Remove(faceKey); - for (int i = 0; i < face.vertexIds.Count; i++) { - int start = face.vertexIds[i]; - int end = face.vertexIds[(i + 1) % face.vertexIds.Count]; - EdgeKey edgeKey = new EdgeKey(mesh.id, start, end); - if (edgeInfo.Remove(edgeKey)) { - edges.Remove(edgeKey); - } - } - } - foreach (int vertexId in mesh.GetVertexIds()) { - vertices.Remove(new VertexKey(mesh.id, vertexId)); + internal class DistancePairComparer : IComparer> + { + public int Compare(DistancePair left, DistancePair right) + { + return left.distance.CompareTo(right.distance); } - } } - /// - /// Find the meshes closest to the given point, within the given radius. The current implementation - /// just looks for the closest face (within that radius) and returns its associated mesh. - /// - /// The spatialIndex will return keys that belong to meshes that are hidden. We will need to manually - /// remove them using MeshMath.RemoveKeysForMeshes() - /// - [MethodImplAttribute(MethodImplOptions.Synchronized)] - public bool FindMeshesClosestTo(Vector3 point, float radius, out List> meshIds) { - List> faces; - // We do not check here if a mesh exists in the model, as that check is carried out in FindFacesClosestTo. - if (FindFacesClosestTo(point, radius, false, out faces)) { - meshIds = new List>(); - lock (condemnedMeshesLock) { - foreach (DistancePair pair in faces) { - if (condemnedMeshes.Contains(pair.value.meshId)) continue; - DistancePair pairInt = new DistancePair(pair.distance, pair.value.meshId); - meshIds.Add(pairInt); - } + public class SpatialIndex + { + public const int MAX_INTERSECT_RESULTS = 100000; + + public CollisionSystem meshes { get; private set; } + private CollisionSystem faces; + private CollisionSystem edges; + private CollisionSystem vertices; + private CollisionSystem meshBounds; + private Dictionary faceInfo; + private Dictionary edgeInfo; + + // A reference to the model, which is the single point of truth as to whether an item exists, despite the fact + // that this spatial index contains collections of meshes and other items. + // The reason we wish to treat the model as a single point of truth is that changes to the model happen on the + // main thread, whereas the spatial index is updated on a background thread. The major worry is that something + // is removed from the model, but returned from the spatial index to a tool. See bug for discussion. + private Model model; + + /// + /// Meshes that are declared to be invalid and pending removal. These meshes may still exist in the index + /// but will be removed soon, so we behave as if they didn't exist. This is used for performance reasons, + /// so that the main thread can immediately mark meshes for deletion while leaving the actual cleanup + /// to the background task. + /// + private HashSet condemnedMeshes = new HashSet(); + private object condemnedMeshesLock = new object(); // lock this while accessing condemnedMeshes. + // IMPORTANT: never call a synchronized method of this class while holding condemnedMeshesLock. + // It might deadlock (because another thread might be holding the monitor lock and waiting for + // condemnedMeshesLock). + + public SpatialIndex(Model model, Bounds bounds) + { + this.model = model; + Setup(bounds); } - return true; - } else { - meshIds = new List>(); - return false; - } - } - /// - /// Finds the nearest mesh to a point (given in model-space), returning false if no mesh - /// is within the given radius. - /// - /// This searches the Mesh Octree as opposed to using nearestFace as a proxy, and is intended as a - /// fallback when there are no nearby faces. - /// This method measures distance from the given point to the 'offset' of each mesh, which is a proxy for - /// the mesh center, though may not actually be the geometric center of the mesh. - /// This method only considers meshes if the point lies within the bounding box the mesh, a simple - /// check that may be confused by any geometry more complex than a rectangular prism. - /// On the plus side, it's cheap. - /// - [MethodImplAttribute(MethodImplOptions.Synchronized)] - public bool FindNearestMeshTo(Vector3 point, float radius, out int? nearestMesh, - bool ignoreHiddenMeshes = false) { - Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); - nearestMesh = null; - HashSet meshIds; - - if (meshes.IntersectedBy(searchBounds, out meshIds)) { - float minDistance = float.MaxValue; - - lock (condemnedMeshesLock) { - foreach (int meshId in meshIds) { - // Confirm the mesh actually still exists in the model. - if (!model.HasMesh(meshId) || condemnedMeshes.Contains(meshId)) { - continue; - } + private void Setup(Bounds bounds) + { + meshes = new NativeSpatial(); + faces = new NativeSpatial(); + edges = new NativeSpatial(); + vertices = new NativeSpatial(); + meshBounds = new NativeSpatial(); - if (ignoreHiddenMeshes && model.IsMeshHidden(meshId)) { - continue; - } + faceInfo = new Dictionary(); + edgeInfo = new Dictionary(); + } - Bounds bounds = meshBounds.BoundsForItem(meshId); - if (!bounds.Contains(point)) { - continue; - } - float distanceToPoint = Vector3.Distance(bounds.center, point); - if (distanceToPoint < minDistance) { - minDistance = distanceToPoint; - nearestMesh = meshId; + /// + /// Add a Mesh to the index. Will index all faces and vertices. This method is not synchronized. It should not + /// touch any indexes directly. (Currently it calculates the values it will store and then stores them via a + /// synchronized method.) + /// + public void AddMesh(MMesh mesh) + { + // Calc all the face and edge info before we lock the index: + Dictionary faceInfos = new Dictionary(); + Dictionary edgeInfos = new Dictionary(); + foreach (Face face in mesh.GetFaces()) + { + faceInfos[new FaceKey(mesh.id, face.id)] = CalculateFaceInfo(mesh, face); + for (int i = 0; i < face.vertexIds.Count; i++) + { + int start = face.vertexIds[i]; + int end = face.vertexIds[(i + 1) % face.vertexIds.Count]; + // Edges will show up twice, since two faces always share an edge in reverse order. + // Don't do anything if it is already in edgeInfos. + EdgeKey edgeKey = new EdgeKey(mesh.id, start, end); + if (!edgeInfos.ContainsKey(edgeKey)) + { + edgeInfos[edgeKey] = CalculateEdgeInfo( + mesh.VertexPositionInModelCoords(start), mesh.VertexPositionInModelCoords(end)); + } + } } - } + // Lock the index and add everything: + LoadMeshIntoIndex(mesh, faceInfos, edgeInfos); } - } - - return nearestMesh.HasValue; - } - /// - /// Finds the nearest mesh to a point (given in model-space), even if the origin point is not within the - /// nearby mesh, returning false if no mesh is within the given radius. - /// - /// This searches the Mesh Octree as opposed to using nearestFace as a proxy, and is intended as a - /// fallback when there are no nearby faces. - /// This method measures distance from the given point to the 'offset' of each mesh, which is a proxy for - /// the mesh center, though may not actually be the geometric center of the mesh. - /// This method only considers meshes even if the point does not lie within the bounding box the mesh, a simple - /// check that may be confused by any geometry more complex than a rectangular prism. - /// On the plus side, it's cheap. - /// - [MethodImplAttribute(MethodImplOptions.Synchronized)] - public bool FindNearestMeshToNotIncludingPoint(Vector3 point, float radius, out int? nearestMesh, - bool ignoreHiddenMeshes = false) { - Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); - nearestMesh = null; - HashSet meshIds; - - if (meshes.IntersectedBy(searchBounds, out meshIds)) { - float minDistance = float.MaxValue; - - lock (condemnedMeshesLock) { - foreach (int meshId in meshIds) { - // Confirm the mesh actually still exists in the model. - if (!model.HasMesh(meshId) || condemnedMeshes.Contains(meshId)) { - continue; + /// + /// Mark a mesh as condemned and pending deletion. + /// + public void CondemnMesh(int meshId) + { + lock (condemnedMeshesLock) + { + condemnedMeshes.Add(meshId); } + } - if (ignoreHiddenMeshes && model.IsMeshHidden(meshId)) { - continue; + /// + /// Remove a mesh from the index. Expects all the same face and vertex ids from when + /// the mesh was inserted. + /// + [MethodImplAttribute(MethodImplOptions.Synchronized)] + public void RemoveMesh(MMesh mesh) + { + lock (condemnedMeshesLock) + { + condemnedMeshes.Remove(mesh.id); } - - Bounds bounds = meshBounds.BoundsForItem(meshId); - float distanceToPoint = Vector3.Distance(bounds.center, point); - if (distanceToPoint < minDistance) { - minDistance = distanceToPoint; - nearestMesh = meshId; + if (meshBounds.HasItem(mesh.id)) + { + meshBounds.Remove(mesh.id); + } + if (meshes.HasItem(mesh.id)) + { + meshes.Remove(mesh.id); + foreach (Face face in mesh.GetFaces()) + { + FaceKey faceKey = new FaceKey(mesh.id, face.id); + faces.Remove(faceKey); + faceInfo.Remove(faceKey); + for (int i = 0; i < face.vertexIds.Count; i++) + { + int start = face.vertexIds[i]; + int end = face.vertexIds[(i + 1) % face.vertexIds.Count]; + EdgeKey edgeKey = new EdgeKey(mesh.id, start, end); + if (edgeInfo.Remove(edgeKey)) + { + edges.Remove(edgeKey); + } + } + } + foreach (int vertexId in mesh.GetVertexIds()) + { + vertices.Remove(new VertexKey(mesh.id, vertexId)); + } } - } } - } - return nearestMesh.HasValue; - } + /// + /// Find the meshes closest to the given point, within the given radius. The current implementation + /// just looks for the closest face (within that radius) and returns its associated mesh. + /// + /// The spatialIndex will return keys that belong to meshes that are hidden. We will need to manually + /// remove them using MeshMath.RemoveKeysForMeshes() + /// + [MethodImplAttribute(MethodImplOptions.Synchronized)] + public bool FindMeshesClosestTo(Vector3 point, float radius, out List> meshIds) + { + List> faces; + // We do not check here if a mesh exists in the model, as that check is carried out in FindFacesClosestTo. + if (FindFacesClosestTo(point, radius, false, out faces)) + { + meshIds = new List>(); + lock (condemnedMeshesLock) + { + foreach (DistancePair pair in faces) + { + if (condemnedMeshes.Contains(pair.value.meshId)) continue; + DistancePair pairInt = new DistancePair(pair.distance, pair.value.meshId); + meshIds.Add(pairInt); + } + } + return true; + } + else + { + meshIds = new List>(); + return false; + } + } - /// - /// Finds the nearest meshes to a point ordered by nearness, which is defined as the distance from the point to - /// the center of the mesh's bounds in model-space, even if the origin point is - /// not within the nearby mesh, returning false if no mesh is within the given radius. - /// - /// This searches the Mesh Octree as opposed to using nearestFace as a proxy, and is intended as a - /// fallback when there are no nearby faces. - /// This method measures distance from the given point to the 'offset' of each mesh, which is a proxy for - /// the mesh center, though may not actually be the geometric center of the mesh. - /// This method only considers meshes even if the point does not lie within the bounding box the mesh, a simple - /// check that may be confused by any geometry more complex than a rectangular prism. - /// On the plus side, it's cheap. - /// - [MethodImplAttribute(MethodImplOptions.Synchronized)] - public bool FindNearestMeshesToNotIncludingPoint(Vector3 point, float radius, - out List> nearestMeshes, bool ignoreHiddenMeshes = false) { - Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); - List> results = new List>(); - HashSet meshIds; - - if (meshes.IntersectedBy(searchBounds, out meshIds)) { - lock (condemnedMeshesLock) { - foreach (int meshId in meshIds) { - // Confirm the mesh actually still exists in the model. - if (!model.HasMesh(meshId) || condemnedMeshes.Contains(meshId)) { - continue; + /// + /// Finds the nearest mesh to a point (given in model-space), returning false if no mesh + /// is within the given radius. + /// + /// This searches the Mesh Octree as opposed to using nearestFace as a proxy, and is intended as a + /// fallback when there are no nearby faces. + /// This method measures distance from the given point to the 'offset' of each mesh, which is a proxy for + /// the mesh center, though may not actually be the geometric center of the mesh. + /// This method only considers meshes if the point lies within the bounding box the mesh, a simple + /// check that may be confused by any geometry more complex than a rectangular prism. + /// On the plus side, it's cheap. + /// + [MethodImplAttribute(MethodImplOptions.Synchronized)] + public bool FindNearestMeshTo(Vector3 point, float radius, out int? nearestMesh, + bool ignoreHiddenMeshes = false) + { + Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); + nearestMesh = null; + HashSet meshIds; + + if (meshes.IntersectedBy(searchBounds, out meshIds)) + { + float minDistance = float.MaxValue; + + lock (condemnedMeshesLock) + { + foreach (int meshId in meshIds) + { + // Confirm the mesh actually still exists in the model. + if (!model.HasMesh(meshId) || condemnedMeshes.Contains(meshId)) + { + continue; + } + + if (ignoreHiddenMeshes && model.IsMeshHidden(meshId)) + { + continue; + } + + Bounds bounds = meshBounds.BoundsForItem(meshId); + if (!bounds.Contains(point)) + { + continue; + } + float distanceToPoint = Vector3.Distance(bounds.center, point); + if (distanceToPoint < minDistance) + { + minDistance = distanceToPoint; + nearestMesh = meshId; + } + } + } } - if (ignoreHiddenMeshes && model.IsMeshHidden(meshId)) { - continue; + return nearestMesh.HasValue; + } + + /// + /// Finds the nearest mesh to a point (given in model-space), even if the origin point is not within the + /// nearby mesh, returning false if no mesh is within the given radius. + /// + /// This searches the Mesh Octree as opposed to using nearestFace as a proxy, and is intended as a + /// fallback when there are no nearby faces. + /// This method measures distance from the given point to the 'offset' of each mesh, which is a proxy for + /// the mesh center, though may not actually be the geometric center of the mesh. + /// This method only considers meshes even if the point does not lie within the bounding box the mesh, a simple + /// check that may be confused by any geometry more complex than a rectangular prism. + /// On the plus side, it's cheap. + /// + [MethodImplAttribute(MethodImplOptions.Synchronized)] + public bool FindNearestMeshToNotIncludingPoint(Vector3 point, float radius, out int? nearestMesh, + bool ignoreHiddenMeshes = false) + { + Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); + nearestMesh = null; + HashSet meshIds; + + if (meshes.IntersectedBy(searchBounds, out meshIds)) + { + float minDistance = float.MaxValue; + + lock (condemnedMeshesLock) + { + foreach (int meshId in meshIds) + { + // Confirm the mesh actually still exists in the model. + if (!model.HasMesh(meshId) || condemnedMeshes.Contains(meshId)) + { + continue; + } + + if (ignoreHiddenMeshes && model.IsMeshHidden(meshId)) + { + continue; + } + + Bounds bounds = meshBounds.BoundsForItem(meshId); + float distanceToPoint = Vector3.Distance(bounds.center, point); + if (distanceToPoint < minDistance) + { + minDistance = distanceToPoint; + nearestMesh = meshId; + } + } + } } - Bounds bounds = meshBounds.BoundsForItem(meshId); - float distance = Vector3.Distance(bounds.center, point); - results.Add(new DistancePair(distance, meshId)); - } + return nearestMesh.HasValue; } - } - if (results.Count > 0) { - results.Sort(new DistancePairComparer()); - nearestMeshes = results; - return true; - } else { - nearestMeshes = results; - return false; - } - } - /// - /// Find the faces closest to a given point, within the given radius. Uses a heuristic to determine - /// which face is closest. That heuristic depends on the size of the face, the distance the point is - /// to the face's plane and the distance to the center of the face. - /// - /// The spatialIndex will return keys that belong to meshes that are hidden. We will need to manually - /// remove them using MeshMath.RemoveKeysForMeshes() - /// - /// The point we are finding faces close to. - /// The radius from the point we are finding faces in. - /// - /// Whether or not the point has to be inside the bounds of the face to be considered close. - /// - /// The faces that were found to be the closest. - /// Whether there are any close faces. - [MethodImplAttribute(MethodImplOptions.Synchronized)] - public bool FindFacesClosestTo(Vector3 point, float radius, bool ignoreInFace, - out List> closestFaces, int limit = 100) { - Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); - List> results = new List>(); - HashSet faceKeys; - if (faces.IntersectedBy(searchBounds, out faceKeys, limit)) { - lock (condemnedMeshesLock) { - foreach (FaceKey faceKey in faceKeys) { - // Confirm the mesh actually still exists in the model, and the face actually still exists in the mesh. - if (!model.HasMesh(faceKey.meshId) || condemnedMeshes.Contains(faceKey.meshId) || - !model.GetMesh(faceKey.meshId).HasFace(faceKey.faceId)) { - continue; + /// + /// Finds the nearest meshes to a point ordered by nearness, which is defined as the distance from the point to + /// the center of the mesh's bounds in model-space, even if the origin point is + /// not within the nearby mesh, returning false if no mesh is within the given radius. + /// + /// This searches the Mesh Octree as opposed to using nearestFace as a proxy, and is intended as a + /// fallback when there are no nearby faces. + /// This method measures distance from the given point to the 'offset' of each mesh, which is a proxy for + /// the mesh center, though may not actually be the geometric center of the mesh. + /// This method only considers meshes even if the point does not lie within the bounding box the mesh, a simple + /// check that may be confused by any geometry more complex than a rectangular prism. + /// On the plus side, it's cheap. + /// + [MethodImplAttribute(MethodImplOptions.Synchronized)] + public bool FindNearestMeshesToNotIncludingPoint(Vector3 point, float radius, + out List> nearestMeshes, bool ignoreHiddenMeshes = false) + { + Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); + List> results = new List>(); + HashSet meshIds; + + if (meshes.IntersectedBy(searchBounds, out meshIds)) + { + lock (condemnedMeshesLock) + { + foreach (int meshId in meshIds) + { + // Confirm the mesh actually still exists in the model. + if (!model.HasMesh(meshId) || condemnedMeshes.Contains(meshId)) + { + continue; + } + + if (ignoreHiddenMeshes && model.IsMeshHidden(meshId)) + { + continue; + } + + Bounds bounds = meshBounds.BoundsForItem(meshId); + float distance = Vector3.Distance(bounds.center, point); + results.Add(new DistancePair(distance, meshId)); + } + } + } + if (results.Count > 0) + { + results.Sort(new DistancePairComparer()); + nearestMeshes = results; + return true; } - FaceInfo info = faceInfo[faceKey]; - float distanceToPlane = info.plane.GetDistanceToPoint(point); - if (Mathf.Abs(distanceToPlane) < radius) { - // Add the face to the results if we don't care about the position being in the border of the face - // or if we do care about the position being within the border of the face and it is. - if (ignoreInFace || Math3d.IsInside(info.border, point - info.plane.normal * distanceToPlane)) { - results.Add(new DistancePair(Mathf.Abs(distanceToPlane), faceKey)); - } + else + { + nearestMeshes = results; + return false; } - } } - } - if (results.Count > 0) { - results.Sort(new DistancePairComparer()); - closestFaces = results; - return true; - } else { - closestFaces = results; - return false; - } - } - /// - /// Find the meshes closest to a given point, within the given radius. We do this by testing intersection - /// against faces, and then adding the meshIds of all faces to a hashSet. - /// - /// The spatialIndex will return keys that belong to meshes that are hidden, so we manually remove those at - /// the end of the process. - /// - /// The point we are finding faces close to. - /// The radius from the point we are finding faces in. - /// The meshes that were found within the radius. - [MethodImplAttribute(MethodImplOptions.Synchronized)] - public void FindMeshesClosestTo(Vector3 point, float radius, out HashSet closeMeshes) { - Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); - closeMeshes = new HashSet(); - HashSet faceKeys; - if (faces.IntersectedBy(searchBounds, out faceKeys)) { - lock (condemnedMeshesLock) { - foreach (FaceKey faceKey in faceKeys) { - if (closeMeshes.Contains(faceKey.meshId)) continue; - // Confirm the mesh actually still exists in the model, and the face actually still exists in the mesh. - if (!model.HasMesh(faceKey.meshId) || condemnedMeshes.Contains(faceKey.meshId) || - !model.GetMesh(faceKey.meshId).HasFace(faceKey.faceId)) { - continue; + /// + /// Find the faces closest to a given point, within the given radius. Uses a heuristic to determine + /// which face is closest. That heuristic depends on the size of the face, the distance the point is + /// to the face's plane and the distance to the center of the face. + /// + /// The spatialIndex will return keys that belong to meshes that are hidden. We will need to manually + /// remove them using MeshMath.RemoveKeysForMeshes() + /// + /// The point we are finding faces close to. + /// The radius from the point we are finding faces in. + /// + /// Whether or not the point has to be inside the bounds of the face to be considered close. + /// + /// The faces that were found to be the closest. + /// Whether there are any close faces. + [MethodImplAttribute(MethodImplOptions.Synchronized)] + public bool FindFacesClosestTo(Vector3 point, float radius, bool ignoreInFace, + out List> closestFaces, int limit = 100) + { + Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); + List> results = new List>(); + HashSet faceKeys; + if (faces.IntersectedBy(searchBounds, out faceKeys, limit)) + { + lock (condemnedMeshesLock) + { + foreach (FaceKey faceKey in faceKeys) + { + // Confirm the mesh actually still exists in the model, and the face actually still exists in the mesh. + if (!model.HasMesh(faceKey.meshId) || condemnedMeshes.Contains(faceKey.meshId) || + !model.GetMesh(faceKey.meshId).HasFace(faceKey.faceId)) + { + continue; + } + FaceInfo info = faceInfo[faceKey]; + float distanceToPlane = info.plane.GetDistanceToPoint(point); + if (Mathf.Abs(distanceToPlane) < radius) + { + // Add the face to the results if we don't care about the position being in the border of the face + // or if we do care about the position being within the border of the face and it is. + if (ignoreInFace || Math3d.IsInside(info.border, point - info.plane.normal * distanceToPlane)) + { + results.Add(new DistancePair(Mathf.Abs(distanceToPlane), faceKey)); + } + } + } + } } + if (results.Count > 0) + { + results.Sort(new DistancePairComparer()); + closestFaces = results; + return true; + } + else + { + closestFaces = results; + return false; + } + } - FaceInfo info = faceInfo[faceKey]; - float distanceToPlane = info.plane.GetDistanceToPoint(point); - if (Mathf.Abs(distanceToPlane) < radius) { - closeMeshes.Add(faceKey.meshId); + /// + /// Find the meshes closest to a given point, within the given radius. We do this by testing intersection + /// against faces, and then adding the meshIds of all faces to a hashSet. + /// + /// The spatialIndex will return keys that belong to meshes that are hidden, so we manually remove those at + /// the end of the process. + /// + /// The point we are finding faces close to. + /// The radius from the point we are finding faces in. + /// The meshes that were found within the radius. + [MethodImplAttribute(MethodImplOptions.Synchronized)] + public void FindMeshesClosestTo(Vector3 point, float radius, out HashSet closeMeshes) + { + Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); + closeMeshes = new HashSet(); + HashSet faceKeys; + if (faces.IntersectedBy(searchBounds, out faceKeys)) + { + lock (condemnedMeshesLock) + { + foreach (FaceKey faceKey in faceKeys) + { + if (closeMeshes.Contains(faceKey.meshId)) continue; + // Confirm the mesh actually still exists in the model, and the face actually still exists in the mesh. + if (!model.HasMesh(faceKey.meshId) || condemnedMeshes.Contains(faceKey.meshId) || + !model.GetMesh(faceKey.meshId).HasFace(faceKey.faceId)) + { + continue; + } + + FaceInfo info = faceInfo[faceKey]; + float distanceToPlane = info.plane.GetDistanceToPoint(point); + if (Mathf.Abs(distanceToPlane) < radius) + { + closeMeshes.Add(faceKey.meshId); + } + } + } } - } + closeMeshes.ExceptWith(model.GetHiddenMeshes()); } - } - closeMeshes.ExceptWith(model.GetHiddenMeshes()); - } - - /// - /// Find the meshes closest to a given point, within the given radius. We do this by testing intersection - /// against faces, and then adding the meshIds of all faces to a hashSet. - /// - /// The spatialIndex will return keys that belong to meshes that are hidden, so we manually remove those at - /// the end of the process. - /// - /// The point we are finding faces close to. - /// The radius from the point we are finding faces in. - /// The meshes that were found within the radius. - [MethodImplAttribute(MethodImplOptions.Synchronized)] - public void FindMeshesClosestToDirect(Vector3 point, float radius, ref HashSet closeMeshes) { - Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); - meshBounds.IntersectedByPreallocated(searchBounds, ref closeMeshes); - closeMeshes.ExceptWith(model.GetHiddenMeshes()); - } - /// - /// The heuristic to determine which face is closest to a given point. This is probably - /// something we want to tinker with. For now it takes the rms of the distance to the plane - /// and to the center then adds a "fudge" to make larger faces seem farther away (so it'll - /// be easier to select small faces.) - /// - public static float AdjustedFaceDistance(float disToPlane, float disToCenter, float radius) { - return Mathf.Sqrt(disToPlane * disToPlane + disToCenter * disToCenter) + radius * 0.001f; - } + /// + /// Find the meshes closest to a given point, within the given radius. We do this by testing intersection + /// against faces, and then adding the meshIds of all faces to a hashSet. + /// + /// The spatialIndex will return keys that belong to meshes that are hidden, so we manually remove those at + /// the end of the process. + /// + /// The point we are finding faces close to. + /// The radius from the point we are finding faces in. + /// The meshes that were found within the radius. + [MethodImplAttribute(MethodImplOptions.Synchronized)] + public void FindMeshesClosestToDirect(Vector3 point, float radius, ref HashSet closeMeshes) + { + Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); + meshBounds.IntersectedByPreallocated(searchBounds, ref closeMeshes); + closeMeshes.ExceptWith(model.GetHiddenMeshes()); + } - /// - /// Find the edges closest to a given point, within the given radius. Uses a heuristic to determine - /// which edge is closest. That heuristic depends on the length of the edge, and the distance to the line - /// of the edge. - /// - /// The spatialIndex will return keys that belong to meshes that are hidden. We will need to manually - /// remove them using MeshMath.RemoveKeysForMeshes() - /// - /// The point we are finding edges close to. - /// The radius from the point we are finding edges in. - /// - /// Whether or not the position has to be on the edge to be considered close. - /// - /// The edges that were found to be the closest. - /// Whether there are any close edges. - [MethodImplAttribute(MethodImplOptions.Synchronized)] - public bool FindEdgesClosestTo(Vector3 point, float radius, bool ignoreInEdge, - out List> closestEdges) { - Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); - List> results = new List>(); - HashSet edgeKeys; - if (edges.IntersectedBy(searchBounds, out edgeKeys)) { - lock (condemnedMeshesLock) { - foreach (EdgeKey edgeKey in edgeKeys) { - // Confirm the mesh actually still exists in the model, and the verts actually still exist in the mesh. - if (!model.HasMesh(edgeKey.meshId) || condemnedMeshes.Contains(edgeKey.meshId)) { - continue; + /// + /// The heuristic to determine which face is closest to a given point. This is probably + /// something we want to tinker with. For now it takes the rms of the distance to the plane + /// and to the center then adds a "fudge" to make larger faces seem farther away (so it'll + /// be easier to select small faces.) + /// + public static float AdjustedFaceDistance(float disToPlane, float disToCenter, float radius) + { + return Mathf.Sqrt(disToPlane * disToPlane + disToCenter * disToCenter) + radius * 0.001f; + } + + /// + /// Find the edges closest to a given point, within the given radius. Uses a heuristic to determine + /// which edge is closest. That heuristic depends on the length of the edge, and the distance to the line + /// of the edge. + /// + /// The spatialIndex will return keys that belong to meshes that are hidden. We will need to manually + /// remove them using MeshMath.RemoveKeysForMeshes() + /// + /// The point we are finding edges close to. + /// The radius from the point we are finding edges in. + /// + /// Whether or not the position has to be on the edge to be considered close. + /// + /// The edges that were found to be the closest. + /// Whether there are any close edges. + [MethodImplAttribute(MethodImplOptions.Synchronized)] + public bool FindEdgesClosestTo(Vector3 point, float radius, bool ignoreInEdge, + out List> closestEdges) + { + Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); + List> results = new List>(); + HashSet edgeKeys; + if (edges.IntersectedBy(searchBounds, out edgeKeys)) + { + lock (condemnedMeshesLock) + { + foreach (EdgeKey edgeKey in edgeKeys) + { + // Confirm the mesh actually still exists in the model, and the verts actually still exist in the mesh. + if (!model.HasMesh(edgeKey.meshId) || condemnedMeshes.Contains(edgeKey.meshId)) + { + continue; + } + MMesh mesh = model.GetMesh(edgeKey.meshId); + if (!mesh.HasVertex(edgeKey.vertexId1) || !mesh.HasVertex(edgeKey.vertexId2)) + { + continue; + } + EdgeInfo info = edgeInfo[edgeKey]; + // Project point onto line: + Vector3 linePointToPoint = point - info.edgeStart; + float t = Vector3.Dot(linePointToPoint, info.edgeVector); + if (ignoreInEdge || (t >= 0 && t <= info.length)) + { + Vector3 onLine = info.edgeStart + info.edgeVector * t; + float distanceToLine = Vector3.Distance(onLine, point); + if (distanceToLine < radius) + { + results.Add(new DistancePair(distanceToLine, edgeKey)); + } + } + } + } } - MMesh mesh = model.GetMesh(edgeKey.meshId); - if (!mesh.HasVertex(edgeKey.vertexId1) || !mesh.HasVertex(edgeKey.vertexId2)) { - continue; + if (results.Count > 0) + { + results.Sort(new DistancePairComparer()); + closestEdges = results; + return true; } - EdgeInfo info = edgeInfo[edgeKey]; - // Project point onto line: - Vector3 linePointToPoint = point - info.edgeStart; - float t = Vector3.Dot(linePointToPoint, info.edgeVector); - if (ignoreInEdge || (t >= 0 && t <= info.length)) { - Vector3 onLine = info.edgeStart + info.edgeVector * t; - float distanceToLine = Vector3.Distance(onLine, point); - if (distanceToLine < radius) { - results.Add(new DistancePair(distanceToLine, edgeKey)); - } + else + { + closestEdges = new List>(); + return false; } - } } - } - if (results.Count > 0) { - results.Sort(new DistancePairComparer()); - closestEdges = results; - return true; - } else { - closestEdges = new List>(); - return false; - } - } - /// - /// The heuristic to determine which edge is closest to a given point. Takes the distance to the - /// line of the edge and adds a penalty for longer edges. This makes shorter edges a little easier - /// to select. - /// - private float AdjustedEdgeDistance(float disToLine, float length) { - return disToLine + length * 0.001f; - } + /// + /// The heuristic to determine which edge is closest to a given point. Takes the distance to the + /// line of the edge and adds a penalty for longer edges. This makes shorter edges a little easier + /// to select. + /// + private float AdjustedEdgeDistance(float disToLine, float length) + { + return disToLine + length * 0.001f; + } - /// - /// Find the vertex closest to a given point, within the given radius. - /// - /// The spatialIndex will return keys that belong to meshes that are hidden. We will need to manually - /// remove them using MeshMath.RemoveKeysForMeshes() - /// - /// The point we are finding vertices close to. - /// The radius from the point we are finding vertices in. - /// The vertices that were found to be the closest. - /// If there are any close vertices. - [MethodImplAttribute(MethodImplOptions.Synchronized)] - public bool FindVerticesClosestTo(Vector3 point, float radius, out List> closestVertices) { - List> results = new List>(); - Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); - HashSet verts; - if (vertices.IntersectedBy(searchBounds, out verts)) { - lock (condemnedMeshesLock) { - foreach (VertexKey vert in verts) { - // Confirm the mesh actually still exists in the model, and the vert actually still exists in the mesh. - if (!model.HasMesh(vert.meshId) || condemnedMeshes.Contains(vert.meshId) || - !model.GetMesh(vert.meshId).HasVertex(vert.vertexId)) { - continue; + /// + /// Find the vertex closest to a given point, within the given radius. + /// + /// The spatialIndex will return keys that belong to meshes that are hidden. We will need to manually + /// remove them using MeshMath.RemoveKeysForMeshes() + /// + /// The point we are finding vertices close to. + /// The radius from the point we are finding vertices in. + /// The vertices that were found to be the closest. + /// If there are any close vertices. + [MethodImplAttribute(MethodImplOptions.Synchronized)] + public bool FindVerticesClosestTo(Vector3 point, float radius, out List> closestVertices) + { + List> results = new List>(); + Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); + HashSet verts; + if (vertices.IntersectedBy(searchBounds, out verts)) + { + lock (condemnedMeshesLock) + { + foreach (VertexKey vert in verts) + { + // Confirm the mesh actually still exists in the model, and the vert actually still exists in the mesh. + if (!model.HasMesh(vert.meshId) || condemnedMeshes.Contains(vert.meshId) || + !model.GetMesh(vert.meshId).HasVertex(vert.vertexId)) + { + continue; + } + Bounds bounds = vertices.BoundsForItem(vert); + float distance = Vector3.Distance(point, bounds.center); + if (distance < radius) + { + results.Add(new DistancePair(distance, vert)); + } + } + } + } + if (results.Count > 0) + { + results.Sort(new DistancePairComparer()); + closestVertices = results; + return true; } - Bounds bounds = vertices.BoundsForItem(vert); - float distance = Vector3.Distance(point, bounds.center); - if (distance < radius) { - results.Add(new DistancePair(distance, vert)); + else + { + closestVertices = new List>(); + return false; } - } } - } - if (results.Count > 0) { - results.Sort(new DistancePairComparer()); - closestVertices = results; - return true; - } else { - closestVertices = new List>(); - return false; - } - } - /// - /// Find the vertex closest to a given point, within the given radius. - /// - /// The spatialIndex will return keys that belong to meshes that are hidden. We will need to manually - /// remove them using MeshMath.RemoveKeysForMeshes() - /// - /// The point we are finding vertices close to. - /// The radius from the point we are finding vertices in. - /// The vertices that were found to be the closest. - [MethodImplAttribute(MethodImplOptions.Synchronized)] - public void FindVerticesClosestTo(Vector3 point, float radius, out HashSet closestVertices) { - closestVertices = new HashSet(); - Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); - HashSet verts; - - if (vertices.IntersectedBy(searchBounds, out verts)) { - float radius2 = radius * radius; - lock (condemnedMeshesLock) { - foreach (VertexKey vert in verts) { - // Confirm the mesh actually still exists in the model, and the vert actually still exists in the mesh. - if (!model.HasMesh(vert.meshId) || condemnedMeshes.Contains(vert.meshId) || - !model.GetMesh(vert.meshId).HasVertex(vert.vertexId)) { - continue; - } - Bounds bounds = vertices.BoundsForItem(vert); - Vector3 diff = point - bounds.center; - float dist2 = Vector3.Dot(diff, diff); - if (dist2 < radius2) { - closestVertices.Add(vert); + /// + /// Find the vertex closest to a given point, within the given radius. + /// + /// The spatialIndex will return keys that belong to meshes that are hidden. We will need to manually + /// remove them using MeshMath.RemoveKeysForMeshes() + /// + /// The point we are finding vertices close to. + /// The radius from the point we are finding vertices in. + /// The vertices that were found to be the closest. + [MethodImplAttribute(MethodImplOptions.Synchronized)] + public void FindVerticesClosestTo(Vector3 point, float radius, out HashSet closestVertices) + { + closestVertices = new HashSet(); + Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); + HashSet verts; + + if (vertices.IntersectedBy(searchBounds, out verts)) + { + float radius2 = radius * radius; + lock (condemnedMeshesLock) + { + foreach (VertexKey vert in verts) + { + // Confirm the mesh actually still exists in the model, and the vert actually still exists in the mesh. + if (!model.HasMesh(vert.meshId) || condemnedMeshes.Contains(vert.meshId) || + !model.GetMesh(vert.meshId).HasVertex(vert.vertexId)) + { + continue; + } + Bounds bounds = vertices.BoundsForItem(vert); + Vector3 diff = point - bounds.center; + float dist2 = Vector3.Dot(diff, diff); + if (dist2 < radius2) + { + closestVertices.Add(vert); + } + } + } } - } } - } - } - /// - /// Find the edges closest to a given point, within the given radius, using the closest distance between - /// the point and the line segment belonging to the edge. - /// - /// The spatialIndex will return keys that belong to meshes that are hidden. We will need to manually - /// remove them using MeshMath.RemoveKeysForMeshes() - /// - /// The point we are finding edges close to. - /// The radius from the point we are finding edges in. - /// The edges that were found to be the closest. - [MethodImplAttribute(MethodImplOptions.Synchronized)] - public void FindEdgesClosestTo(Vector3 point, float radius, out HashSet closestEdges) { - closestEdges = new HashSet(); - Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); - HashSet edgeKeys; - if (edges.IntersectedBy(searchBounds, out edgeKeys)) { - float radius2 = radius * radius; - lock (condemnedMeshesLock) { - foreach (EdgeKey edgeKey in edgeKeys) { - // Confirm the mesh actually still exists in the model, and the verts actually still exist in the mesh. - if (!model.HasMesh(edgeKey.meshId) || condemnedMeshes.Contains(edgeKey.meshId)) { - continue; + /// + /// Find the edges closest to a given point, within the given radius, using the closest distance between + /// the point and the line segment belonging to the edge. + /// + /// The spatialIndex will return keys that belong to meshes that are hidden. We will need to manually + /// remove them using MeshMath.RemoveKeysForMeshes() + /// + /// The point we are finding edges close to. + /// The radius from the point we are finding edges in. + /// The edges that were found to be the closest. + [MethodImplAttribute(MethodImplOptions.Synchronized)] + public void FindEdgesClosestTo(Vector3 point, float radius, out HashSet closestEdges) + { + closestEdges = new HashSet(); + Bounds searchBounds = new Bounds(point, Vector3.one * radius * 2); + HashSet edgeKeys; + if (edges.IntersectedBy(searchBounds, out edgeKeys)) + { + float radius2 = radius * radius; + lock (condemnedMeshesLock) + { + foreach (EdgeKey edgeKey in edgeKeys) + { + // Confirm the mesh actually still exists in the model, and the verts actually still exist in the mesh. + if (!model.HasMesh(edgeKey.meshId) || condemnedMeshes.Contains(edgeKey.meshId)) + { + continue; + } + MMesh mesh = model.GetMesh(edgeKey.meshId); + if (!mesh.HasVertex(edgeKey.vertexId1) || !mesh.HasVertex(edgeKey.vertexId2)) + { + continue; + } + EdgeInfo info = edgeInfo[edgeKey]; + // Project point onto line: + Vector3 linePointToPoint = point - info.edgeStart; + float t = Vector3.Dot(linePointToPoint, info.edgeVector); + t = Mathf.Clamp(t, 0.0f, info.length); + Vector3 onLine = info.edgeStart + info.edgeVector * t; + Vector3 diff = point - onLine; + float dist2 = Vector3.Dot(diff, diff); + if (dist2 < radius2) + { + closestEdges.Add(edgeKey); + } + } + } } - MMesh mesh = model.GetMesh(edgeKey.meshId); - if (!mesh.HasVertex(edgeKey.vertexId1) || !mesh.HasVertex(edgeKey.vertexId2)) { - continue; - } - EdgeInfo info = edgeInfo[edgeKey]; - // Project point onto line: - Vector3 linePointToPoint = point - info.edgeStart; - float t = Vector3.Dot(linePointToPoint, info.edgeVector); - t = Mathf.Clamp(t, 0.0f, info.length); - Vector3 onLine = info.edgeStart + info.edgeVector * t; - Vector3 diff = point - onLine; - float dist2 = Vector3.Dot(diff, diff); - if (dist2 < radius2) { - closestEdges.Add(edgeKey); - } - } } - } - } - /// - /// Finds meshes which intersect the given bounds. - /// - [MethodImplAttribute(MethodImplOptions.Synchronized)] - public bool FindIntersectingMeshes(Bounds boundingBox, out HashSet meshIds) { - bool success = meshBounds.IntersectedBy(boundingBox, out meshIds); - if (success) { - // Confirm the meshes actually still exist in the model. - List missingOrCondemnedMeshIds = new List(); - lock (condemnedMeshesLock) { - foreach (int meshId in meshIds) { - if (!model.HasMesh(meshId) || condemnedMeshes.Contains(meshId)) { - missingOrCondemnedMeshIds.Add(meshId); + /// + /// Finds meshes which intersect the given bounds. + /// + [MethodImplAttribute(MethodImplOptions.Synchronized)] + public bool FindIntersectingMeshes(Bounds boundingBox, out HashSet meshIds) + { + bool success = meshBounds.IntersectedBy(boundingBox, out meshIds); + if (success) + { + // Confirm the meshes actually still exist in the model. + List missingOrCondemnedMeshIds = new List(); + lock (condemnedMeshesLock) + { + foreach (int meshId in meshIds) + { + if (!model.HasMesh(meshId) || condemnedMeshes.Contains(meshId)) + { + missingOrCondemnedMeshIds.Add(meshId); + } + } + } + foreach (int meshId in missingOrCondemnedMeshIds) + { + meshIds.Remove(meshId); + } } - } + return success; } - foreach (int meshId in missingOrCondemnedMeshIds) { - meshIds.Remove(meshId); + + /// + /// Resets the entire state of the Spatial Index, using the given bounds. + /// + public void Reset(Bounds bounds) + { + Setup(bounds); } - } - return success; - } - /// - /// Resets the entire state of the Spatial Index, using the given bounds. - /// - public void Reset(Bounds bounds) { - Setup(bounds); - } + public static FaceInfo CalculateFaceInfo(MMesh mesh, Face face) + { + Vector3 center = Vector3.zero; + Vector3 firstPt = mesh.VertexPositionInModelCoords(face.vertexIds[0]); + Bounds bounds = new Bounds(firstPt, Vector3.zero); + List coords = new List(); + + foreach (int vertId in face.vertexIds) + { + Vector3 inModelCoords = mesh.VertexPositionInModelCoords(vertId); + center += inModelCoords; + bounds.Encapsulate(inModelCoords); + coords.Add(inModelCoords); + } + center /= face.vertexIds.Count; - public static FaceInfo CalculateFaceInfo(MMesh mesh, Face face) { - Vector3 center = Vector3.zero; - Vector3 firstPt = mesh.VertexPositionInModelCoords(face.vertexIds[0]); - Bounds bounds = new Bounds(firstPt, Vector3.zero); - List coords = new List(); - - foreach (int vertId in face.vertexIds) { - Vector3 inModelCoords = mesh.VertexPositionInModelCoords(vertId); - center += inModelCoords; - bounds.Encapsulate(inModelCoords); - coords.Add(inModelCoords); - } - center /= face.vertexIds.Count; - - FaceInfo faceInfo = new FaceInfo(); - faceInfo.baryCenter = center; - faceInfo.bounds = bounds; - faceInfo.plane = new Plane(MeshMath.CalculateNormal(coords), center); - faceInfo.border = coords; - - return faceInfo; - } + FaceInfo faceInfo = new FaceInfo(); + faceInfo.baryCenter = center; + faceInfo.bounds = bounds; + faceInfo.plane = new Plane(MeshMath.CalculateNormal(coords), center); + faceInfo.border = coords; - private EdgeInfo CalculateEdgeInfo(Vector3 start, Vector3 end) { - EdgeInfo edgeInfo = new EdgeInfo(); - Bounds bounds = new Bounds(start, Vector3.zero); - bounds.Encapsulate(end); - edgeInfo.bounds = bounds; - edgeInfo.length = Vector3.Distance(start, end); - edgeInfo.edgeStart = start; - edgeInfo.edgeVector = (end - start).normalized; - return edgeInfo; - } + return faceInfo; + } - [MethodImplAttribute(MethodImplOptions.Synchronized)] - public FaceInfo GetFaceInfo(FaceKey key) { - return faceInfo[key]; - } + private EdgeInfo CalculateEdgeInfo(Vector3 start, Vector3 end) + { + EdgeInfo edgeInfo = new EdgeInfo(); + Bounds bounds = new Bounds(start, Vector3.zero); + bounds.Encapsulate(end); + edgeInfo.bounds = bounds; + edgeInfo.length = Vector3.Distance(start, end); + edgeInfo.edgeStart = start; + edgeInfo.edgeVector = (end - start).normalized; + return edgeInfo; + } - [MethodImplAttribute(MethodImplOptions.Synchronized)] - public bool TryGetFaceInfo(FaceKey key, out FaceInfo info) { - return faceInfo.TryGetValue(key, out info); - } + [MethodImplAttribute(MethodImplOptions.Synchronized)] + public FaceInfo GetFaceInfo(FaceKey key) + { + return faceInfo[key]; + } - [MethodImplAttribute(MethodImplOptions.Synchronized)] - private void LoadMeshIntoIndex(MMesh mesh, - Dictionary faceInfos, Dictionary edgeInfos) { - lock (condemnedMeshesLock) { - condemnedMeshes.Remove(mesh.id); - } - meshes.Add(mesh.id, mesh.bounds); - meshBounds.Add(mesh.id, mesh.bounds); - foreach (KeyValuePair pair in faceInfos) { - faces.Add(pair.Key, pair.Value.bounds); - faceInfo[pair.Key] = pair.Value; - } - foreach (KeyValuePair pair in edgeInfos) { - edges.Add(pair.Key, pair.Value.bounds); - edgeInfo[pair.Key] = pair.Value; - } - foreach (int vertId in mesh.GetVertexIds()) { - vertices.Add(new VertexKey(mesh.id, vertId), - new Bounds(mesh.VertexPositionInModelCoords(vertId), Vector3.zero)); - } - } + [MethodImplAttribute(MethodImplOptions.Synchronized)] + public bool TryGetFaceInfo(FaceKey key, out FaceInfo info) + { + return faceInfo.TryGetValue(key, out info); + } + + [MethodImplAttribute(MethodImplOptions.Synchronized)] + private void LoadMeshIntoIndex(MMesh mesh, + Dictionary faceInfos, Dictionary edgeInfos) + { + lock (condemnedMeshesLock) + { + condemnedMeshes.Remove(mesh.id); + } + meshes.Add(mesh.id, mesh.bounds); + meshBounds.Add(mesh.id, mesh.bounds); + foreach (KeyValuePair pair in faceInfos) + { + faces.Add(pair.Key, pair.Value.bounds); + faceInfo[pair.Key] = pair.Value; + } + foreach (KeyValuePair pair in edgeInfos) + { + edges.Add(pair.Key, pair.Value.bounds); + edgeInfo[pair.Key] = pair.Value; + } + foreach (int vertId in mesh.GetVertexIds()) + { + vertices.Add(new VertexKey(mesh.id, vertId), + new Bounds(mesh.VertexPositionInModelCoords(vertId), Vector3.zero)); + } + } - // Test only. - public SpatialIndex(Bounds bounds) { - Setup(bounds); + // Test only. + public SpatialIndex(Bounds bounds) + { + Setup(bounds); + } } - } } diff --git a/Assets/Scripts/model/core/Spine.cs b/Assets/Scripts/model/core/Spine.cs index 230147d6..b35460d9 100644 --- a/Assets/Scripts/model/core/Spine.cs +++ b/Assets/Scripts/model/core/Spine.cs @@ -17,367 +17,402 @@ using System.Collections.Generic; using UnityEngine; -namespace com.google.apps.peltzer.client.model.core { - /// - /// Struct for a vertebra. A vertebra is part of a spine and consists of: A position, which when combined with the - /// spine's origin defines a spine; and the allowed normal of a face placed on this vertebra. - /// See bug DIAGRAM 0 ANATOMY OF A SPINE. - /// - public class Vertebra { - // The model space position of the vertebra. - public Vector3 position; - // The direction of this vertebra. This is equivalent to the vector that points from the origin of the spine to - // the vertebra position. This is used to check if different vertebrae are in line with each other. - public Vector3 direction; - // The normal of a face if it is placed on top of this vertebra. - public Vector3 normal; - - public Vertebra(Vector3 position, Vector3 direction, Vector3 normal) { - this.position = position; - this.direction = direction; - this.normal = normal; - } - - /// - /// Checks if a given vertebra is equivalent to this vertebra. - /// - /// The vertebra being compared. - /// Whether they have the same position and normal. - public bool IsEquivalent(Vertebra other) { - if (other == null) { - return false; - } - - if (Math3d.CompareVectors(other.position, position, 0.001f) - && Math3d.CompareVectors(other.normal, normal, 0.001f)) { - return true; - } - - return false; - } - } - - /// - /// A spine is the geometric structure that a segment of a stroke is formed around. A group of spines connected - /// together defines a stroke. A spine is just a line with a defined length and angle. The spine has an origin - /// point which is the ending of the last spine segment and ends at one of a pre-determined set of "vertebrae". - /// The positions of the valid vertebrae are geometrically designed so that a stroke face can be rotated around - /// the vertebra and an edge on the stroke face will lay perfectly flush with the previous stroke face. - /// - /// See bug DIAGRAM 0 ANATOMY OF A SPINE. - /// - public class Spine { - /// - /// The weight given to choosing the normalVertebra. A larger weight will create more controlled angular strokes - /// by "sticking" to the normal while a smaller weight will produce more variation. - /// - private const float NORMAL_WEIGHT = 1.0f; - /// - /// An angle threshold in degrees to add weight for selecting the normal vertebra. - /// - private const float NORMAL_THRESHOLD = 65.0f; - /// - /// An angle threshold in degrees to add weight for re-selecting the current vertebra as the nearest vertebra. - /// - private const float CURRENT_VERTEBRA_THRESHOLD = 10f; - /// - /// An angle threshold in degrees to add weight for selecting a new vertebra that shares the same direction vector - /// as the last spine. - /// - private const float LAST_SPINE_THRESHOLD = 5f; - /// - /// The angle we will "bend" the stroke so that we can safely create a cap on the stroke where it changes direction. - /// This is almost 180 because we want to create a new face at an angle that points in the opposite direction but - /// still has some volume to avoid exposing backfaces or creating points in the stroke with zero volume. - /// - private const float CAP_BEND = 170f; - /// - /// The allowed change in rotation from face to face. - /// - public const float CURVATURE = 45f; +namespace com.google.apps.peltzer.client.model.core +{ /// - /// Handle floating point errors. + /// Struct for a vertebra. A vertebra is part of a spine and consists of: A position, which when combined with the + /// spine's origin defines a spine; and the allowed normal of a face placed on this vertebra. + /// See bug DIAGRAM 0 ANATOMY OF A SPINE. /// - public const float EPSILON = 0.01f; + public class Vertebra + { + // The model space position of the vertebra. + public Vector3 position; + // The direction of this vertebra. This is equivalent to the vector that points from the origin of the spine to + // the vertebra position. This is used to check if different vertebrae are in line with each other. + public Vector3 direction; + // The normal of a face if it is placed on top of this vertebra. + public Vector3 normal; + + public Vertebra(Vector3 position, Vector3 direction, Vector3 normal) + { + this.position = position; + this.direction = direction; + this.normal = normal; + } - /// - /// The length of the spine. The length of the spine is the distance between the centers of two faces such that if - /// one of the faces is rotated at some angle theta (CURVATURE) to the other face the faces will meet but - /// not intersect. - /// - public readonly float length; - /// - /// The normal of the face used to generate this spine. - /// - public readonly Vector3 normal; - /// - /// The origin of the spine. A spine segement is a line from the origin to one of the validVertebrae. The origin is - /// equivalent to the center of the face used to define the spine. - /// - public readonly Vector3 origin; - /// - /// The angle in degrees between the normal and all valid vertebrae excluding the normalVertebra. - /// - private readonly float spineDegrees; - /// - /// The vertebra along the spine's normal. - /// - private readonly Vertebra normalVertebra; - /// - /// The other possible vertebrae for the spine excluding the normal. Each vertebra correlates to an edge of the - /// defining face. See bug DIAGRAM 6. - /// - private readonly List validVertebrae; - /// - /// The vertebra that is currently selected for this spine. - /// - private Vertebra currentVertebra; - /// - /// The radius of the face that defines this spine. - /// - private float radius; - private float vertLength; + /// + /// Checks if a given vertebra is equivalent to this vertebra. + /// + /// The vertebra being compared. + /// Whether they have the same position and normal. + public bool IsEquivalent(Vertebra other) + { + if (other == null) + { + return false; + } - /// - /// Creates a spine from the coplanar clockwise vertices defining a face. - /// - /// The vertices of the defining face. - /// - /// An axis that all vertebrae must be perpendicular to. The definingAxis lets us limit stroke creation so that the - /// stroke is locked to a plane. The definingAxis represents the normal of the plane, which guarantees that any - /// vector perpendicular to the definingAxis exists in the plane. - /// - public Spine(List vertices, Vector3 definingAxis) { - origin = MeshMath.CalculateGeometricCenter(vertices); - normal = MeshMath.CalculateNormal(vertices); - // spineDegrees is the angle between the normal and every non-normal valid vertebra. See bug DIAGRAM 2. - spineDegrees = CURVATURE / 2f; - - // Find the length of the spine. See bug DIAGRAM 1. - // This is the length if we bend the spine so that edges are flush. This is more restrictive than vertLength so we - // use this as the defining length of the spine. - length = FindSpineLength(MeshMath.FindHeightOfARegularPolygonalFace(vertices)); - // Find the radius of the defining face. - radius = MeshMath.FindRadiusOfARegularPolygonalFace(vertices); - vertLength = FindSpineLength(radius * 2.0f); - Vector3 spine = normal * length; - - // We have everything we need to define the normalVertebra. - normalVertebra = new Vertebra(origin + spine, spine, normal); - - // Now we want to find all the other vertebrae. To do this we define two vectors. One which will represent the - // vertebra and one that will represent the normal for that vertebra. We rotate both vectors so that their - // projections onto the defining face would be perpendicular with any edge. The one used to define vertebra is - // rotated spineDegrees and the one used to define the normal is rotated by the allowed curvature. Now that we - // have these vectors we just rotate them around the normal so we can use them to define a vertebra for each - // edge. See bug DIAGRAM 3 and DIAGRAM 4. - if (definingAxis == Vector3.zero) { - // The faceAxis is used as an axis to rotate around so that the normal is perpendicular with an edge. - Vector3 faceAxis = vertices[1] - vertices[0]; - - Quaternion spineRotation = Quaternion.AngleAxis(spineDegrees, faceAxis); - Vector3 rotatedNormal = (Quaternion.AngleAxis(CURVATURE, faceAxis) * spine).normalized; - - // The angle we will need to rotate the vectors so that we can define a vertebra for each edge. - // Note: The number of vertices == number of edges. - float angle = 360f / (vertices.Count * 2); - validVertebrae = new List(); - - // Rotate around the normal and define a vertebra for each rotation until we complete a full rotation. - // See bug DIAGRAM 5(top), DIAGRAM 5(side), DIAGRAM 6. - for (int i = 0; i * angle < (360f - EPSILON); i++) { - Vector3 referenceVector = i % 2 == 0 ? spineRotation * spine : spineRotation * (normal * vertLength); - Vector3 vertebraPosition = origin + Quaternion.AngleAxis(i * angle, normal) * referenceVector; - Vector3 vertebraNormal = Quaternion.AngleAxis(i * angle, normal) * rotatedNormal; - validVertebrae.Add(new Vertebra(vertebraPosition, vertebraPosition - origin, vertebraNormal)); - } - } else { - // We have a definingAxis that is used to limit the number of possible vertebrae. All vertebrae must be - // perpendicular to the definingAxis which forces the stroke to be created on Plane. We make this true by - // rotating around the definingAxis to generate vertebrae. Note that there can only be the normalVertebra - // and two other vertebrae in the plane so we can just directly calculate them. - validVertebrae = new List(); - Vector3 rotatedSpine = Quaternion.AngleAxis(spineDegrees, definingAxis) * spine; - Vector3 rotatedNormal = (Quaternion.AngleAxis(CURVATURE, definingAxis) * spine).normalized; - validVertebrae.Add(new Vertebra(origin + rotatedSpine, rotatedSpine, rotatedNormal)); - - rotatedSpine = Quaternion.AngleAxis(-spineDegrees, definingAxis) * spine; - rotatedNormal = (Quaternion.AngleAxis(-CURVATURE, definingAxis) * spine).normalized; - validVertebrae.Add(new Vertebra(origin + rotatedSpine, rotatedSpine, rotatedNormal)); - } - - // Set the currently selected vertebra to be null on creation. - currentVertebra = null; - } + if (Math3d.CompareVectors(other.position, position, 0.001f) + && Math3d.CompareVectors(other.normal, normal, 0.001f)) + { + return true; + } - public static float FindSpineLength (float faceHeight) { - return faceHeight * Mathf.Sin((CURVATURE * Mathf.Deg2Rad) / 2); + return false; + } } /// - /// Find the nearest vertebra to a given position. - /// - /// To find the nearest vertebra we compare the angle between vectors instead of position. We start by imagining - /// a vector that goes from the origin to the position. This vector can have an angle from the normal of 0 to 180 - /// degrees. To decide which vertebra we should return we map all 180 possible degrees to a vertebra. For example - /// if the position vector has a degree of separation from the normal of 0 to 15 degrees we'd return vertebra A. - /// if the angle was 16 - 90 vertebra B and 90 - 180 vertebra C. + /// A spine is the geometric structure that a segment of a stroke is formed around. A group of spines connected + /// together defines a stroke. A spine is just a line with a defined length and angle. The spine has an origin + /// point which is the ending of the last spine segment and ends at one of a pre-determined set of "vertebrae". + /// The positions of the valid vertebrae are geometrically designed so that a stroke face can be rotated around + /// the vertebra and an edge on the stroke face will lay perfectly flush with the previous stroke face. /// - /// See bug DIAGRAM 7 (snapping and not snapping). + /// See bug DIAGRAM 0 ANATOMY OF A SPINE. /// - /// The position we are comparing. - /// Whether we want to snap. - /// - /// The selected vertebra of the spine before this spine. This is used to try and favor selecting a vertebra - /// such that this spine will be in line with the last. - /// - /// The upwards direction of the controller. - /// Whether or not we should enforce a checkpoint. - /// The nearest vertebra. - public Vertebra NearestVertebra(Vector3 position, bool isSnapping, bool isManualCheckpointing, - Vertebra lastSpineVertebra, Vector3 controllerUp, out bool shouldForceCheckpoint) { - // We will only force a checkpoint in a few cases so set it to false by default. - shouldForceCheckpoint = false; - - Vector3 requiredChange = position - origin; - - // Find the actual angle from the normal. - float angleFromNormal = Vector3.Angle(normal, requiredChange); - - if (!isSnapping) { - // If the user is about to move back on themselves bend the stroke backwards by creating a nearly flat "cap" - // face where the stroke changes direction. - if (angleFromNormal > 90f - EPSILON) { - // We need to bend the stroke backwards in on itself without accidentally exposing back faces. To do this we - // generate a spine that lays almost parallel to the previous face. - - // Find how long the spine will be. - float length = Mathf.Sqrt(Mathf.Pow((radius), 2) - + Mathf.Pow((0.001f), 2) - - (2 * (radius) * (0.001f) * Mathf.Cos(CAP_BEND * Mathf.Deg2Rad))); - - Vector3 scaledNormal = normal.normalized * length; - Vector3 rotationalAxis = Vector3.Cross(normal, requiredChange); - float angle = 90f - ((180f - CAP_BEND) / 2f); - Vector3 capDirection = Quaternion.AngleAxis(angle, rotationalAxis) * scaledNormal; - - Vector3 capPosition = origin + capDirection; - Vector3 capNormal = Quaternion.AngleAxis(CAP_BEND, rotationalAxis) * normal; - - // We should consider any bend cap generation as automatic and force a checkpoint. - shouldForceCheckpoint = true; - return new Vertebra(capPosition, capDirection, capNormal); + public class Spine + { + /// + /// The weight given to choosing the normalVertebra. A larger weight will create more controlled angular strokes + /// by "sticking" to the normal while a smaller weight will produce more variation. + /// + private const float NORMAL_WEIGHT = 1.0f; + /// + /// An angle threshold in degrees to add weight for selecting the normal vertebra. + /// + private const float NORMAL_THRESHOLD = 65.0f; + /// + /// An angle threshold in degrees to add weight for re-selecting the current vertebra as the nearest vertebra. + /// + private const float CURRENT_VERTEBRA_THRESHOLD = 10f; + /// + /// An angle threshold in degrees to add weight for selecting a new vertebra that shares the same direction vector + /// as the last spine. + /// + private const float LAST_SPINE_THRESHOLD = 5f; + /// + /// The angle we will "bend" the stroke so that we can safely create a cap on the stroke where it changes direction. + /// This is almost 180 because we want to create a new face at an angle that points in the opposite direction but + /// still has some volume to avoid exposing backfaces or creating points in the stroke with zero volume. + /// + private const float CAP_BEND = 170f; + /// + /// The allowed change in rotation from face to face. + /// + public const float CURVATURE = 45f; + /// + /// Handle floating point errors. + /// + public const float EPSILON = 0.01f; + + /// + /// The length of the spine. The length of the spine is the distance between the centers of two faces such that if + /// one of the faces is rotated at some angle theta (CURVATURE) to the other face the faces will meet but + /// not intersect. + /// + public readonly float length; + /// + /// The normal of the face used to generate this spine. + /// + public readonly Vector3 normal; + /// + /// The origin of the spine. A spine segement is a line from the origin to one of the validVertebrae. The origin is + /// equivalent to the center of the face used to define the spine. + /// + public readonly Vector3 origin; + /// + /// The angle in degrees between the normal and all valid vertebrae excluding the normalVertebra. + /// + private readonly float spineDegrees; + /// + /// The vertebra along the spine's normal. + /// + private readonly Vertebra normalVertebra; + /// + /// The other possible vertebrae for the spine excluding the normal. Each vertebra correlates to an edge of the + /// defining face. See bug DIAGRAM 6. + /// + private readonly List validVertebrae; + /// + /// The vertebra that is currently selected for this spine. + /// + private Vertebra currentVertebra; + /// + /// The radius of the face that defines this spine. + /// + private float radius; + private float vertLength; + + /// + /// Creates a spine from the coplanar clockwise vertices defining a face. + /// + /// The vertices of the defining face. + /// + /// An axis that all vertebrae must be perpendicular to. The definingAxis lets us limit stroke creation so that the + /// stroke is locked to a plane. The definingAxis represents the normal of the plane, which guarantees that any + /// vector perpendicular to the definingAxis exists in the plane. + /// + public Spine(List vertices, Vector3 definingAxis) + { + origin = MeshMath.CalculateGeometricCenter(vertices); + normal = MeshMath.CalculateNormal(vertices); + // spineDegrees is the angle between the normal and every non-normal valid vertebra. See bug DIAGRAM 2. + spineDegrees = CURVATURE / 2f; + + // Find the length of the spine. See bug DIAGRAM 1. + // This is the length if we bend the spine so that edges are flush. This is more restrictive than vertLength so we + // use this as the defining length of the spine. + length = FindSpineLength(MeshMath.FindHeightOfARegularPolygonalFace(vertices)); + // Find the radius of the defining face. + radius = MeshMath.FindRadiusOfARegularPolygonalFace(vertices); + vertLength = FindSpineLength(radius * 2.0f); + Vector3 spine = normal * length; + + // We have everything we need to define the normalVertebra. + normalVertebra = new Vertebra(origin + spine, spine, normal); + + // Now we want to find all the other vertebrae. To do this we define two vectors. One which will represent the + // vertebra and one that will represent the normal for that vertebra. We rotate both vectors so that their + // projections onto the defining face would be perpendicular with any edge. The one used to define vertebra is + // rotated spineDegrees and the one used to define the normal is rotated by the allowed curvature. Now that we + // have these vectors we just rotate them around the normal so we can use them to define a vertebra for each + // edge. See bug DIAGRAM 3 and DIAGRAM 4. + if (definingAxis == Vector3.zero) + { + // The faceAxis is used as an axis to rotate around so that the normal is perpendicular with an edge. + Vector3 faceAxis = vertices[1] - vertices[0]; + + Quaternion spineRotation = Quaternion.AngleAxis(spineDegrees, faceAxis); + Vector3 rotatedNormal = (Quaternion.AngleAxis(CURVATURE, faceAxis) * spine).normalized; + + // The angle we will need to rotate the vectors so that we can define a vertebra for each edge. + // Note: The number of vertices == number of edges. + float angle = 360f / (vertices.Count * 2); + validVertebrae = new List(); + + // Rotate around the normal and define a vertebra for each rotation until we complete a full rotation. + // See bug DIAGRAM 5(top), DIAGRAM 5(side), DIAGRAM 6. + for (int i = 0; i * angle < (360f - EPSILON); i++) + { + Vector3 referenceVector = i % 2 == 0 ? spineRotation * spine : spineRotation * (normal * vertLength); + Vector3 vertebraPosition = origin + Quaternion.AngleAxis(i * angle, normal) * referenceVector; + Vector3 vertebraNormal = Quaternion.AngleAxis(i * angle, normal) * rotatedNormal; + validVertebrae.Add(new Vertebra(vertebraPosition, vertebraPosition - origin, vertebraNormal)); + } + } + else + { + // We have a definingAxis that is used to limit the number of possible vertebrae. All vertebrae must be + // perpendicular to the definingAxis which forces the stroke to be created on Plane. We make this true by + // rotating around the definingAxis to generate vertebrae. Note that there can only be the normalVertebra + // and two other vertebrae in the plane so we can just directly calculate them. + validVertebrae = new List(); + Vector3 rotatedSpine = Quaternion.AngleAxis(spineDegrees, definingAxis) * spine; + Vector3 rotatedNormal = (Quaternion.AngleAxis(CURVATURE, definingAxis) * spine).normalized; + validVertebrae.Add(new Vertebra(origin + rotatedSpine, rotatedSpine, rotatedNormal)); + + rotatedSpine = Quaternion.AngleAxis(-spineDegrees, definingAxis) * spine; + rotatedNormal = (Quaternion.AngleAxis(-CURVATURE, definingAxis) * spine).normalized; + validVertebrae.Add(new Vertebra(origin + rotatedSpine, rotatedSpine, rotatedNormal)); + } + + // Set the currently selected vertebra to be null on creation. + currentVertebra = null; } - // Determine the normal of the vertebra based on controllerUp. This allows the user more freedom. - Vector3 currentNormal = - Vector3.Angle(controllerUp, requiredChange) < Vector3.Angle(-controllerUp, requiredChange) ? - controllerUp : -controllerUp; - - // Check for danger. - - // Check to see if the user has moved less than a spine's length. This is a sure fire way to create an invalid - // mesh. - if (Vector3.Distance(origin, position) < length) { - // Check if the user also has the face bent at an angle greater than the allowed curvature. If this happens - // while the user is too close to the previous face the verts could cross over the last face causing the mesh - // to invert and be invalid. - - // Or if the user is greater than spineDegrees away from the normal. They are outside the magic threshold - // where the length and curvature allowance forces valid meshes. If the user is in this zone we have know - // guarantee they are creating a valid mesh. - if (Vector3.Angle(normal, currentNormal) > CURVATURE || angleFromNormal > spineDegrees) { - // Abort. The user is up to no good. Enforce our snapping logic so they can't create an invalid mesh. - return NearestVertebra(position, /*isSnapping*/ true, isManualCheckpointing, lastSpineVertebra, - controllerUp, out shouldForceCheckpoint); - } else { - // They aren't doing anything too outrageous. Lengthening the spine will enforce a valid mesh. - Vector3 scaledChange = length * ((position - origin).normalized); - position = origin + scaledChange; - requiredChange = position - origin; - } + public static float FindSpineLength(float faceHeight) + { + return faceHeight * Mathf.Sin((CURVATURE * Mathf.Deg2Rad) / 2); } - return new Vertebra(position, position - origin, currentNormal); - } else { - // Reduce the normal threshold while manualCheckpointing. The further a user moves away from the last spine the - // large the motion they need to make to cross the angular threshold so we want to reduce the threshold - // reducing this affect. - float normalThreshold = isManualCheckpointing ? NORMAL_THRESHOLD / 2.0f : NORMAL_THRESHOLD; - - // The position has an angle from the normal that falls within the range of the degrees mapped to the - // normalVertebra. - if (angleFromNormal < normalThreshold) { - // If we are manually checkpointing we want to return a spine that fills the entire distance to the - // controller. - if (isManualCheckpointing) { - // We know we want to choose the normalVertebra but a longer version of it that maps to the current - // controller position. We can just project the controller position onto the normalVertebra to the nearest - // increment of length. - Vector3 projectedControllerPosition = - GridUtils.ProjectPointOntoLine(position, normalVertebra.direction, origin, length); - - // Don't let the user create a segment that is smaller than a spine. Also stops them from going "back" on - // themselves. - if (Vector3.Distance(projectedControllerPosition, origin) < length || angleFromNormal > 89f) { - return normalVertebra; + /// + /// Find the nearest vertebra to a given position. + /// + /// To find the nearest vertebra we compare the angle between vectors instead of position. We start by imagining + /// a vector that goes from the origin to the position. This vector can have an angle from the normal of 0 to 180 + /// degrees. To decide which vertebra we should return we map all 180 possible degrees to a vertebra. For example + /// if the position vector has a degree of separation from the normal of 0 to 15 degrees we'd return vertebra A. + /// if the angle was 16 - 90 vertebra B and 90 - 180 vertebra C. + /// + /// See bug DIAGRAM 7 (snapping and not snapping). + /// + /// The position we are comparing. + /// Whether we want to snap. + /// + /// The selected vertebra of the spine before this spine. This is used to try and favor selecting a vertebra + /// such that this spine will be in line with the last. + /// + /// The upwards direction of the controller. + /// Whether or not we should enforce a checkpoint. + /// The nearest vertebra. + public Vertebra NearestVertebra(Vector3 position, bool isSnapping, bool isManualCheckpointing, + Vertebra lastSpineVertebra, Vector3 controllerUp, out bool shouldForceCheckpoint) + { + // We will only force a checkpoint in a few cases so set it to false by default. + shouldForceCheckpoint = false; + + Vector3 requiredChange = position - origin; + + // Find the actual angle from the normal. + float angleFromNormal = Vector3.Angle(normal, requiredChange); + + if (!isSnapping) + { + // If the user is about to move back on themselves bend the stroke backwards by creating a nearly flat "cap" + // face where the stroke changes direction. + if (angleFromNormal > 90f - EPSILON) + { + // We need to bend the stroke backwards in on itself without accidentally exposing back faces. To do this we + // generate a spine that lays almost parallel to the previous face. + + // Find how long the spine will be. + float length = Mathf.Sqrt(Mathf.Pow((radius), 2) + + Mathf.Pow((0.001f), 2) + - (2 * (radius) * (0.001f) * Mathf.Cos(CAP_BEND * Mathf.Deg2Rad))); + + Vector3 scaledNormal = normal.normalized * length; + Vector3 rotationalAxis = Vector3.Cross(normal, requiredChange); + float angle = 90f - ((180f - CAP_BEND) / 2f); + Vector3 capDirection = Quaternion.AngleAxis(angle, rotationalAxis) * scaledNormal; + + Vector3 capPosition = origin + capDirection; + Vector3 capNormal = Quaternion.AngleAxis(CAP_BEND, rotationalAxis) * normal; + + // We should consider any bend cap generation as automatic and force a checkpoint. + shouldForceCheckpoint = true; + return new Vertebra(capPosition, capDirection, capNormal); + } + + // Determine the normal of the vertebra based on controllerUp. This allows the user more freedom. + Vector3 currentNormal = + Vector3.Angle(controllerUp, requiredChange) < Vector3.Angle(-controllerUp, requiredChange) ? + controllerUp : -controllerUp; + + // Check for danger. + + // Check to see if the user has moved less than a spine's length. This is a sure fire way to create an invalid + // mesh. + if (Vector3.Distance(origin, position) < length) + { + // Check if the user also has the face bent at an angle greater than the allowed curvature. If this happens + // while the user is too close to the previous face the verts could cross over the last face causing the mesh + // to invert and be invalid. + + // Or if the user is greater than spineDegrees away from the normal. They are outside the magic threshold + // where the length and curvature allowance forces valid meshes. If the user is in this zone we have know + // guarantee they are creating a valid mesh. + if (Vector3.Angle(normal, currentNormal) > CURVATURE || angleFromNormal > spineDegrees) + { + // Abort. The user is up to no good. Enforce our snapping logic so they can't create an invalid mesh. + return NearestVertebra(position, /*isSnapping*/ true, isManualCheckpointing, lastSpineVertebra, + controllerUp, out shouldForceCheckpoint); + } + else + { + // They aren't doing anything too outrageous. Lengthening the spine will enforce a valid mesh. + Vector3 scaledChange = length * ((position - origin).normalized); + position = origin + scaledChange; + requiredChange = position - origin; + } + } + + return new Vertebra(position, position - origin, currentNormal); + } + else + { + // Reduce the normal threshold while manualCheckpointing. The further a user moves away from the last spine the + // large the motion they need to make to cross the angular threshold so we want to reduce the threshold + // reducing this affect. + float normalThreshold = isManualCheckpointing ? NORMAL_THRESHOLD / 2.0f : NORMAL_THRESHOLD; + + // The position has an angle from the normal that falls within the range of the degrees mapped to the + // normalVertebra. + if (angleFromNormal < normalThreshold) + { + // If we are manually checkpointing we want to return a spine that fills the entire distance to the + // controller. + if (isManualCheckpointing) + { + // We know we want to choose the normalVertebra but a longer version of it that maps to the current + // controller position. We can just project the controller position onto the normalVertebra to the nearest + // increment of length. + Vector3 projectedControllerPosition = + GridUtils.ProjectPointOntoLine(position, normalVertebra.direction, origin, length); + + // Don't let the user create a segment that is smaller than a spine. Also stops them from going "back" on + // themselves. + if (Vector3.Distance(projectedControllerPosition, origin) < length || angleFromNormal > 89f) + { + return normalVertebra; + } + return new Vertebra(projectedControllerPosition, projectedControllerPosition - origin, + normalVertebra.normal); + } + else + { + return normalVertebra; + } + } + + // We are outside the threshold of the normalVertebra. We need to figure out which of the validVertebrae + // we are closest to. + float nearestAngle = Mathf.Infinity; + Vertebra nearestVertebra = null; + + foreach (Vertebra vertebra in validVertebrae) + { + // Find the actual angle from the vertebra. + float angle = Vector3.Angle(requiredChange, vertebra.position - origin); + + if (angle < nearestAngle) + { + nearestAngle = angle; + nearestVertebra = vertebra; + } + } + + if (isManualCheckpointing) + { + // We know we want to choose the nearestVertebra but a longer version of it that maps to the current + // controller position. We can just project the controller position onto the nearestVertebra to the nearest + // increment of length. + Vector3 projectedControllerPosition = + GridUtils.ProjectPointOntoLine(position, nearestVertebra.direction, origin, length); + + // Don't let the user create a segement that is smaller than a spine. Also stops them from going "back" on + // themselves. + if (Vector3.Distance(projectedControllerPosition, origin) < length || angleFromNormal > 89f) + { + return nearestVertebra; + } + return new Vertebra(projectedControllerPosition, projectedControllerPosition - origin, + nearestVertebra.normal); + } + else + { + return nearestVertebra; + } } - return new Vertebra(projectedControllerPosition, projectedControllerPosition - origin, - normalVertebra.normal); - } else { - return normalVertebra; - } } - // We are outside the threshold of the normalVertebra. We need to figure out which of the validVertebrae - // we are closest to. - float nearestAngle = Mathf.Infinity; - Vertebra nearestVertebra = null; - - foreach (Vertebra vertebra in validVertebrae) { - // Find the actual angle from the vertebra. - float angle = Vector3.Angle(requiredChange, vertebra.position - origin); - - if (angle < nearestAngle) { - nearestAngle = angle; - nearestVertebra = vertebra; - } + /// + /// Sets a given vertebra as the currently selected vertebra for this spine. + /// + /// The vertebra being selected. + public void SelectVertebra(Vertebra vertebra) + { + currentVertebra = vertebra; } - if (isManualCheckpointing) { - // We know we want to choose the nearestVertebra but a longer version of it that maps to the current - // controller position. We can just project the controller position onto the nearestVertebra to the nearest - // increment of length. - Vector3 projectedControllerPosition = - GridUtils.ProjectPointOntoLine(position, nearestVertebra.direction, origin, length); - - // Don't let the user create a segement that is smaller than a spine. Also stops them from going "back" on - // themselves. - if (Vector3.Distance(projectedControllerPosition, origin) < length || angleFromNormal > 89f) { - return nearestVertebra; - } - return new Vertebra(projectedControllerPosition, projectedControllerPosition - origin, - nearestVertebra.normal); - } else { - return nearestVertebra; + /// + /// Returns the currently selected vertebra for this spine. + /// + /// The currently selected vertebra. + public Vertebra CurrentVertebra() + { + return currentVertebra; } - } - } - - /// - /// Sets a given vertebra as the currently selected vertebra for this spine. - /// - /// The vertebra being selected. - public void SelectVertebra(Vertebra vertebra) { - currentVertebra = vertebra; - } - - /// - /// Returns the currently selected vertebra for this spine. - /// - /// The currently selected vertebra. - public Vertebra CurrentVertebra() { - return currentVertebra; } - } } diff --git a/Assets/Scripts/model/core/Vertex.cs b/Assets/Scripts/model/core/Vertex.cs index 205f7ab7..306f7f8c 100644 --- a/Assets/Scripts/model/core/Vertex.cs +++ b/Assets/Scripts/model/core/Vertex.cs @@ -14,23 +14,26 @@ using UnityEngine; -namespace com.google.apps.peltzer.client.model.core { +namespace com.google.apps.peltzer.client.model.core +{ - /// - /// A shared vertex. Represents a location in space that can be - /// shared by multiple faces in a single MMesh. - /// - public class Vertex { - private readonly int _id; - private Vector3 _loc; + /// + /// A shared vertex. Represents a location in space that can be + /// shared by multiple faces in a single MMesh. + /// + public class Vertex + { + private readonly int _id; + private Vector3 _loc; - // Read-only getters. - public int id { get { return _id; } } - public Vector3 loc { get { return _loc; } } + // Read-only getters. + public int id { get { return _id; } } + public Vector3 loc { get { return _loc; } } - public Vertex(int id, Vector3 loc) { - _id = id; - _loc = loc; + public Vertex(int id, Vector3 loc) + { + _id = id; + _loc = loc; + } } - } } diff --git a/Assets/Scripts/model/core/VertexKey.cs b/Assets/Scripts/model/core/VertexKey.cs index e4af127c..e179e8f9 100644 --- a/Assets/Scripts/model/core/VertexKey.cs +++ b/Assets/Scripts/model/core/VertexKey.cs @@ -12,37 +12,43 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace com.google.apps.peltzer.client.model.core { - /// - /// A canonical id for a vertex, which includes the id of the mesh it belongs to. - /// - public class VertexKey { - private readonly int _meshId; - private readonly int _vertexId; - private readonly int _hashCode; +namespace com.google.apps.peltzer.client.model.core +{ + /// + /// A canonical id for a vertex, which includes the id of the mesh it belongs to. + /// + public class VertexKey + { + private readonly int _meshId; + private readonly int _vertexId; + private readonly int _hashCode; - public VertexKey(int meshId, int vertexId) { - _meshId = meshId; - _vertexId = vertexId; - // 31 is a good number: http://stackoverflow.com/questions/299304/why-does-javas-hashcode-in-string-use-31-as-a-multiplier - _hashCode = (151 + meshId) * 31 + vertexId; - } + public VertexKey(int meshId, int vertexId) + { + _meshId = meshId; + _vertexId = vertexId; + // 31 is a good number: http://stackoverflow.com/questions/299304/why-does-javas-hashcode-in-string-use-31-as-a-multiplier + _hashCode = (151 + meshId) * 31 + vertexId; + } - public override bool Equals(object obj) { - return Equals(obj as VertexKey); - } + public override bool Equals(object obj) + { + return Equals(obj as VertexKey); + } - public bool Equals(VertexKey otherKey) { - return otherKey != null - && _vertexId == otherKey._vertexId - && _meshId == otherKey._meshId; - } + public bool Equals(VertexKey otherKey) + { + return otherKey != null + && _vertexId == otherKey._vertexId + && _meshId == otherKey._meshId; + } - public override int GetHashCode() { - return _hashCode; - } + public override int GetHashCode() + { + return _hashCode; + } - public int meshId { get { return _meshId; } } - public int vertexId { get { return _vertexId; } } - } + public int meshId { get { return _meshId; } } + public int vertexId { get { return _vertexId; } } + } } diff --git a/Assets/Scripts/model/csg/CsgContext.cs b/Assets/Scripts/model/csg/CsgContext.cs index bfda4dc6..af1d4267 100644 --- a/Assets/Scripts/model/csg/CsgContext.cs +++ b/Assets/Scripts/model/csg/CsgContext.cs @@ -20,38 +20,46 @@ using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.model.csg { - /// - /// Context for CSG operations. Unifies vertices. - /// - public class CsgContext { - // We allow new points to be added if they are at least 3 epsilons away from existing points. - private readonly int WIGGLE_ROOM = 3; - private CollisionSystem tree; +namespace com.google.apps.peltzer.client.model.csg +{ + /// + /// Context for CSG operations. Unifies vertices. + /// + public class CsgContext + { + // We allow new points to be added if they are at least 3 epsilons away from existing points. + private readonly int WIGGLE_ROOM = 3; + private CollisionSystem tree; - public CsgContext(Bounds bounds) { - tree = new NativeSpatial(); - } + public CsgContext(Bounds bounds) + { + tree = new NativeSpatial(); + } - public CsgVertex CreateOrGetVertexAt(Vector3 loc) { - Bounds bb = new Bounds(loc, Vector3.one * CsgMath.EPSILON * WIGGLE_ROOM); - CsgVertex closest = null; - HashSet vertices; - if (tree.IntersectedBy(bb, out vertices)) { - float closestDist = 1000; - foreach (CsgVertex potential in vertices) { - float d = Vector3.Distance(loc, potential.loc); - if (d < CsgMath.EPSILON && d < closestDist) { - closest = potential; - closestDist = d; - } + public CsgVertex CreateOrGetVertexAt(Vector3 loc) + { + Bounds bb = new Bounds(loc, Vector3.one * CsgMath.EPSILON * WIGGLE_ROOM); + CsgVertex closest = null; + HashSet vertices; + if (tree.IntersectedBy(bb, out vertices)) + { + float closestDist = 1000; + foreach (CsgVertex potential in vertices) + { + float d = Vector3.Distance(loc, potential.loc); + if (d < CsgMath.EPSILON && d < closestDist) + { + closest = potential; + closestDist = d; + } + } + } + if (closest == null) + { + closest = new CsgVertex(loc); + tree.Add(closest, new Bounds(loc, Vector3.zero)); + } + return closest; } - } - if (closest == null) { - closest = new CsgVertex(loc); - tree.Add(closest, new Bounds(loc, Vector3.zero)); - } - return closest; } - } } diff --git a/Assets/Scripts/model/csg/CsgMath.cs b/Assets/Scripts/model/csg/CsgMath.cs index 54df126e..2ed21eaa 100644 --- a/Assets/Scripts/model/csg/CsgMath.cs +++ b/Assets/Scripts/model/csg/CsgMath.cs @@ -19,61 +19,74 @@ using UnityEngine; using com.google.apps.peltzer.client.model.core; -namespace com.google.apps.peltzer.client.model.csg { - public class CsgMath { - // Floating point slop for testing things like coplanar, etc. - public const float EPSILON = 0.0001f; +namespace com.google.apps.peltzer.client.model.csg +{ + public class CsgMath + { + // Floating point slop for testing things like coplanar, etc. + public const float EPSILON = 0.0001f; - // Given a Unity Plane, find a point on that plane. - // public for testing. - public static Vector3 PointOnPlane(Plane plane) { - return -(plane.normal * plane.distance); - } + // Given a Unity Plane, find a point on that plane. + // public for testing. + public static Vector3 PointOnPlane(Plane plane) + { + return -(plane.normal * plane.distance); + } - // Find the intersection of a ray with a plane. (Should return a value even if the plane is 'behind' the ray.) - public static void RayPlaneIntersection( - out Vector3 intersection, Vector3 rayStart, Vector3 rayNormal, Plane plane) { - Ray ray = new Ray(rayStart, rayNormal); - float d; - plane.Raycast(ray, out d); - if (Math.Abs(d) > EPSILON) { - intersection = ray.GetPoint(d); - } else { - intersection = rayStart; - } - } + // Find the intersection of a ray with a plane. (Should return a value even if the plane is 'behind' the ray.) + public static void RayPlaneIntersection( + out Vector3 intersection, Vector3 rayStart, Vector3 rayNormal, Plane plane) + { + Ray ray = new Ray(rayStart, rayNormal); + float d; + plane.Raycast(ray, out d); + if (Math.Abs(d) > EPSILON) + { + intersection = ray.GetPoint(d); + } + else + { + intersection = rayStart; + } + } - // Is the given point inside the given polygon. Assumes all points are coplanar. - // Returns 1 if inside, -1 if outside and 0 if on boundary. - // public for testing. - public static int IsInside(CsgPolygon poly, Vector3 point) { - bool onEdge = false; + // Is the given point inside the given polygon. Assumes all points are coplanar. + // Returns 1 if inside, -1 if outside and 0 if on boundary. + // public for testing. + public static int IsInside(CsgPolygon poly, Vector3 point) + { + bool onEdge = false; - for (int i = 0; i < poly.vertices.Count; i++) { - Vector3 a = poly.vertices[i].loc; - Vector3 b = poly.vertices[(i + 1) % poly.vertices.Count].loc; - Vector3 c = poly.vertices[(i + 2) % poly.vertices.Count].loc; - int sameSide = SameSide(a, b, point, c); - if (sameSide < 0) { - return -1; + for (int i = 0; i < poly.vertices.Count; i++) + { + Vector3 a = poly.vertices[i].loc; + Vector3 b = poly.vertices[(i + 1) % poly.vertices.Count].loc; + Vector3 c = poly.vertices[(i + 2) % poly.vertices.Count].loc; + int sameSide = SameSide(a, b, point, c); + if (sameSide < 0) + { + return -1; + } + if (sameSide == 0) + { + onEdge = true; + } + } + return onEdge ? 0 : 1; } - if (sameSide == 0) { - onEdge = true; - } - } - return onEdge ? 0 : 1; - } - // Returns 1 if inside, -1 if outside and 0 if on boundary. - private static int SameSide(Vector3 a, Vector3 b, Vector3 check, Vector3 reference) { - Vector3 checkSide = MeshMath.CalculateNormal(a, b, check); - Vector3 referenceSide = MeshMath.CalculateNormal(a, b, reference); - if (checkSide.magnitude < EPSILON) { - return 0; - } - // Empirically, I've found that == is too lenient and distance is too restrictive. - // squared distance is not only more efficient, but it also produces better results. - return (referenceSide - checkSide).sqrMagnitude < EPSILON ? 1 : -1; + // Returns 1 if inside, -1 if outside and 0 if on boundary. + private static int SameSide(Vector3 a, Vector3 b, Vector3 check, Vector3 reference) + { + Vector3 checkSide = MeshMath.CalculateNormal(a, b, check); + Vector3 referenceSide = MeshMath.CalculateNormal(a, b, reference); + if (checkSide.magnitude < EPSILON) + { + return 0; + } + // Empirically, I've found that == is too lenient and distance is too restrictive. + // squared distance is not only more efficient, but it also produces better results. + return (referenceSide - checkSide).sqrMagnitude < EPSILON ? 1 : -1; + } } - } } diff --git a/Assets/Scripts/model/csg/CsgObject.cs b/Assets/Scripts/model/csg/CsgObject.cs index 8b3207a4..022c8b87 100644 --- a/Assets/Scripts/model/csg/CsgObject.cs +++ b/Assets/Scripts/model/csg/CsgObject.cs @@ -17,23 +17,27 @@ using System.Linq; using UnityEngine; -namespace com.google.apps.peltzer.client.model.csg { - /// - /// A collection of CsgPolygons that close a space. - /// - public class CsgObject { - public List polygons { get; private set; } - public List vertices { get; private set; } - public Bounds bounds { get; private set; } +namespace com.google.apps.peltzer.client.model.csg +{ + /// + /// A collection of CsgPolygons that close a space. + /// + public class CsgObject + { + public List polygons { get; private set; } + public List vertices { get; private set; } + public Bounds bounds { get; private set; } - public CsgObject(List polygons, List vertices) { - this.polygons = polygons; - this.vertices = vertices; - Bounds bounds = new Bounds(vertices[0].loc, Vector3.zero); - for(int i = 1; i < vertices.Count; i++) { - bounds.Encapsulate(vertices[i].loc); - } - this.bounds = bounds; + public CsgObject(List polygons, List vertices) + { + this.polygons = polygons; + this.vertices = vertices; + Bounds bounds = new Bounds(vertices[0].loc, Vector3.zero); + for (int i = 1; i < vertices.Count; i++) + { + bounds.Encapsulate(vertices[i].loc); + } + this.bounds = bounds; + } } - } } diff --git a/Assets/Scripts/model/csg/CsgOperations.cs b/Assets/Scripts/model/csg/CsgOperations.cs index 91220e30..36f797a2 100644 --- a/Assets/Scripts/model/csg/CsgOperations.cs +++ b/Assets/Scripts/model/csg/CsgOperations.cs @@ -21,398 +21,491 @@ using com.google.apps.peltzer.client.model.util; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.model.csg { - - public class CsgOperations { - private const float COPLANAR_EPS = 0.001f; - - /// - /// Subtract a mesh from all intersecting meshes in a model. - /// - /// true if the subtract brush intersects with meshes in the scene. - public static bool SubtractMeshFromModel(Model model, SpatialIndex spatialIndex, MMesh toSubtract) { - Bounds bounds = toSubtract.bounds; - - List commands = new List(); - HashSet intersectingMeshIds; - if (spatialIndex.FindIntersectingMeshes(toSubtract.bounds, out intersectingMeshIds)) { - foreach (int meshId in intersectingMeshIds) { - MMesh mesh = model.GetMesh(meshId); - MMesh result = Subtract(mesh, toSubtract); - commands.Add(new DeleteMeshCommand(mesh.id)); - // If the result is null, it means the mesh was entirely erased. No need to add a new version back. - if (result != null) { - if (model.CanAddMesh(result)) { - commands.Add(new AddMeshCommand(result)); - } else { - // Abort everything if an invalid mesh would be generated. - return false; +namespace com.google.apps.peltzer.client.model.csg +{ + + public class CsgOperations + { + private const float COPLANAR_EPS = 0.001f; + + /// + /// Subtract a mesh from all intersecting meshes in a model. + /// + /// true if the subtract brush intersects with meshes in the scene. + public static bool SubtractMeshFromModel(Model model, SpatialIndex spatialIndex, MMesh toSubtract) + { + Bounds bounds = toSubtract.bounds; + + List commands = new List(); + HashSet intersectingMeshIds; + if (spatialIndex.FindIntersectingMeshes(toSubtract.bounds, out intersectingMeshIds)) + { + foreach (int meshId in intersectingMeshIds) + { + MMesh mesh = model.GetMesh(meshId); + MMesh result = Subtract(mesh, toSubtract); + commands.Add(new DeleteMeshCommand(mesh.id)); + // If the result is null, it means the mesh was entirely erased. No need to add a new version back. + if (result != null) + { + if (model.CanAddMesh(result)) + { + commands.Add(new AddMeshCommand(result)); + } + else + { + // Abort everything if an invalid mesh would be generated. + return false; + } + } + } + } + if (commands.Count > 0) + { + model.ApplyCommand(new CompositeCommand(commands)); + return true; } - } + return false; } - } - if (commands.Count > 0) { - model.ApplyCommand(new CompositeCommand(commands)); - return true; - } - return false; - } - /// - /// Subtract a mesh from another. Returns a new MMesh that is the result of the subtraction. - /// If the result is an empty space, returns null. - /// - public static MMesh Subtract(MMesh subtrahend, MMesh minuend) { - // If the objects don't overlap, just bail out: - - if (!subtrahend.bounds.Intersects(minuend.bounds)) { - return subtrahend.Clone(); - } - - // Our epsilons aren't very good for operations that are either very small or very big, - // so translate and scale the two csg shapes so they're centered around the origin - // and reasonably sized. This prevents a lot of floating point error in the ensuing maths. - // - // Here's a good article for comparing floating point numbers: - // https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/ - Vector3 operationalCenter = (subtrahend.bounds.center + minuend.bounds.center) / 2.0f; - float averageRadius = (subtrahend.bounds.extents.magnitude + minuend.bounds.extents.magnitude) / 2.0f; - Vector3 operationOffset = -operationalCenter; - float operationScale = 1.0f / averageRadius; - if (operationScale < 1.0f) { - operationScale = 1.0f; - } - - Bounds operationBounds = new Bounds(); - foreach (int vertexId in subtrahend.GetVertexIds()) { - operationBounds.Encapsulate((subtrahend.VertexPositionInModelCoords(vertexId) + operationOffset) * operationScale); - } - foreach (int vertexId in minuend.GetVertexIds()) { - operationBounds.Encapsulate((minuend.VertexPositionInModelCoords(vertexId) + operationOffset) * operationScale); - } - operationBounds.Expand(0.01f); - - CsgContext ctx = new CsgContext(operationBounds); - - CsgObject leftObj = ToCsg(ctx, subtrahend, operationOffset, operationScale); - CsgObject rightObj = ToCsg(ctx, minuend, operationOffset, operationScale); - List result = CsgSubtract(ctx, leftObj, rightObj); - if (result.Count > 0) { - HashSet combinedRemixIds = null; - if (subtrahend.remixIds != null || minuend.remixIds != null) { - combinedRemixIds = new HashSet(); - if (subtrahend.remixIds != null) combinedRemixIds.UnionWith(subtrahend.remixIds); - if (minuend.remixIds != null) combinedRemixIds.UnionWith(minuend.remixIds); - } - return FromPolys( - subtrahend.id, - subtrahend.offset, - subtrahend.rotation, - result, - operationOffset, - operationScale, - combinedRemixIds); - } else { - return null; - } - } + /// + /// Subtract a mesh from another. Returns a new MMesh that is the result of the subtraction. + /// If the result is an empty space, returns null. + /// + public static MMesh Subtract(MMesh subtrahend, MMesh minuend) + { + // If the objects don't overlap, just bail out: + + if (!subtrahend.bounds.Intersects(minuend.bounds)) + { + return subtrahend.Clone(); + } - /// - /// Perform the subtract on CsgObjects. The implementation follows the paper: - /// http://vis.cs.brown.edu/results/videos/bib/pdf/Laidlaw-1986-CSG.pdf - /// - private static List CsgSubtract(CsgContext ctx, CsgObject leftObj, CsgObject rightObj) { - SplitObject(ctx, leftObj, rightObj); - SplitObject(ctx, rightObj, leftObj); - SplitObject(ctx, leftObj, rightObj); - ClassifyPolygons(leftObj, rightObj); - ClassifyPolygons(rightObj, leftObj); - - FaceProperties facePropertiesForNewFaces = leftObj.polygons[0].faceProperties; - List polys = SelectPolygons(leftObj, false, null, PolygonStatus.OUTSIDE, PolygonStatus.OPPOSITE); - polys.AddRange(SelectPolygons(rightObj, true, facePropertiesForNewFaces, PolygonStatus.INSIDE)); - - return polys; - } + // Our epsilons aren't very good for operations that are either very small or very big, + // so translate and scale the two csg shapes so they're centered around the origin + // and reasonably sized. This prevents a lot of floating point error in the ensuing maths. + // + // Here's a good article for comparing floating point numbers: + // https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/ + Vector3 operationalCenter = (subtrahend.bounds.center + minuend.bounds.center) / 2.0f; + float averageRadius = (subtrahend.bounds.extents.magnitude + minuend.bounds.extents.magnitude) / 2.0f; + Vector3 operationOffset = -operationalCenter; + float operationScale = 1.0f / averageRadius; + if (operationScale < 1.0f) + { + operationScale = 1.0f; + } - /// - /// Select all of the polygons in the object with any of the given statuses. - /// - private static List SelectPolygons(CsgObject obj, bool invert, FaceProperties? overwriteFaceProperties, params PolygonStatus[] status) { - HashSet selectedStatus = new HashSet(status); - List polys = new List(); - - foreach(CsgPolygon poly in obj.polygons) { - if (selectedStatus.Contains(poly.status)) { - CsgPolygon polyToAdd = poly; - if (invert) { - polyToAdd = poly.Invert(); - } - if (overwriteFaceProperties.HasValue) { - polyToAdd.faceProperties = overwriteFaceProperties.Value; - } - polys.Add(polyToAdd); + Bounds operationBounds = new Bounds(); + foreach (int vertexId in subtrahend.GetVertexIds()) + { + operationBounds.Encapsulate((subtrahend.VertexPositionInModelCoords(vertexId) + operationOffset) * operationScale); + } + foreach (int vertexId in minuend.GetVertexIds()) + { + operationBounds.Encapsulate((minuend.VertexPositionInModelCoords(vertexId) + operationOffset) * operationScale); + } + operationBounds.Expand(0.01f); + + CsgContext ctx = new CsgContext(operationBounds); + + CsgObject leftObj = ToCsg(ctx, subtrahend, operationOffset, operationScale); + CsgObject rightObj = ToCsg(ctx, minuend, operationOffset, operationScale); + List result = CsgSubtract(ctx, leftObj, rightObj); + if (result.Count > 0) + { + HashSet combinedRemixIds = null; + if (subtrahend.remixIds != null || minuend.remixIds != null) + { + combinedRemixIds = new HashSet(); + if (subtrahend.remixIds != null) combinedRemixIds.UnionWith(subtrahend.remixIds); + if (minuend.remixIds != null) combinedRemixIds.UnionWith(minuend.remixIds); + } + return FromPolys( + subtrahend.id, + subtrahend.offset, + subtrahend.rotation, + result, + operationOffset, + operationScale, + combinedRemixIds); + } + else + { + return null; + } } - } - - return polys; - } - // Section 7: Classify all polygons in the object. - private static void ClassifyPolygons(CsgObject obj, CsgObject wrt) { - // Set up adjacency information. - foreach(CsgPolygon poly in obj.polygons) { - for(int i = 0; i < poly.vertices.Count; i++) { - int j = (i + 1) % poly.vertices.Count; - poly.vertices[i].neighbors.Add(poly.vertices[j]); - poly.vertices[j].neighbors.Add(poly.vertices[i]); + /// + /// Perform the subtract on CsgObjects. The implementation follows the paper: + /// http://vis.cs.brown.edu/results/videos/bib/pdf/Laidlaw-1986-CSG.pdf + /// + private static List CsgSubtract(CsgContext ctx, CsgObject leftObj, CsgObject rightObj) + { + SplitObject(ctx, leftObj, rightObj); + SplitObject(ctx, rightObj, leftObj); + SplitObject(ctx, leftObj, rightObj); + ClassifyPolygons(leftObj, rightObj); + ClassifyPolygons(rightObj, leftObj); + + FaceProperties facePropertiesForNewFaces = leftObj.polygons[0].faceProperties; + List polys = SelectPolygons(leftObj, false, null, PolygonStatus.OUTSIDE, PolygonStatus.OPPOSITE); + polys.AddRange(SelectPolygons(rightObj, true, facePropertiesForNewFaces, PolygonStatus.INSIDE)); + + return polys; } - } - - // Classify polys. - foreach(CsgPolygon poly in obj.polygons) { - if (HasUnknown(poly) || AllBoundary(poly)) { - ClassifyPolygonUsingRaycast(poly, wrt); - if (poly.status == PolygonStatus.INSIDE || poly.status == PolygonStatus.OUTSIDE) { - VertexStatus newStatus = poly.status == PolygonStatus.INSIDE ? VertexStatus.INSIDE : VertexStatus.OUTSIDE; - foreach (CsgVertex vertex in poly.vertices) { - PropagateVertexStatus(vertex, newStatus); + + /// + /// Select all of the polygons in the object with any of the given statuses. + /// + private static List SelectPolygons(CsgObject obj, bool invert, FaceProperties? overwriteFaceProperties, params PolygonStatus[] status) + { + HashSet selectedStatus = new HashSet(status); + List polys = new List(); + + foreach (CsgPolygon poly in obj.polygons) + { + if (selectedStatus.Contains(poly.status)) + { + CsgPolygon polyToAdd = poly; + if (invert) + { + polyToAdd = poly.Invert(); + } + if (overwriteFaceProperties.HasValue) + { + polyToAdd.faceProperties = overwriteFaceProperties.Value; + } + polys.Add(polyToAdd); + } } - } - } else { - // Use the status of the first vertex that is inside or outside. - foreach(CsgVertex vertex in poly.vertices) { - if (vertex.status == VertexStatus.INSIDE) { - poly.status = PolygonStatus.INSIDE; - break; + + return polys; + } + + // Section 7: Classify all polygons in the object. + private static void ClassifyPolygons(CsgObject obj, CsgObject wrt) + { + // Set up adjacency information. + foreach (CsgPolygon poly in obj.polygons) + { + for (int i = 0; i < poly.vertices.Count; i++) + { + int j = (i + 1) % poly.vertices.Count; + poly.vertices[i].neighbors.Add(poly.vertices[j]); + poly.vertices[j].neighbors.Add(poly.vertices[i]); + } } - if (vertex.status == VertexStatus.OUTSIDE) { - poly.status = PolygonStatus.OUTSIDE; - break; + + // Classify polys. + foreach (CsgPolygon poly in obj.polygons) + { + if (HasUnknown(poly) || AllBoundary(poly)) + { + ClassifyPolygonUsingRaycast(poly, wrt); + if (poly.status == PolygonStatus.INSIDE || poly.status == PolygonStatus.OUTSIDE) + { + VertexStatus newStatus = poly.status == PolygonStatus.INSIDE ? VertexStatus.INSIDE : VertexStatus.OUTSIDE; + foreach (CsgVertex vertex in poly.vertices) + { + PropagateVertexStatus(vertex, newStatus); + } + } + } + else + { + // Use the status of the first vertex that is inside or outside. + foreach (CsgVertex vertex in poly.vertices) + { + if (vertex.status == VertexStatus.INSIDE) + { + poly.status = PolygonStatus.INSIDE; + break; + } + if (vertex.status == VertexStatus.OUTSIDE) + { + poly.status = PolygonStatus.OUTSIDE; + break; + } + } + AssertOrThrow.True(poly.status != PolygonStatus.UNKNOWN, "Should have classified polygon."); + } } - } - AssertOrThrow.True(poly.status != PolygonStatus.UNKNOWN, "Should have classified polygon."); } - } - } - // Fig 8.1: Propagate vertex status. - private static void PropagateVertexStatus(CsgVertex vertex, VertexStatus newStatus) { - if (vertex.status == VertexStatus.UNKNOWN) { - vertex.status = newStatus; - foreach(CsgVertex neighbor in vertex.neighbors) { - PropagateVertexStatus(neighbor, newStatus); + // Fig 8.1: Propagate vertex status. + private static void PropagateVertexStatus(CsgVertex vertex, VertexStatus newStatus) + { + if (vertex.status == VertexStatus.UNKNOWN) + { + vertex.status = newStatus; + foreach (CsgVertex neighbor in vertex.neighbors) + { + PropagateVertexStatus(neighbor, newStatus); + } + } } - } - } - // Fig 7.2: Classify a given polygon by raycasting from its barycenter into the faces of the other object. - // Public for testing. - public static void ClassifyPolygonUsingRaycast(CsgPolygon poly, CsgObject wrt) { - Vector3 rayStart = poly.baryCenter; - Vector3 rayNormal = poly.plane.normal; - CsgPolygon closest = null; - float closestPolyDist = float.MaxValue; - - bool done; - int count = 0; - do { - done = true; // Done unless we hit a special case. - foreach(CsgPolygon otherPoly in wrt.polygons) { - float dot = Vector3.Dot(rayNormal, otherPoly.plane.normal); - bool perp = Mathf.Abs(dot) < CsgMath.EPSILON; - bool onOtherPlane = Mathf.Abs(otherPoly.plane.GetDistanceToPoint(rayStart)) < CsgMath.EPSILON; - Vector3 projectedToOtherPlane = Vector3.zero; - float signedDist = -1f; - if (!perp) { - CsgMath.RayPlaneIntersection(out projectedToOtherPlane, rayStart, rayNormal, otherPoly.plane); - float dist = Vector3.Distance(projectedToOtherPlane, rayStart); - signedDist = dist * Mathf.Sign(Vector3.Dot(rayNormal, (projectedToOtherPlane - rayStart))); - } - - if (perp && onOtherPlane) { - done = false; - break; - } else if(perp && !onOtherPlane) { - // no intersection - } else if(!perp && onOtherPlane) { - int isInside = CsgMath.IsInside(otherPoly, projectedToOtherPlane); - if (isInside >= 0) { - closestPolyDist = 0; - closest = otherPoly; - break; + // Fig 7.2: Classify a given polygon by raycasting from its barycenter into the faces of the other object. + // Public for testing. + public static void ClassifyPolygonUsingRaycast(CsgPolygon poly, CsgObject wrt) + { + Vector3 rayStart = poly.baryCenter; + Vector3 rayNormal = poly.plane.normal; + CsgPolygon closest = null; + float closestPolyDist = float.MaxValue; + + bool done; + int count = 0; + do + { + done = true; // Done unless we hit a special case. + foreach (CsgPolygon otherPoly in wrt.polygons) + { + float dot = Vector3.Dot(rayNormal, otherPoly.plane.normal); + bool perp = Mathf.Abs(dot) < CsgMath.EPSILON; + bool onOtherPlane = Mathf.Abs(otherPoly.plane.GetDistanceToPoint(rayStart)) < CsgMath.EPSILON; + Vector3 projectedToOtherPlane = Vector3.zero; + float signedDist = -1f; + if (!perp) + { + CsgMath.RayPlaneIntersection(out projectedToOtherPlane, rayStart, rayNormal, otherPoly.plane); + float dist = Vector3.Distance(projectedToOtherPlane, rayStart); + signedDist = dist * Mathf.Sign(Vector3.Dot(rayNormal, (projectedToOtherPlane - rayStart))); + } + + if (perp && onOtherPlane) + { + done = false; + break; + } + else if (perp && !onOtherPlane) + { + // no intersection + } + else if (!perp && onOtherPlane) + { + int isInside = CsgMath.IsInside(otherPoly, projectedToOtherPlane); + if (isInside >= 0) + { + closestPolyDist = 0; + closest = otherPoly; + break; + } + } + else if (!perp && signedDist > 0) + { + if (signedDist < closestPolyDist) + { + int isInside = CsgMath.IsInside(otherPoly, projectedToOtherPlane); + if (isInside > 0) + { + closest = otherPoly; + closestPolyDist = signedDist; + } + else if (isInside == 0) + { + // On segment, perturb and try again. + done = false; + break; + } + } + } + } + if (!done) + { + // Perturb the normal and try again. + rayNormal += new Vector3( + UnityEngine.Random.Range(-0.1f, 0.1f), + UnityEngine.Random.Range(-0.1f, 0.1f), + UnityEngine.Random.Range(-0.1f, 0.1f)); + rayNormal = rayNormal.normalized; + } + count++; + } while (!done && count < 5); + + if (closest == null) + { + // Didn't hit any polys, we are outside. + poly.status = PolygonStatus.OUTSIDE; } - } else if (!perp && signedDist > 0) { - if (signedDist < closestPolyDist) { - int isInside = CsgMath.IsInside(otherPoly, projectedToOtherPlane); - if (isInside > 0) { - closest = otherPoly; - closestPolyDist = signedDist; - } else if (isInside == 0) { - // On segment, perturb and try again. - done = false; - break; - } + else + { + float dot = Vector3.Dot(poly.plane.normal, closest.plane.normal); + if (Mathf.Abs(closestPolyDist) < CsgMath.EPSILON) + { + poly.status = dot < 0 ? PolygonStatus.OPPOSITE : PolygonStatus.SAME; + } + else + { + poly.status = dot < 0 ? PolygonStatus.OUTSIDE : PolygonStatus.INSIDE; + } } - } } - if (!done) { - // Perturb the normal and try again. - rayNormal += new Vector3( - UnityEngine.Random.Range(-0.1f, 0.1f), - UnityEngine.Random.Range(-0.1f, 0.1f), - UnityEngine.Random.Range(-0.1f, 0.1f)); - rayNormal = rayNormal.normalized; - } - count++; - } while (!done && count < 5) ; - - if (closest == null) { - // Didn't hit any polys, we are outside. - poly.status = PolygonStatus.OUTSIDE; - } else { - float dot = Vector3.Dot(poly.plane.normal, closest.plane.normal); - if (Mathf.Abs(closestPolyDist) < CsgMath.EPSILON) { - poly.status = dot < 0 ? PolygonStatus.OPPOSITE : PolygonStatus.SAME; - } else { - poly.status = dot < 0 ? PolygonStatus.OUTSIDE : PolygonStatus.INSIDE; + + private static bool HasUnknown(CsgPolygon poly) + { + foreach (CsgVertex vertex in poly.vertices) + { + if (vertex.status == VertexStatus.UNKNOWN) + { + return true; + } + } + return false; } - } - } - private static bool HasUnknown(CsgPolygon poly) { - foreach (CsgVertex vertex in poly.vertices) { - if (vertex.status == VertexStatus.UNKNOWN) { - return true; + private static bool AllBoundary(CsgPolygon poly) + { + foreach (CsgVertex vertex in poly.vertices) + { + if (vertex.status != VertexStatus.BOUNDARY) + { + return false; + } + } + return true; } - } - return false; - } - private static bool AllBoundary(CsgPolygon poly) { - foreach (CsgVertex vertex in poly.vertices) { - if (vertex.status != VertexStatus.BOUNDARY) { - return false; + // Public for testing. + public static void SplitObject(CsgContext ctx, CsgObject toSplit, CsgObject splitBy) + { + bool splitPoly; + int count = 0; + HashSet alreadySplit = new HashSet(); + do + { + splitPoly = false; + // Temporary guard to prevent infinite loops while there are bugs. + // TODO(bug) figure out why csg creates so many rejected splits. + count++; + if (count > 100) + { + // This usually occurs when csg keeps trying to do the same invalid split over and over. + // If the algorithm has reached this point, it usually means that the two meshes are + // split enough to perform a pretty good looking csg subtraction. More investigation + // should be done on bug and we may be able to remove this guard. + return; + } + foreach (CsgPolygon toSplitPoly in toSplit.polygons) + { + if (alreadySplit.Contains(toSplitPoly)) + { + continue; + } + alreadySplit.Add(toSplitPoly); + if (toSplitPoly.bounds.Intersects(splitBy.bounds)) + { + foreach (CsgPolygon splitByPoly in splitBy.polygons) + { + if (toSplitPoly.bounds.Intersects(splitByPoly.bounds) + && !Coplanar(toSplitPoly.plane, splitByPoly.plane)) + { + splitPoly = PolygonSplitter.SplitPolys(ctx, toSplit, toSplitPoly, splitByPoly); + if (splitPoly) + { + break; + } + } + } + } + if (splitPoly) + { + break; + } + } + } while (splitPoly); } - } - return true; - } - // Public for testing. - public static void SplitObject(CsgContext ctx, CsgObject toSplit, CsgObject splitBy) { - bool splitPoly; - int count = 0; - HashSet alreadySplit = new HashSet(); - do { - splitPoly = false; - // Temporary guard to prevent infinite loops while there are bugs. - // TODO(bug) figure out why csg creates so many rejected splits. - count++; - if (count > 100) { - // This usually occurs when csg keeps trying to do the same invalid split over and over. - // If the algorithm has reached this point, it usually means that the two meshes are - // split enough to perform a pretty good looking csg subtraction. More investigation - // should be done on bug and we may be able to remove this guard. - return; + private static bool Coplanar(Plane plane1, Plane plane2) + { + return Mathf.Abs(plane1.distance - plane2.distance) < COPLANAR_EPS + && Vector3.Distance(plane1.normal, plane2.normal) < COPLANAR_EPS; } - foreach (CsgPolygon toSplitPoly in toSplit.polygons) { - if (alreadySplit.Contains(toSplitPoly)) { - continue; - } - alreadySplit.Add(toSplitPoly); - if (toSplitPoly.bounds.Intersects(splitBy.bounds)) { - foreach (CsgPolygon splitByPoly in splitBy.polygons) { - if (toSplitPoly.bounds.Intersects(splitByPoly.bounds) - && !Coplanar(toSplitPoly.plane, splitByPoly.plane)) { - splitPoly = PolygonSplitter.SplitPolys(ctx, toSplit, toSplitPoly, splitByPoly); - if (splitPoly) { - break; + + // Make an MMesh from a set of CsgPolys. Each unique CsgVertex should be a unique vertex in the MMesh. + // Public for testing. + public static MMesh FromPolys(int id, Vector3 offset, Quaternion rotation, List polys, + Vector3? csgOffset = null, float? scale = null, HashSet remixIds = null) + { + if (!csgOffset.HasValue) + { + csgOffset = Vector3.zero; + } + if (!scale.HasValue) + { + scale = 1.0f; + } + Dictionary vertexToId = new Dictionary(); + MMesh newMesh = new MMesh(id, Vector3.zero, Quaternion.identity, + new Dictionary(), new Dictionary(), MMesh.GROUP_NONE, remixIds); + MMesh.GeometryOperation constructionOperation = newMesh.StartOperation(); + foreach (CsgPolygon poly in polys) + { + List vertexIds = new List(); + List normals = new List(); + foreach (CsgVertex vertex in poly.vertices) + { + int vertId; + if (!vertexToId.TryGetValue(vertex, out vertId)) + { + + + Vertex meshVertex = constructionOperation.AddVertexMeshSpace(Quaternion.Inverse(rotation) * + ((vertex.loc / scale.Value - csgOffset.Value) - offset)); + vertId = meshVertex.id; + vertexToId[vertex] = vertId; + } + vertexIds.Add(vertId); + normals.Add(poly.plane.normal); } - } + constructionOperation.AddFace(vertexIds, poly.faceProperties); } - } - if (splitPoly) { - break; - } - } - } while (splitPoly); - } - - private static bool Coplanar(Plane plane1, Plane plane2) { - return Mathf.Abs(plane1.distance - plane2.distance) < COPLANAR_EPS - && Vector3.Distance(plane1.normal, plane2.normal) < COPLANAR_EPS; - } + constructionOperation.Commit(); + newMesh.offset = offset; + newMesh.rotation = rotation; - // Make an MMesh from a set of CsgPolys. Each unique CsgVertex should be a unique vertex in the MMesh. - // Public for testing. - public static MMesh FromPolys(int id, Vector3 offset, Quaternion rotation, List polys, - Vector3? csgOffset = null, float? scale = null, HashSet remixIds = null) { - if (!csgOffset.HasValue) { - csgOffset = Vector3.zero; - } - if (!scale.HasValue) { - scale = 1.0f; - } - Dictionary vertexToId = new Dictionary(); - MMesh newMesh = new MMesh(id, Vector3.zero, Quaternion.identity, - new Dictionary(), new Dictionary(), MMesh.GROUP_NONE, remixIds); - MMesh.GeometryOperation constructionOperation = newMesh.StartOperation(); - foreach (CsgPolygon poly in polys) { - List vertexIds = new List(); - List normals = new List(); - foreach(CsgVertex vertex in poly.vertices) { - int vertId; - if (!vertexToId.TryGetValue(vertex, out vertId)) { - - - Vertex meshVertex = constructionOperation.AddVertexMeshSpace(Quaternion.Inverse(rotation) * - ((vertex.loc / scale.Value - csgOffset.Value) - offset)); - vertId = meshVertex.id; - vertexToId[vertex] = vertId; - } - vertexIds.Add(vertId); - normals.Add(poly.plane.normal); + return newMesh; } - constructionOperation.AddFace(vertexIds, poly.faceProperties); - } - constructionOperation.Commit(); - newMesh.offset = offset; - newMesh.rotation = rotation; - return newMesh; - } + // Convert an MMesh into a CsgObject. + // Public for testing. + public static CsgObject ToCsg(CsgContext ctx, MMesh mesh, Vector3? offset = null, float? scale = null) + { + if (!offset.HasValue) + { + offset = Vector3.zero; + } + if (!scale.HasValue) + { + scale = 1.0f; + } + Dictionary idToVert = new Dictionary(); + foreach (int vertexId in mesh.GetVertexIds()) + { + idToVert[vertexId] = ctx.CreateOrGetVertexAt((mesh.VertexPositionInModelCoords(vertexId) + offset.Value) * scale.Value); + } - // Convert an MMesh into a CsgObject. - // Public for testing. - public static CsgObject ToCsg(CsgContext ctx, MMesh mesh, Vector3? offset = null, float? scale = null) { - if (!offset.HasValue) { - offset = Vector3.zero; - } - if (!scale.HasValue) { - scale = 1.0f; - } - Dictionary idToVert = new Dictionary(); - foreach(int vertexId in mesh.GetVertexIds()) { - idToVert[vertexId] = ctx.CreateOrGetVertexAt((mesh.VertexPositionInModelCoords(vertexId) + offset.Value) * scale.Value); - } - - List polys = new List(); - foreach(Face face in mesh.GetFaces()) { - GeneratePolygonsForFace(polys, idToVert, mesh, face); - } - - return new CsgObject(polys, new List(idToVert.Values)); - } + List polys = new List(); + foreach (Face face in mesh.GetFaces()) + { + GeneratePolygonsForFace(polys, idToVert, mesh, face); + } - // Generate CsgPolygons for a Face. CsgPolygons should be convex and have no holes. - private static void GeneratePolygonsForFace( - List polys, Dictionary idToVert, MMesh mesh, Face face) { - List vertices = new List(); - foreach (int vertexId in face.vertexIds) { - vertices.Add(idToVert[vertexId]); - } - CsgPolygon poly = new CsgPolygon(vertices, face.properties); - polys.Add(poly); + return new CsgObject(polys, new List(idToVert.Values)); + } + + // Generate CsgPolygons for a Face. CsgPolygons should be convex and have no holes. + private static void GeneratePolygonsForFace( + List polys, Dictionary idToVert, MMesh mesh, Face face) + { + List vertices = new List(); + foreach (int vertexId in face.vertexIds) + { + vertices.Add(idToVert[vertexId]); + } + CsgPolygon poly = new CsgPolygon(vertices, face.properties); + polys.Add(poly); + } } - } } diff --git a/Assets/Scripts/model/csg/CsgPolygon.cs b/Assets/Scripts/model/csg/CsgPolygon.cs index 18d8880c..351dab4e 100644 --- a/Assets/Scripts/model/csg/CsgPolygon.cs +++ b/Assets/Scripts/model/csg/CsgPolygon.cs @@ -19,57 +19,66 @@ using com.google.apps.peltzer.client.model.core; -namespace com.google.apps.peltzer.client.model.csg { - public enum PolygonStatus { - UNKNOWN, - INSIDE, - OUTSIDE, - SAME, - OPPOSITE, - } +namespace com.google.apps.peltzer.client.model.csg +{ + public enum PolygonStatus + { + UNKNOWN, + INSIDE, + OUTSIDE, + SAME, + OPPOSITE, + } - /// - /// A convex, coplanar polygon. - /// - public class CsgPolygon { - public List vertices { get; set; } - public Plane plane { get; private set; } - public FaceProperties faceProperties { get; set; } - public Bounds bounds { get; private set; } - public PolygonStatus status { get; set; } - public Vector3 baryCenter; + /// + /// A convex, coplanar polygon. + /// + public class CsgPolygon + { + public List vertices { get; set; } + public Plane plane { get; private set; } + public FaceProperties faceProperties { get; set; } + public Bounds bounds { get; private set; } + public PolygonStatus status { get; set; } + public Vector3 baryCenter; - public CsgPolygon(List vertices, FaceProperties faceProperties, Vector3? normal = null) { - this.vertices = vertices; - this.faceProperties = faceProperties; + public CsgPolygon(List vertices, FaceProperties faceProperties, Vector3? normal = null) + { + this.vertices = vertices; + this.faceProperties = faceProperties; - if (normal != null) { - plane = new Plane(normal.Value, vertices[0].loc); - } else { - plane = new Plane(vertices[0].loc, vertices[1].loc, vertices[2].loc); - } + if (normal != null) + { + plane = new Plane(normal.Value, vertices[0].loc); + } + else + { + plane = new Plane(vertices[0].loc, vertices[1].loc, vertices[2].loc); + } - // Calc bounds and baryCenter - Bounds bounds = new Bounds(vertices[0].loc, Vector3.zero); - baryCenter = vertices[0].loc; - for (int i = 1; i < vertices.Count; i++) { - bounds.Encapsulate(vertices[i].loc); - baryCenter += vertices[i].loc; - } - baryCenter /= (float)vertices.Count; + // Calc bounds and baryCenter + Bounds bounds = new Bounds(vertices[0].loc, Vector3.zero); + baryCenter = vertices[0].loc; + for (int i = 1; i < vertices.Count; i++) + { + bounds.Encapsulate(vertices[i].loc); + baryCenter += vertices[i].loc; + } + baryCenter /= (float)vertices.Count; - // Expand a bit to cover floating point error. - bounds.Expand(0.002f); - this.bounds = bounds; + // Expand a bit to cover floating point error. + bounds.Expand(0.002f); + this.bounds = bounds; - // Status is UNKNOWN to start with. - status = PolygonStatus.UNKNOWN; - } + // Status is UNKNOWN to start with. + status = PolygonStatus.UNKNOWN; + } - internal CsgPolygon Invert() { - List newVerts = new List(vertices); - newVerts.Reverse(); - return new CsgPolygon(newVerts, faceProperties, -plane.normal); + internal CsgPolygon Invert() + { + List newVerts = new List(vertices); + newVerts.Reverse(); + return new CsgPolygon(newVerts, faceProperties, -plane.normal); + } } - } } diff --git a/Assets/Scripts/model/csg/CsgUtil.cs b/Assets/Scripts/model/csg/CsgUtil.cs index 9031f78a..57af27c7 100644 --- a/Assets/Scripts/model/csg/CsgUtil.cs +++ b/Assets/Scripts/model/csg/CsgUtil.cs @@ -18,145 +18,175 @@ using System.Text; using UnityEngine; -namespace com.google.apps.peltzer.client.model.csg { - public class PolyEdge { - CsgVertex a; - CsgVertex b; - - public PolyEdge(CsgVertex a, CsgVertex b) { - this.a = a; - this.b = b; - } - - public PolyEdge Reversed() { - return new PolyEdge(b, a); - } - - public override bool Equals(object obj) { - if (obj is PolyEdge) { - PolyEdge other = (PolyEdge)obj; - return a == other.a && b == other.b; - } - return false; - } - - public override int GetHashCode() { - int hc = 13; - hc = (hc * 31) + a.GetHashCode(); - hc = (hc * 31) + b.GetHashCode(); - return hc; - } - } - - public class CsgUtil { - - // Do some sanity checks on a polygon split: - // 1) All polys should have the same normal. - // 2) Each vertex from the original poly should be in at least one splitPoly - // 3) Each split poly should share at least one edge with one other (the edge should be reversed) - // 4) No edge should be in more than one poly (in the same order) - // 5) No vertex should be in the same poly more than once - // 6) Every edge in the initial polygon should be in a split, except those *edges* that were split. - // We pass in numSplitEdges to tell the test how many that should be. - public static bool IsValidPolygonSplit(CsgPolygon initialPoly, List splitPolys, int numSplitEdges) { - List> vertsForPolys = new List>(); - List> edgesForPolys = new List>(); - - // Set up some datastructures, check normals while we are looping. - foreach (CsgPolygon poly in splitPolys) { - vertsForPolys.Add(new HashSet(poly.vertices)); - edgesForPolys.Add(Edges(poly)); - if (Vector3.Distance(initialPoly.plane.normal, poly.plane.normal) > 0.001f) { - Console.Write("Normals do not match: " + initialPoly.plane.normal + " vs " + poly.plane.normal); - return false; - } - } - - // Look for each vertex from the original poly: - foreach (CsgVertex vert in initialPoly.vertices) { - bool found = false; - foreach (HashSet verts in vertsForPolys) { - if (verts.Contains(vert)) { - found = true; - break; - } +namespace com.google.apps.peltzer.client.model.csg +{ + public class PolyEdge + { + CsgVertex a; + CsgVertex b; + + public PolyEdge(CsgVertex a, CsgVertex b) + { + this.a = a; + this.b = b; } - if (!found) { - Console.Write("Vertex from original poly is missing from split polys"); - return false; + + public PolyEdge Reversed() + { + return new PolyEdge(b, a); } - } - - // For each poly, find another poly with a matching edge (going the other direction) - for (int i = 0; i < edgesForPolys.Count; i++) { - HashSet polyEdges = edgesForPolys[i]; - bool foundEdge = false; - for (int j = 0; j < edgesForPolys.Count; j++) { - if (i == j) { - continue; // Don't compare polygon to itself - } - foreach (PolyEdge edge in polyEdges) { - if (edgesForPolys[j].Contains(edge.Reversed())) { - foundEdge = true; + + public override bool Equals(object obj) + { + if (obj is PolyEdge) + { + PolyEdge other = (PolyEdge)obj; + return a == other.a && b == other.b; } - } - } - if (!foundEdge) { - Console.Write("Poly " + i + " does not have any edges in other polys"); - return false; - } - } - - // Check that the total number of edges is the same as the sum of all edges in all splits - // i.e. there are no duplicate edges. - HashSet alledges = new HashSet(); - int sum = 0; - foreach (HashSet edges in edgesForPolys) { - sum += edges.Count; - alledges.UnionWith(edges); - } - if (sum != alledges.Count) { - Console.Write("Found duplicate edges."); - return false; - } - - // Check to make sure no polys have the same vertex more than once. - for (int i = 0; i < vertsForPolys.Count; i++) { - // The 'Set' should have the same number of verts as the 'List' - if (vertsForPolys[i].Count != splitPolys[i].vertices.Count) { - Console.Write("Found duplicate vertex"); - return false; + return false; } - } - - // Look for all edges in the list above. The count should be the same number of edges - // in the initial poly minus the number of edges that were split. - int count = numSplitEdges; - HashSet initialEdges = Edges(initialPoly); - foreach (PolyEdge initialEdge in initialEdges) { - if (alledges.Contains(initialEdge)) { - count++; - } - } - if (initialEdges.Count != count) { - Console.Write("Edges from initial poly are missing"); - return false; - } - return true; + public override int GetHashCode() + { + int hc = 13; + hc = (hc * 31) + a.GetHashCode(); + hc = (hc * 31) + b.GetHashCode(); + return hc; + } } - private static HashSet Edges(CsgPolygon poly) { - HashSet edges = new HashSet(); + public class CsgUtil + { + + // Do some sanity checks on a polygon split: + // 1) All polys should have the same normal. + // 2) Each vertex from the original poly should be in at least one splitPoly + // 3) Each split poly should share at least one edge with one other (the edge should be reversed) + // 4) No edge should be in more than one poly (in the same order) + // 5) No vertex should be in the same poly more than once + // 6) Every edge in the initial polygon should be in a split, except those *edges* that were split. + // We pass in numSplitEdges to tell the test how many that should be. + public static bool IsValidPolygonSplit(CsgPolygon initialPoly, List splitPolys, int numSplitEdges) + { + List> vertsForPolys = new List>(); + List> edgesForPolys = new List>(); + + // Set up some datastructures, check normals while we are looping. + foreach (CsgPolygon poly in splitPolys) + { + vertsForPolys.Add(new HashSet(poly.vertices)); + edgesForPolys.Add(Edges(poly)); + if (Vector3.Distance(initialPoly.plane.normal, poly.plane.normal) > 0.001f) + { + Console.Write("Normals do not match: " + initialPoly.plane.normal + " vs " + poly.plane.normal); + return false; + } + } + + // Look for each vertex from the original poly: + foreach (CsgVertex vert in initialPoly.vertices) + { + bool found = false; + foreach (HashSet verts in vertsForPolys) + { + if (verts.Contains(vert)) + { + found = true; + break; + } + } + if (!found) + { + Console.Write("Vertex from original poly is missing from split polys"); + return false; + } + } + + // For each poly, find another poly with a matching edge (going the other direction) + for (int i = 0; i < edgesForPolys.Count; i++) + { + HashSet polyEdges = edgesForPolys[i]; + bool foundEdge = false; + for (int j = 0; j < edgesForPolys.Count; j++) + { + if (i == j) + { + continue; // Don't compare polygon to itself + } + foreach (PolyEdge edge in polyEdges) + { + if (edgesForPolys[j].Contains(edge.Reversed())) + { + foundEdge = true; + } + } + } + if (!foundEdge) + { + Console.Write("Poly " + i + " does not have any edges in other polys"); + return false; + } + } + + // Check that the total number of edges is the same as the sum of all edges in all splits + // i.e. there are no duplicate edges. + HashSet alledges = new HashSet(); + int sum = 0; + foreach (HashSet edges in edgesForPolys) + { + sum += edges.Count; + alledges.UnionWith(edges); + } + if (sum != alledges.Count) + { + Console.Write("Found duplicate edges."); + return false; + } + + // Check to make sure no polys have the same vertex more than once. + for (int i = 0; i < vertsForPolys.Count; i++) + { + // The 'Set' should have the same number of verts as the 'List' + if (vertsForPolys[i].Count != splitPolys[i].vertices.Count) + { + Console.Write("Found duplicate vertex"); + return false; + } + } - for (int i = 0; i < poly.vertices.Count; i++) { - CsgVertex a = poly.vertices[i]; - CsgVertex b = poly.vertices[(i + 1) % poly.vertices.Count]; - edges.Add(new PolyEdge(a, b)); - } + // Look for all edges in the list above. The count should be the same number of edges + // in the initial poly minus the number of edges that were split. + int count = numSplitEdges; + HashSet initialEdges = Edges(initialPoly); + foreach (PolyEdge initialEdge in initialEdges) + { + if (alledges.Contains(initialEdge)) + { + count++; + } + } + if (initialEdges.Count != count) + { + Console.Write("Edges from initial poly are missing"); + return false; + } + + return true; + } - return edges; + private static HashSet Edges(CsgPolygon poly) + { + HashSet edges = new HashSet(); + + for (int i = 0; i < poly.vertices.Count; i++) + { + CsgVertex a = poly.vertices[i]; + CsgVertex b = poly.vertices[(i + 1) % poly.vertices.Count]; + edges.Add(new PolyEdge(a, b)); + } + + return edges; + } } - } } diff --git a/Assets/Scripts/model/csg/CsgVertex.cs b/Assets/Scripts/model/csg/CsgVertex.cs index 32a07a15..d4a4efeb 100644 --- a/Assets/Scripts/model/csg/CsgVertex.cs +++ b/Assets/Scripts/model/csg/CsgVertex.cs @@ -17,34 +17,39 @@ using System.Linq; using UnityEngine; -namespace com.google.apps.peltzer.client.model.csg { - public enum VertexStatus { - UNKNOWN, - INSIDE, - OUTSIDE, - BOUNDARY - } +namespace com.google.apps.peltzer.client.model.csg +{ + public enum VertexStatus + { + UNKNOWN, + INSIDE, + OUTSIDE, + BOUNDARY + } - /// - /// A vertex with an associated 'status'. The status determines whether a given vertex is inside another - /// object (or outside or on its boundary). - /// - [System.Diagnostics.DebuggerDisplay("{ToString()}")] - public class CsgVertex { - public Vector3 loc { get; private set; } - public HashSet neighbors { get; private set; } - public VertexStatus status { get; set; } - private readonly String asString; + /// + /// A vertex with an associated 'status'. The status determines whether a given vertex is inside another + /// object (or outside or on its boundary). + /// + [System.Diagnostics.DebuggerDisplay("{ToString()}")] + public class CsgVertex + { + public Vector3 loc { get; private set; } + public HashSet neighbors { get; private set; } + public VertexStatus status { get; set; } + private readonly String asString; - public CsgVertex(Vector3 loc) { - this.loc = loc; - this.neighbors = new HashSet(); - this.status = VertexStatus.UNKNOWN; - this.asString = "(" + loc.x.ToString("0.000") + ", " + loc.y.ToString("0.000") + ", " + loc.z.ToString("0.000") + ")"; - } + public CsgVertex(Vector3 loc) + { + this.loc = loc; + this.neighbors = new HashSet(); + this.status = VertexStatus.UNKNOWN; + this.asString = "(" + loc.x.ToString("0.000") + ", " + loc.y.ToString("0.000") + ", " + loc.z.ToString("0.000") + ")"; + } - public override string ToString() { - return asString; + public override string ToString() + { + return asString; + } } - } } diff --git a/Assets/Scripts/model/csg/FaceDecomposer.cs b/Assets/Scripts/model/csg/FaceDecomposer.cs index f1fa26dd..446fd080 100644 --- a/Assets/Scripts/model/csg/FaceDecomposer.cs +++ b/Assets/Scripts/model/csg/FaceDecomposer.cs @@ -20,44 +20,52 @@ using com.google.apps.peltzer.client.model.render; using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.model.csg { - public class FaceDecomposer { - /// - /// Decompose a face into convex polygons. Ideally this should produce as few pieces as possible. - /// The current implementation either returns the border -- if it is convex without holes -- or - /// uses the FaceTriangulator to split it into triangles. - /// - /// Outside of the polygon -- in clockwise order. - /// Holes in the polygon -- each in counterclockwise order. - /// A list of polygons, each represented as a list of vertex ids. - public static List> Decompose(List border, List> holes) { +namespace com.google.apps.peltzer.client.model.csg +{ + public class FaceDecomposer + { + /// + /// Decompose a face into convex polygons. Ideally this should produce as few pieces as possible. + /// The current implementation either returns the border -- if it is convex without holes -- or + /// uses the FaceTriangulator to split it into triangles. + /// + /// Outside of the polygon -- in clockwise order. + /// Holes in the polygon -- each in counterclockwise order. + /// A list of polygons, each represented as a list of vertex ids. + public static List> Decompose(List border, List> holes) + { - // If there are no holes and the face is convex, just return the border as the poly. - if (holes.Count == 0) { - bool convex = true; - // If it has 3 vertices it will always be convex. - if (border.Count > 3) { - Vector3 faceNormal = MeshMath.CalculateNormal(border); - for (int i = 0; i < border.Count; i++) { - convex = Math3d.IsConvex( - border[(i + 1) % border.Count].loc, - border[i].loc, - border[(i + 2) % border.Count].loc, - faceNormal); - if (!convex) { - break; + // If there are no holes and the face is convex, just return the border as the poly. + if (holes.Count == 0) + { + bool convex = true; + // If it has 3 vertices it will always be convex. + if (border.Count > 3) + { + Vector3 faceNormal = MeshMath.CalculateNormal(border); + for (int i = 0; i < border.Count; i++) + { + convex = Math3d.IsConvex( + border[(i + 1) % border.Count].loc, + border[i].loc, + border[(i + 2) % border.Count].loc, + faceNormal); + if (!convex) + { + break; + } + } + } + if (convex) + { + List poly = new List(border.Select(v => v.id)); + return new List> { poly }; + } } - } - } - if (convex) { - List poly = new List(border.Select(v => v.id)); - return new List> { poly }; - } - } - // Otherwise, blow the face apart into triangles using the FaceTriangulator. - List triangles = FaceTriangulator.Triangulate(border); - return new List>(triangles.Select(t => new List() { t.vertId0, t.vertId1, t.vertId2 })); + // Otherwise, blow the face apart into triangles using the FaceTriangulator. + List triangles = FaceTriangulator.Triangulate(border); + return new List>(triangles.Select(t => new List() { t.vertId0, t.vertId1, t.vertId2 })); + } } - } } diff --git a/Assets/Scripts/model/csg/FaceRecomposer.cs b/Assets/Scripts/model/csg/FaceRecomposer.cs index af682ae4..9bc4a248 100644 --- a/Assets/Scripts/model/csg/FaceRecomposer.cs +++ b/Assets/Scripts/model/csg/FaceRecomposer.cs @@ -20,137 +20,160 @@ using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.model.csg { - public class FaceRecomposer { - /// - /// Given a list of polygons (which are specified as a list of vertices) we try to merge together - /// as many polygons as possible. - /// - /// We assume that two vertices with the same id are the same vertex (semantically speaking). So our - /// strategy is to look for matching segments in two polygons. If two polygons have the same pair - /// (in reverse order from each other) within their vertex list, we can join them at that segment. - /// - /// If there are no overlapping segments, we also look for a shared vertex. If we find one, we - /// look for a possible T-junction. If we find one, we will join the polygons at that junction. - /// - public static List> RecomposeFace(List> startPieces) { - List> joinedPieces = new List>(); - while (startPieces.Count > 0) { - Dictionary curSegments = new Dictionary(); - Dictionary curVerts = new Dictionary(); - List curPiece = startPieces[0]; - startPieces.RemoveAt(0); - LoadSegments(curPiece, curSegments, curVerts); +namespace com.google.apps.peltzer.client.model.csg +{ + public class FaceRecomposer + { + /// + /// Given a list of polygons (which are specified as a list of vertices) we try to merge together + /// as many polygons as possible. + /// + /// We assume that two vertices with the same id are the same vertex (semantically speaking). So our + /// strategy is to look for matching segments in two polygons. If two polygons have the same pair + /// (in reverse order from each other) within their vertex list, we can join them at that segment. + /// + /// If there are no overlapping segments, we also look for a shared vertex. If we find one, we + /// look for a possible T-junction. If we find one, we will join the polygons at that junction. + /// + public static List> RecomposeFace(List> startPieces) + { + List> joinedPieces = new List>(); + while (startPieces.Count > 0) + { + Dictionary curSegments = new Dictionary(); + Dictionary curVerts = new Dictionary(); + List curPiece = startPieces[0]; + startPieces.RemoveAt(0); + LoadSegments(curPiece, curSegments, curVerts); - // Walk through all other pieces to see if one shares a segment. - bool foundJoin; - do { - foundJoin = false; - for (int i = 0; i < startPieces.Count; i++) { - List toJoin = startPieces[i]; + // Walk through all other pieces to see if one shares a segment. + bool foundJoin; + do + { + foundJoin = false; + for (int i = 0; i < startPieces.Count; i++) + { + List toJoin = startPieces[i]; - for (int j = 0; j < toJoin.Count; j++) { - Edge seg = new Edge(toJoin[(j + 1) % toJoin.Count].vertexId, toJoin[j].vertexId); - int joinAt; - if (curSegments.TryGetValue(seg, out joinAt)) { - JoinPiecesAt(curPiece, joinAt, toJoin, j); - LoadSegments(curPiece, curSegments, curVerts); - startPieces.RemoveAt(i); - foundJoin = true; - break; - } - if (foundJoin) { - break; - } - } + for (int j = 0; j < toJoin.Count; j++) + { + Edge seg = new Edge(toJoin[(j + 1) % toJoin.Count].vertexId, toJoin[j].vertexId); + int joinAt; + if (curSegments.TryGetValue(seg, out joinAt)) + { + JoinPiecesAt(curPiece, joinAt, toJoin, j); + LoadSegments(curPiece, curSegments, curVerts); + startPieces.RemoveAt(i); + foundJoin = true; + break; + } + if (foundJoin) + { + break; + } + } - if (!foundJoin) { - // Look for a matching point. Maybe split the polygon at that point. - for (int j = 0; j < toJoin.Count; j++) { - int idx; - if (curVerts.TryGetValue(toJoin[j].vertexId, out idx)) { - int jMinusOne = (j - 1 + toJoin.Count) % toJoin.Count; - int jPlusOne = (j + 1) % toJoin.Count; - int idxPlusOne = (idx + 1) % curPiece.Count; - int idxMinusOne = (idx - 1 + curPiece.Count) % curPiece.Count; + if (!foundJoin) + { + // Look for a matching point. Maybe split the polygon at that point. + for (int j = 0; j < toJoin.Count; j++) + { + int idx; + if (curVerts.TryGetValue(toJoin[j].vertexId, out idx)) + { + int jMinusOne = (j - 1 + toJoin.Count) % toJoin.Count; + int jPlusOne = (j + 1) % toJoin.Count; + int idxPlusOne = (idx + 1) % curPiece.Count; + int idxMinusOne = (idx - 1 + curPiece.Count) % curPiece.Count; - if (IsOnSegment(toJoin[jPlusOne], curPiece[idx], curPiece[idxMinusOne]) - || IsOnSegment(curPiece[idxMinusOne], toJoin[j], toJoin[jPlusOne])) { - curPiece.RemoveAt(idx); - curPiece.InsertRange(idx, CycleAndRemoveN(toJoin, j + 1, 1)); - LoadSegments(curPiece, curSegments, curVerts); - startPieces.RemoveAt(i); - foundJoin = true; - break; - } else if (IsOnSegment(toJoin[jMinusOne], curPiece[idx], curPiece[idxPlusOne]) - || IsOnSegment(curPiece[idxPlusOne], toJoin[j], toJoin[jMinusOne])) { - curPiece.RemoveAt(idx); - curPiece.InsertRange(idx, CycleAndRemoveN(toJoin, j + 1, 1)); - LoadSegments(curPiece, curSegments, curVerts); - startPieces.RemoveAt(i); - foundJoin = true; - break; - } - } - } + if (IsOnSegment(toJoin[jPlusOne], curPiece[idx], curPiece[idxMinusOne]) + || IsOnSegment(curPiece[idxMinusOne], toJoin[j], toJoin[jPlusOne])) + { + curPiece.RemoveAt(idx); + curPiece.InsertRange(idx, CycleAndRemoveN(toJoin, j + 1, 1)); + LoadSegments(curPiece, curSegments, curVerts); + startPieces.RemoveAt(i); + foundJoin = true; + break; + } + else if (IsOnSegment(toJoin[jMinusOne], curPiece[idx], curPiece[idxPlusOne]) + || IsOnSegment(curPiece[idxPlusOne], toJoin[j], toJoin[jMinusOne])) + { + curPiece.RemoveAt(idx); + curPiece.InsertRange(idx, CycleAndRemoveN(toJoin, j + 1, 1)); + LoadSegments(curPiece, curSegments, curVerts); + startPieces.RemoveAt(i); + foundJoin = true; + break; + } + } + } + } + if (foundJoin) + { + break; + } + } + } while (foundJoin); + joinedPieces.Add(curPiece); } - if (foundJoin) { - break; - } - } - } while (foundJoin); - joinedPieces.Add(curPiece); - } - return joinedPieces; - } + return joinedPieces; + } - /// - /// Look for a T-junction. - /// - private static bool IsOnSegment(SolidVertex point, SolidVertex start, SolidVertex end) { - Vector3 startLoc = start.position; - Vector3 lineVec = (end.position - start.position).normalized; - float lineLength = Vector3.Distance(start.position, end.position); - Vector3 startToPoint = point.position - startLoc; - float t = Vector3.Dot(startToPoint, lineVec); - Vector3 projected = startLoc + lineVec * t; - return t >= 0 && t <= lineLength && Vector3.Distance(projected, point.position) < 0.0005; - } + /// + /// Look for a T-junction. + /// + private static bool IsOnSegment(SolidVertex point, SolidVertex start, SolidVertex end) + { + Vector3 startLoc = start.position; + Vector3 lineVec = (end.position - start.position).normalized; + float lineLength = Vector3.Distance(start.position, end.position); + Vector3 startToPoint = point.position - startLoc; + float t = Vector3.Dot(startToPoint, lineVec); + Vector3 projected = startLoc + lineVec * t; + return t >= 0 && t <= lineLength && Vector3.Distance(projected, point.position) < 0.0005; + } - /// - /// Join two polygons that share a segment. - /// - private static void JoinPiecesAt(List mainPiece, int joinAt, List toJoin, int joinTo) { - List toInsert = new List(); - for (int i = 0; i < (toJoin.Count - 2); i++) { - toInsert.Add(toJoin[(i + joinTo + 2) % toJoin.Count]); - } - mainPiece.InsertRange((joinAt + 1) % mainPiece.Count, toInsert); - } + /// + /// Join two polygons that share a segment. + /// + private static void JoinPiecesAt(List mainPiece, int joinAt, List toJoin, int joinTo) + { + List toInsert = new List(); + for (int i = 0; i < (toJoin.Count - 2); i++) + { + toInsert.Add(toJoin[(i + joinTo + 2) % toJoin.Count]); + } + mainPiece.InsertRange((joinAt + 1) % mainPiece.Count, toInsert); + } - /// - /// Grab a sub-chain of a polygon. - /// - private static List CycleAndRemoveN(List orig, int offset, int n) { - List copy = new List(); - for (int i = 0; i < (orig.Count - n); i++) { - copy.Add(orig[(i + offset) % orig.Count]); - } - return copy; - } + /// + /// Grab a sub-chain of a polygon. + /// + private static List CycleAndRemoveN(List orig, int offset, int n) + { + List copy = new List(); + for (int i = 0; i < (orig.Count - n); i++) + { + copy.Add(orig[(i + offset) % orig.Count]); + } + return copy; + } - /// - /// Fill the indexes with data from the given polygon. - /// - private static void LoadSegments( - List curPiece, Dictionary curSegments, Dictionary curVerts) { - curSegments.Clear(); - curVerts.Clear(); - for (int i = 0; i < curPiece.Count; i++) { - curVerts[curPiece[i].vertexId] = i; - curSegments[new Edge(curPiece[i].vertexId, curPiece[(i + 1) % curPiece.Count].vertexId)] = i; - } + /// + /// Fill the indexes with data from the given polygon. + /// + private static void LoadSegments( + List curPiece, Dictionary curSegments, Dictionary curVerts) + { + curSegments.Clear(); + curVerts.Clear(); + for (int i = 0; i < curPiece.Count; i++) + { + curVerts[curPiece[i].vertexId] = i; + curSegments[new Edge(curPiece[i].vertexId, curPiece[(i + 1) % curPiece.Count].vertexId)] = i; + } + } } - } } diff --git a/Assets/Scripts/model/csg/PolygonSplitter.cs b/Assets/Scripts/model/csg/PolygonSplitter.cs index 985ef250..47a2ff34 100644 --- a/Assets/Scripts/model/csg/PolygonSplitter.cs +++ b/Assets/Scripts/model/csg/PolygonSplitter.cs @@ -19,895 +19,1070 @@ using UnityEngine; using com.google.apps.peltzer.client.model.core; -namespace com.google.apps.peltzer.client.model.csg { - - /// - /// Constants for segment endpoint type and segment types. - /// - public class Endpoint { - public const int VERTEX = 1; - public const int FACE = 2; - public const int EDGE = 3; - - public const int V_V_V = VERTEX * 100 + VERTEX * 10 + VERTEX; - public const int V_E_V = VERTEX * 100 + EDGE * 10 + VERTEX; - public const int V_E_E = VERTEX * 100 + EDGE * 10 + EDGE; - public const int V_F_V = VERTEX * 100 + FACE * 10 + VERTEX; - public const int V_F_E = VERTEX * 100 + FACE * 10 + EDGE; - public const int V_F_F = VERTEX * 100 + FACE * 10 + FACE; - public const int E_E_V = EDGE * 100 + EDGE * 10 + VERTEX; - public const int E_E_E = EDGE * 100 + EDGE * 10 + EDGE; - public const int E_F_V = EDGE * 100 + FACE * 10 + VERTEX; - public const int E_F_E = EDGE * 100 + FACE * 10 + EDGE; - public const int E_F_F = EDGE * 100 + FACE * 10 + FACE; - public const int F_F_V = FACE * 100 + FACE * 10 + VERTEX; - public const int F_F_E = FACE * 100 + FACE * 10 + EDGE; - public const int F_F_F = FACE * 100 + FACE * 10 + FACE; - - public static int combine(int a, int b, int c) { - return a * 100 + b * 10 + c; - } - } - - /// - /// Descriptor of segment which is the intersection of two polygons. - /// See Fig 5.1. - /// - public class SegmentDescriptor { - public int start; - public int middle; - public int end; - - public int startVertIdx; - public int endVertIdx; - public float startDist; - public float endDist; - - public CsgVertex startVertex; - public CsgVertex endVertex; - - // Values after trimmed to the splitting poly. - public int finalStart; - public int finalMiddle; - public int finalEnd; - - public CsgVertex finalStartVertex; - public CsgVertex finalEndVertex; - } - - /// - /// Code to split a polygon with another one. The resulting polygons must always be convex without holes. - /// - public class PolygonSplitter { - // Enable to get debug logging. - private const bool DEBUG = false; - // Enable to add polygon split checks. - private const bool VERIFY_POLY_SPLITS = false; - - /// Section 6: Split both of the polygons with respect to each other. - public static bool SplitPolys(CsgContext ctx, CsgObject objA, CsgPolygon polyA, CsgPolygon polyB) { - float[] distAtoB = DistanceFromVertsToPlane(polyA, polyB.plane); - if (!CrossesPlane(distAtoB)) { - // All verts on one side of plane, no intersection. - return false; - } - float[] distBtoA = DistanceFromVertsToPlane(polyB, polyA.plane); - if (!CrossesPlane(distAtoB)) { - // All verts on one side of plane, no intersection. - return false; - } - - // Get the line that is the intersection of the planes. - Vector3 linePt; - Vector3 lineDir; - if (!PlanePlaneIntersection(out linePt, out lineDir, polyA.plane, polyB.plane)) { - // Planes are parallel - return false; - } - - SegmentDescriptor segA = CalcSegmentDescriptor(ctx, linePt, lineDir, distAtoB, polyA); - SegmentDescriptor segB = CalcSegmentDescriptor(ctx, linePt, lineDir, distBtoA, polyB); - - if (segA == null || segB == null) { - return false; - } - - // If the segments don't overlap, there is no intersection. - if (segA.endDist <= segB.startDist || segB.endDist <= segA.startDist) { - return false; - } - - TrimTo(segA, segB); - - return SplitPolyOnSegment(objA, polyA, segA); +namespace com.google.apps.peltzer.client.model.csg +{ + + /// + /// Constants for segment endpoint type and segment types. + /// + public class Endpoint + { + public const int VERTEX = 1; + public const int FACE = 2; + public const int EDGE = 3; + + public const int V_V_V = VERTEX * 100 + VERTEX * 10 + VERTEX; + public const int V_E_V = VERTEX * 100 + EDGE * 10 + VERTEX; + public const int V_E_E = VERTEX * 100 + EDGE * 10 + EDGE; + public const int V_F_V = VERTEX * 100 + FACE * 10 + VERTEX; + public const int V_F_E = VERTEX * 100 + FACE * 10 + EDGE; + public const int V_F_F = VERTEX * 100 + FACE * 10 + FACE; + public const int E_E_V = EDGE * 100 + EDGE * 10 + VERTEX; + public const int E_E_E = EDGE * 100 + EDGE * 10 + EDGE; + public const int E_F_V = EDGE * 100 + FACE * 10 + VERTEX; + public const int E_F_E = EDGE * 100 + FACE * 10 + EDGE; + public const int E_F_F = EDGE * 100 + FACE * 10 + FACE; + public const int F_F_V = FACE * 100 + FACE * 10 + VERTEX; + public const int F_F_E = FACE * 100 + FACE * 10 + EDGE; + public const int F_F_F = FACE * 100 + FACE * 10 + FACE; + + public static int combine(int a, int b, int c) + { + return a * 100 + b * 10 + c; + } } - // Trim segment A to segment B. This is where polygon A is cut by polygon B. - private static void TrimTo(SegmentDescriptor segA, SegmentDescriptor segB) { - segA.finalMiddle = segA.middle; - if (segB.startDist > segA.startDist && Math.Abs(segA.startDist - segB.startDist) > CsgMath.EPSILON) { - // Push segA start distance up. - segA.finalStart = segA.middle; - segA.finalStartVertex = segB.startVertex; - } else { - segA.finalStart = segA.start; - segA.finalStartVertex = segA.startVertex; - } - if (segB.endDist < segA.endDist && Math.Abs(segA.endDist - segB.endDist) > CsgMath.EPSILON) { - // Pull segA end distance back. - segA.finalEnd = segA.middle; - segA.finalEndVertex = segB.endVertex; - } else { - segA.finalEnd = segA.end; - segA.finalEndVertex = segA.endVertex; - } - - if (segA.finalStartVertex == segA.finalEndVertex) { - // Trimmed to a single vertex. - segA.finalStart = Endpoint.VERTEX; - segA.finalMiddle = Endpoint.VERTEX; - segA.finalEnd = Endpoint.VERTEX; - } + /// + /// Descriptor of segment which is the intersection of two polygons. + /// See Fig 5.1. + /// + public class SegmentDescriptor + { + public int start; + public int middle; + public int end; + + public int startVertIdx; + public int endVertIdx; + public float startDist; + public float endDist; + + public CsgVertex startVertex; + public CsgVertex endVertex; + + // Values after trimmed to the splitting poly. + public int finalStart; + public int finalMiddle; + public int finalEnd; + + public CsgVertex finalStartVertex; + public CsgVertex finalEndVertex; } - // Given the exact segment where a polygon is split by the other, actually split the polygon. - // The way the polygon is split depends exactly on how the segment intersects the polygon. - // See Figure 6.3 for all the gory details. - // Public for testing. - public static bool SplitPolyOnSegment(CsgObject obj, CsgPolygon poly, SegmentDescriptor seg) { - int splitType = Endpoint.combine(seg.finalStart, seg.finalMiddle, seg.finalEnd); - - if (DEBUG) { - Console.Write("Split type: " + splitType + " seg = " + seg.startVertIdx + ", " + seg.endVertIdx + ", " + poly.vertices.Count); - } - - // For symmetrical cases, swap everything - if (splitType == Endpoint.F_F_V || splitType == Endpoint.E_E_V) { - if (DEBUG) { - Console.Write("Swapped"); + /// + /// Code to split a polygon with another one. The resulting polygons must always be convex without holes. + /// + public class PolygonSplitter + { + // Enable to get debug logging. + private const bool DEBUG = false; + // Enable to add polygon split checks. + private const bool VERIFY_POLY_SPLITS = false; + + /// Section 6: Split both of the polygons with respect to each other. + public static bool SplitPolys(CsgContext ctx, CsgObject objA, CsgPolygon polyA, CsgPolygon polyB) + { + float[] distAtoB = DistanceFromVertsToPlane(polyA, polyB.plane); + if (!CrossesPlane(distAtoB)) + { + // All verts on one side of plane, no intersection. + return false; + } + float[] distBtoA = DistanceFromVertsToPlane(polyB, polyA.plane); + if (!CrossesPlane(distAtoB)) + { + // All verts on one side of plane, no intersection. + return false; + } + + // Get the line that is the intersection of the planes. + Vector3 linePt; + Vector3 lineDir; + if (!PlanePlaneIntersection(out linePt, out lineDir, polyA.plane, polyB.plane)) + { + // Planes are parallel + return false; + } + + SegmentDescriptor segA = CalcSegmentDescriptor(ctx, linePt, lineDir, distAtoB, polyA); + SegmentDescriptor segB = CalcSegmentDescriptor(ctx, linePt, lineDir, distBtoA, polyB); + + if (segA == null || segB == null) + { + return false; + } + + // If the segments don't overlap, there is no intersection. + if (segA.endDist <= segB.startDist || segB.endDist <= segA.startDist) + { + return false; + } + + TrimTo(segA, segB); + + return SplitPolyOnSegment(objA, polyA, segA); } - Swap(seg); - splitType = Endpoint.combine(seg.finalStart, seg.finalMiddle, seg.finalEnd); - } - - switch (splitType) { - case Endpoint.E_E_E: { - if (Vector3.Distance(seg.finalStartVertex.loc, seg.startVertex.loc) < CsgMath.EPSILON - && Vector3.Distance(seg.finalEndVertex.loc, seg.endVertex.loc) < CsgMath.EPSILON) { - return false; - } - List> newPolys = new List>(); - int startInClockwiseOrder = seg.startVertIdx; - int endInClockwiseOrder = seg.endVertIdx; - CsgVertex startVertInClockwiseOrder = seg.finalStartVertex; - CsgVertex endVertInClockwiseOrder = seg.finalEndVertex; - bool pointsAreDifferent = startVertInClockwiseOrder != endVertInClockwiseOrder; - if ((seg.endVertIdx + 1) % poly.vertices.Count == seg.startVertIdx) { - startInClockwiseOrder = seg.endVertIdx; - endInClockwiseOrder = seg.startVertIdx; - startVertInClockwiseOrder = seg.finalEndVertex; - endVertInClockwiseOrder = seg.finalStartVertex; - } - - newPolys.Add(new List() { + + // Trim segment A to segment B. This is where polygon A is cut by polygon B. + private static void TrimTo(SegmentDescriptor segA, SegmentDescriptor segB) + { + segA.finalMiddle = segA.middle; + if (segB.startDist > segA.startDist && Math.Abs(segA.startDist - segB.startDist) > CsgMath.EPSILON) + { + // Push segA start distance up. + segA.finalStart = segA.middle; + segA.finalStartVertex = segB.startVertex; + } + else + { + segA.finalStart = segA.start; + segA.finalStartVertex = segA.startVertex; + } + if (segB.endDist < segA.endDist && Math.Abs(segA.endDist - segB.endDist) > CsgMath.EPSILON) + { + // Pull segA end distance back. + segA.finalEnd = segA.middle; + segA.finalEndVertex = segB.endVertex; + } + else + { + segA.finalEnd = segA.end; + segA.finalEndVertex = segA.endVertex; + } + + if (segA.finalStartVertex == segA.finalEndVertex) + { + // Trimmed to a single vertex. + segA.finalStart = Endpoint.VERTEX; + segA.finalMiddle = Endpoint.VERTEX; + segA.finalEnd = Endpoint.VERTEX; + } + } + + // Given the exact segment where a polygon is split by the other, actually split the polygon. + // The way the polygon is split depends exactly on how the segment intersects the polygon. + // See Figure 6.3 for all the gory details. + // Public for testing. + public static bool SplitPolyOnSegment(CsgObject obj, CsgPolygon poly, SegmentDescriptor seg) + { + int splitType = Endpoint.combine(seg.finalStart, seg.finalMiddle, seg.finalEnd); + + if (DEBUG) + { + Console.Write("Split type: " + splitType + " seg = " + seg.startVertIdx + ", " + seg.endVertIdx + ", " + poly.vertices.Count); + } + + // For symmetrical cases, swap everything + if (splitType == Endpoint.F_F_V || splitType == Endpoint.E_E_V) + { + if (DEBUG) + { + Console.Write("Swapped"); + } + Swap(seg); + splitType = Endpoint.combine(seg.finalStart, seg.finalMiddle, seg.finalEnd); + } + + switch (splitType) + { + case Endpoint.E_E_E: + { + if (Vector3.Distance(seg.finalStartVertex.loc, seg.startVertex.loc) < CsgMath.EPSILON + && Vector3.Distance(seg.finalEndVertex.loc, seg.endVertex.loc) < CsgMath.EPSILON) + { + return false; + } + List> newPolys = new List>(); + int startInClockwiseOrder = seg.startVertIdx; + int endInClockwiseOrder = seg.endVertIdx; + CsgVertex startVertInClockwiseOrder = seg.finalStartVertex; + CsgVertex endVertInClockwiseOrder = seg.finalEndVertex; + bool pointsAreDifferent = startVertInClockwiseOrder != endVertInClockwiseOrder; + if ((seg.endVertIdx + 1) % poly.vertices.Count == seg.startVertIdx) + { + startInClockwiseOrder = seg.endVertIdx; + endInClockwiseOrder = seg.startVertIdx; + startVertInClockwiseOrder = seg.finalEndVertex; + endVertInClockwiseOrder = seg.finalStartVertex; + } + + newPolys.Add(new List() { poly.vertices[(startInClockwiseOrder - 1 + poly.vertices.Count) % poly.vertices.Count], poly.vertices[startInClockwiseOrder], startVertInClockwiseOrder }); - if (pointsAreDifferent) { - newPolys.Add(new List() { + if (pointsAreDifferent) + { + newPolys.Add(new List() { poly.vertices[(startInClockwiseOrder - 1 + poly.vertices.Count) % poly.vertices.Count], startVertInClockwiseOrder, endVertInClockwiseOrder }); - } - List theRest = new List(); - for (int i = endInClockwiseOrder; i != startInClockwiseOrder; i = (i + 1) % poly.vertices.Count) { - theRest.Add(poly.vertices[i]); - } - theRest.Add(endVertInClockwiseOrder); - newPolys.Add(theRest); - if (SafeReplacePolys(seg, obj, poly, newPolys.ToArray())) { - obj.vertices.Add(startVertInClockwiseOrder); - if (pointsAreDifferent) { - obj.vertices.Add(endVertInClockwiseOrder); - } - return true; - } else { - return false; - } - } - case Endpoint.V_V_V: { - // Fig 6.3 (a) - seg.finalStartVertex.status = VertexStatus.BOUNDARY; - return false; - } - case Endpoint.V_E_V: { - // Fig 6.3 (b) - seg.finalStartVertex.status = VertexStatus.BOUNDARY; - seg.finalEndVertex.status = VertexStatus.BOUNDARY; - return false; - } - case Endpoint.V_E_E: { - // Fig 6.3 (c) - if (seg.endVertex == seg.finalEndVertex && seg.startVertex == seg.finalStartVertex) { - //Debug.Log("no split is possible because segment is bad"); - return false; - } - List mainPart = new List(); - List triPart = new List(); - - for (int i = (seg.startVertIdx + 1) % poly.vertices.Count; i != seg.startVertIdx; i = (i + 1) % poly.vertices.Count) { - mainPart.Add(poly.vertices[i]); - } - mainPart.Add(seg.finalEndVertex); - if (seg.startVertIdx == seg.endVertIdx) { - // I don't think this ever happens. - triPart.Add(poly.vertices[(seg.startVertIdx - 1 + poly.vertices.Count) % poly.vertices.Count]); - triPart.Add(poly.vertices[seg.startVertIdx]); - triPart.Add(seg.finalEndVertex); - } else if (((seg.endVertIdx + 1) % poly.vertices.Count) == seg.startVertIdx) { - triPart.Add(seg.finalEndVertex); - triPart.Add(poly.vertices[seg.startVertIdx]); - triPart.Add(poly.vertices[(seg.startVertIdx + 1) % poly.vertices.Count]); - } else if ((seg.startVertIdx + 1) % poly.vertices.Count == seg.endVertIdx) { - triPart.Add(poly.vertices[(seg.startVertIdx - 1 + poly.vertices.Count) % poly.vertices.Count]); - triPart.Add(poly.vertices[seg.startVertIdx]); - triPart.Add(seg.finalEndVertex); - } else { - // This should never occur. - return false; - } - - if (SafeReplacePolys(seg, obj, poly, mainPart, triPart)) { - obj.vertices.Add(seg.finalEndVertex); - - poly.vertices[seg.startVertIdx].status = VertexStatus.BOUNDARY; - return true; - } else { - return false; - } - } - case Endpoint.E_E_V: { - // Fig 6.3 (c) - List mainPart = new List(); - List triPart = new List(); - - CsgVertex vertToAdd = null; - - if (seg.startVertIdx == seg.endVertIdx) { - for (int i = (seg.startVertIdx + 1) % poly.vertices.Count; i != seg.startVertIdx; i = (i + 1) % poly.vertices.Count) { - mainPart.Add(poly.vertices[i]); - } - mainPart.Add(seg.finalEndVertex); - triPart.Add(poly.vertices[seg.startVertIdx]); - triPart.Add(poly.vertices[(seg.startVertIdx + 1) % poly.vertices.Count]); - triPart.Add(seg.finalEndVertex); - vertToAdd = seg.finalEndVertex; - } else if (((seg.endVertIdx + 1) % poly.vertices.Count) == seg.startVertIdx) { - for (int i = seg.startVertIdx; i != seg.endVertIdx; i = (i + 1) % poly.vertices.Count) { - mainPart.Add(poly.vertices[i]); - } - mainPart.Add(seg.finalStartVertex); - triPart.Add(poly.vertices[(seg.endVertIdx - 1 + poly.vertices.Count) % poly.vertices.Count]); - triPart.Add(poly.vertices[seg.endVertIdx]); - triPart.Add(seg.finalStartVertex); - vertToAdd = seg.finalStartVertex; - } else if ((seg.startVertIdx + 1) % poly.vertices.Count == seg.endVertIdx) { - for (int i = (seg.endVertIdx + 1) % poly.vertices.Count; i != seg.endVertIdx; i = (i + 1) % poly.vertices.Count) { - mainPart.Add(poly.vertices[i]); - } - mainPart.Add(seg.finalStartVertex); - triPart.Add(poly.vertices[seg.endVertIdx]); - triPart.Add(poly.vertices[(seg.endVertIdx + 1) % poly.vertices.Count]); - triPart.Add(seg.finalStartVertex); - vertToAdd = seg.finalStartVertex; - } else { - // Should not occur - return false; - } - - if (vertToAdd != null) { - obj.vertices.Add(vertToAdd); - } - poly.vertices[seg.endVertIdx].status = VertexStatus.BOUNDARY; - - return SafeReplacePolys(seg, obj, poly, mainPart, triPart); - } - case Endpoint.V_F_V: { - // Fig 6.3 (e) - List topPart = new List(); - for (int i = seg.startVertIdx; i != (seg.endVertIdx + 1) % poly.vertices.Count; i = (i + 1) % poly.vertices.Count) { - topPart.Add(poly.vertices[i]); - } - List bottomPart = new List(); - for (int i = seg.endVertIdx; i != (seg.startVertIdx + 1) % poly.vertices.Count; i = (i + 1) % poly.vertices.Count) { - bottomPart.Add(poly.vertices[i]); - } - - if(SafeReplacePolys(seg, obj, poly, topPart, bottomPart)) { - poly.vertices[seg.startVertIdx].status = VertexStatus.BOUNDARY; - poly.vertices[seg.endVertIdx].status = VertexStatus.BOUNDARY; - return true; - } else { - return false; - } - } - case Endpoint.E_F_E: { - // Fig 6.3 (k) - List topPart = new List(); - List bottomPart = new List(); - int vCount = poly.vertices.Count; - int startI = seg.startVertIdx; - int endI = seg.endVertIdx; - CsgVertex startV = seg.finalStartVertex; - CsgVertex endV = seg.finalEndVertex; - if ((endI + 1) % vCount == startI) { - startI = seg.endVertIdx; - endI = seg.startVertIdx; - startV = seg.finalEndVertex; - endV = seg.finalStartVertex; - } - for (int i = (startI + 1) % vCount; i != (endI + 1) % vCount; i = (i + 1) % vCount) { - topPart.Add(poly.vertices[i]); - } - topPart.Add(endV); - topPart.Add(startV); - for (int i = (endI + 1) % vCount; i != (startI + 1) % vCount; i = (i + 1) % vCount) { - bottomPart.Add(poly.vertices[i]); - } - bottomPart.Add(startV); - bottomPart.Add(endV); - - if (SafeReplacePolys(seg, obj, poly, topPart, bottomPart)) { - obj.vertices.Add(seg.finalStartVertex); - obj.vertices.Add(seg.finalEndVertex); - return true; - } else { - return false; - } - } - case Endpoint.V_F_E: { - // Fig 6.3 (f) - List topPart = new List(); - List bottomPart = new List(); - topPart.Add(seg.finalEndVertex); - topPart.Add(seg.finalStartVertex); - - bottomPart.Add(seg.finalStartVertex); - bottomPart.Add(seg.finalEndVertex); - - bool topHalf = true; - for (int i = 1; i < poly.vertices.Count; i++) { - int idx = (seg.startVertIdx + i) % poly.vertices.Count; - if (topHalf) { - topPart.Add(poly.vertices[idx]); - } else { - bottomPart.Add(poly.vertices[idx]); - } - if (idx == seg.endVertIdx) { - topHalf = false; - } - } - - if (SafeReplacePolys(seg, obj, poly, topPart, bottomPart)) { - obj.vertices.Add(seg.finalStartVertex); - obj.vertices.Add(seg.finalEndVertex); - return true; - } else { - return false; - } - } - case Endpoint.E_F_V: { - // Fig 6.3(f) - List topPart = new List(); - for (int i = seg.endVertIdx; i != (seg.startVertIdx + 1) % poly.vertices.Count; i = (i + 1) % poly.vertices.Count) { - topPart.Add(poly.vertices[i]); - } - topPart.Add(seg.finalStartVertex); - List bottomPart = new List(); - bottomPart.Add(seg.finalStartVertex); - for (int i = (seg.startVertIdx + 1) % poly.vertices.Count; i != (seg.endVertIdx + 1) % poly.vertices.Count; i = (i + 1) % poly.vertices.Count) { - bottomPart.Add(poly.vertices[i]); - } - - obj.vertices.Add(seg.finalStartVertex); - poly.vertices[seg.endVertIdx].status = VertexStatus.BOUNDARY; - - return SafeReplacePolys(seg, obj, poly, topPart, bottomPart); - } - case Endpoint.E_F_F: { - // Fig 6.3 (l/m) - List topPart = new List(); - topPart.Add(seg.finalStartVertex); - int topEndIdx = seg.end == Endpoint.VERTEX ? (seg.endVertIdx - 1 + poly.vertices.Count) % poly.vertices.Count - : seg.endVertIdx; - for (int idx = (seg.startVertIdx + 1) % poly.vertices.Count; idx != topEndIdx; - idx = (idx + 1) % poly.vertices.Count) { - topPart.Add(poly.vertices[idx]); - } - topPart.Add(poly.vertices[topEndIdx]); - topPart.Add(seg.finalEndVertex); - - List bottomPart = new List(); - bottomPart.Add(seg.finalEndVertex); - for (int idx = (seg.endVertIdx + 1) % poly.vertices.Count; idx != seg.startVertIdx; - idx = (idx + 1) % poly.vertices.Count) { - bottomPart.Add(poly.vertices[idx]); - } - bottomPart.Add(poly.vertices[seg.startVertIdx]); - bottomPart.Add(seg.finalStartVertex); - - bool result = false; - if (seg.end == Endpoint.VERTEX) { - // Fig 6.3 (l) - CsgVertex prevVert = poly.vertices[(seg.endVertIdx - 1 + poly.vertices.Count) % poly.vertices.Count]; - CsgVertex nextVert = poly.vertices[(seg.endVertIdx + 1) % poly.vertices.Count]; - - result = SafeReplacePolys(seg, obj, poly, topPart, bottomPart, - new List() { seg.finalEndVertex, prevVert, seg.endVertex }, - new List() { seg.finalEndVertex, seg.endVertex, nextVert }); - } else { - // Fig 6.3 (m) - CsgVertex nextVert = poly.vertices[(seg.endVertIdx + 1) % poly.vertices.Count]; - result = SafeReplacePolys(seg, obj, poly, topPart, bottomPart, - new List() { seg.finalEndVertex, poly.vertices[seg.endVertIdx], nextVert }); - } - if (result) { - obj.vertices.Add(seg.finalStartVertex); - obj.vertices.Add(seg.finalEndVertex); - } - return result; - } - case Endpoint.F_F_E: { - List> newPolys = new List>(); - newPolys.Add(new List() { + } + List theRest = new List(); + for (int i = endInClockwiseOrder; i != startInClockwiseOrder; i = (i + 1) % poly.vertices.Count) + { + theRest.Add(poly.vertices[i]); + } + theRest.Add(endVertInClockwiseOrder); + newPolys.Add(theRest); + if (SafeReplacePolys(seg, obj, poly, newPolys.ToArray())) + { + obj.vertices.Add(startVertInClockwiseOrder); + if (pointsAreDifferent) + { + obj.vertices.Add(endVertInClockwiseOrder); + } + return true; + } + else + { + return false; + } + } + case Endpoint.V_V_V: + { + // Fig 6.3 (a) + seg.finalStartVertex.status = VertexStatus.BOUNDARY; + return false; + } + case Endpoint.V_E_V: + { + // Fig 6.3 (b) + seg.finalStartVertex.status = VertexStatus.BOUNDARY; + seg.finalEndVertex.status = VertexStatus.BOUNDARY; + return false; + } + case Endpoint.V_E_E: + { + // Fig 6.3 (c) + if (seg.endVertex == seg.finalEndVertex && seg.startVertex == seg.finalStartVertex) + { + //Debug.Log("no split is possible because segment is bad"); + return false; + } + List mainPart = new List(); + List triPart = new List(); + + for (int i = (seg.startVertIdx + 1) % poly.vertices.Count; i != seg.startVertIdx; i = (i + 1) % poly.vertices.Count) + { + mainPart.Add(poly.vertices[i]); + } + mainPart.Add(seg.finalEndVertex); + if (seg.startVertIdx == seg.endVertIdx) + { + // I don't think this ever happens. + triPart.Add(poly.vertices[(seg.startVertIdx - 1 + poly.vertices.Count) % poly.vertices.Count]); + triPart.Add(poly.vertices[seg.startVertIdx]); + triPart.Add(seg.finalEndVertex); + } + else if (((seg.endVertIdx + 1) % poly.vertices.Count) == seg.startVertIdx) + { + triPart.Add(seg.finalEndVertex); + triPart.Add(poly.vertices[seg.startVertIdx]); + triPart.Add(poly.vertices[(seg.startVertIdx + 1) % poly.vertices.Count]); + } + else if ((seg.startVertIdx + 1) % poly.vertices.Count == seg.endVertIdx) + { + triPart.Add(poly.vertices[(seg.startVertIdx - 1 + poly.vertices.Count) % poly.vertices.Count]); + triPart.Add(poly.vertices[seg.startVertIdx]); + triPart.Add(seg.finalEndVertex); + } + else + { + // This should never occur. + return false; + } + + if (SafeReplacePolys(seg, obj, poly, mainPart, triPart)) + { + obj.vertices.Add(seg.finalEndVertex); + + poly.vertices[seg.startVertIdx].status = VertexStatus.BOUNDARY; + return true; + } + else + { + return false; + } + } + case Endpoint.E_E_V: + { + // Fig 6.3 (c) + List mainPart = new List(); + List triPart = new List(); + + CsgVertex vertToAdd = null; + + if (seg.startVertIdx == seg.endVertIdx) + { + for (int i = (seg.startVertIdx + 1) % poly.vertices.Count; i != seg.startVertIdx; i = (i + 1) % poly.vertices.Count) + { + mainPart.Add(poly.vertices[i]); + } + mainPart.Add(seg.finalEndVertex); + triPart.Add(poly.vertices[seg.startVertIdx]); + triPart.Add(poly.vertices[(seg.startVertIdx + 1) % poly.vertices.Count]); + triPart.Add(seg.finalEndVertex); + vertToAdd = seg.finalEndVertex; + } + else if (((seg.endVertIdx + 1) % poly.vertices.Count) == seg.startVertIdx) + { + for (int i = seg.startVertIdx; i != seg.endVertIdx; i = (i + 1) % poly.vertices.Count) + { + mainPart.Add(poly.vertices[i]); + } + mainPart.Add(seg.finalStartVertex); + triPart.Add(poly.vertices[(seg.endVertIdx - 1 + poly.vertices.Count) % poly.vertices.Count]); + triPart.Add(poly.vertices[seg.endVertIdx]); + triPart.Add(seg.finalStartVertex); + vertToAdd = seg.finalStartVertex; + } + else if ((seg.startVertIdx + 1) % poly.vertices.Count == seg.endVertIdx) + { + for (int i = (seg.endVertIdx + 1) % poly.vertices.Count; i != seg.endVertIdx; i = (i + 1) % poly.vertices.Count) + { + mainPart.Add(poly.vertices[i]); + } + mainPart.Add(seg.finalStartVertex); + triPart.Add(poly.vertices[seg.endVertIdx]); + triPart.Add(poly.vertices[(seg.endVertIdx + 1) % poly.vertices.Count]); + triPart.Add(seg.finalStartVertex); + vertToAdd = seg.finalStartVertex; + } + else + { + // Should not occur + return false; + } + + if (vertToAdd != null) + { + obj.vertices.Add(vertToAdd); + } + poly.vertices[seg.endVertIdx].status = VertexStatus.BOUNDARY; + + return SafeReplacePolys(seg, obj, poly, mainPart, triPart); + } + case Endpoint.V_F_V: + { + // Fig 6.3 (e) + List topPart = new List(); + for (int i = seg.startVertIdx; i != (seg.endVertIdx + 1) % poly.vertices.Count; i = (i + 1) % poly.vertices.Count) + { + topPart.Add(poly.vertices[i]); + } + List bottomPart = new List(); + for (int i = seg.endVertIdx; i != (seg.startVertIdx + 1) % poly.vertices.Count; i = (i + 1) % poly.vertices.Count) + { + bottomPart.Add(poly.vertices[i]); + } + + if (SafeReplacePolys(seg, obj, poly, topPart, bottomPart)) + { + poly.vertices[seg.startVertIdx].status = VertexStatus.BOUNDARY; + poly.vertices[seg.endVertIdx].status = VertexStatus.BOUNDARY; + return true; + } + else + { + return false; + } + } + case Endpoint.E_F_E: + { + // Fig 6.3 (k) + List topPart = new List(); + List bottomPart = new List(); + int vCount = poly.vertices.Count; + int startI = seg.startVertIdx; + int endI = seg.endVertIdx; + CsgVertex startV = seg.finalStartVertex; + CsgVertex endV = seg.finalEndVertex; + if ((endI + 1) % vCount == startI) + { + startI = seg.endVertIdx; + endI = seg.startVertIdx; + startV = seg.finalEndVertex; + endV = seg.finalStartVertex; + } + for (int i = (startI + 1) % vCount; i != (endI + 1) % vCount; i = (i + 1) % vCount) + { + topPart.Add(poly.vertices[i]); + } + topPart.Add(endV); + topPart.Add(startV); + for (int i = (endI + 1) % vCount; i != (startI + 1) % vCount; i = (i + 1) % vCount) + { + bottomPart.Add(poly.vertices[i]); + } + bottomPart.Add(startV); + bottomPart.Add(endV); + + if (SafeReplacePolys(seg, obj, poly, topPart, bottomPart)) + { + obj.vertices.Add(seg.finalStartVertex); + obj.vertices.Add(seg.finalEndVertex); + return true; + } + else + { + return false; + } + } + case Endpoint.V_F_E: + { + // Fig 6.3 (f) + List topPart = new List(); + List bottomPart = new List(); + topPart.Add(seg.finalEndVertex); + topPart.Add(seg.finalStartVertex); + + bottomPart.Add(seg.finalStartVertex); + bottomPart.Add(seg.finalEndVertex); + + bool topHalf = true; + for (int i = 1; i < poly.vertices.Count; i++) + { + int idx = (seg.startVertIdx + i) % poly.vertices.Count; + if (topHalf) + { + topPart.Add(poly.vertices[idx]); + } + else + { + bottomPart.Add(poly.vertices[idx]); + } + if (idx == seg.endVertIdx) + { + topHalf = false; + } + } + + if (SafeReplacePolys(seg, obj, poly, topPart, bottomPart)) + { + obj.vertices.Add(seg.finalStartVertex); + obj.vertices.Add(seg.finalEndVertex); + return true; + } + else + { + return false; + } + } + case Endpoint.E_F_V: + { + // Fig 6.3(f) + List topPart = new List(); + for (int i = seg.endVertIdx; i != (seg.startVertIdx + 1) % poly.vertices.Count; i = (i + 1) % poly.vertices.Count) + { + topPart.Add(poly.vertices[i]); + } + topPart.Add(seg.finalStartVertex); + List bottomPart = new List(); + bottomPart.Add(seg.finalStartVertex); + for (int i = (seg.startVertIdx + 1) % poly.vertices.Count; i != (seg.endVertIdx + 1) % poly.vertices.Count; i = (i + 1) % poly.vertices.Count) + { + bottomPart.Add(poly.vertices[i]); + } + + obj.vertices.Add(seg.finalStartVertex); + poly.vertices[seg.endVertIdx].status = VertexStatus.BOUNDARY; + + return SafeReplacePolys(seg, obj, poly, topPart, bottomPart); + } + case Endpoint.E_F_F: + { + // Fig 6.3 (l/m) + List topPart = new List(); + topPart.Add(seg.finalStartVertex); + int topEndIdx = seg.end == Endpoint.VERTEX ? (seg.endVertIdx - 1 + poly.vertices.Count) % poly.vertices.Count + : seg.endVertIdx; + for (int idx = (seg.startVertIdx + 1) % poly.vertices.Count; idx != topEndIdx; + idx = (idx + 1) % poly.vertices.Count) + { + topPart.Add(poly.vertices[idx]); + } + topPart.Add(poly.vertices[topEndIdx]); + topPart.Add(seg.finalEndVertex); + + List bottomPart = new List(); + bottomPart.Add(seg.finalEndVertex); + for (int idx = (seg.endVertIdx + 1) % poly.vertices.Count; idx != seg.startVertIdx; + idx = (idx + 1) % poly.vertices.Count) + { + bottomPart.Add(poly.vertices[idx]); + } + bottomPart.Add(poly.vertices[seg.startVertIdx]); + bottomPart.Add(seg.finalStartVertex); + + bool result = false; + if (seg.end == Endpoint.VERTEX) + { + // Fig 6.3 (l) + CsgVertex prevVert = poly.vertices[(seg.endVertIdx - 1 + poly.vertices.Count) % poly.vertices.Count]; + CsgVertex nextVert = poly.vertices[(seg.endVertIdx + 1) % poly.vertices.Count]; + + result = SafeReplacePolys(seg, obj, poly, topPart, bottomPart, + new List() { seg.finalEndVertex, prevVert, seg.endVertex }, + new List() { seg.finalEndVertex, seg.endVertex, nextVert }); + } + else + { + // Fig 6.3 (m) + CsgVertex nextVert = poly.vertices[(seg.endVertIdx + 1) % poly.vertices.Count]; + result = SafeReplacePolys(seg, obj, poly, topPart, bottomPart, + new List() { seg.finalEndVertex, poly.vertices[seg.endVertIdx], nextVert }); + } + if (result) + { + obj.vertices.Add(seg.finalStartVertex); + obj.vertices.Add(seg.finalEndVertex); + } + return result; + } + case Endpoint.F_F_E: + { + List> newPolys = new List>(); + newPolys.Add(new List() { poly.vertices[seg.startVertIdx], poly.vertices[(seg.startVertIdx + 1) % poly.vertices.Count], seg.finalStartVertex }); - List topPart = new List(); - for (int i = (seg.startVertIdx + 1) % poly.vertices.Count; i != (seg.endVertIdx + 1) % poly.vertices.Count; i = (i + 1) % poly.vertices.Count) { - topPart.Add(poly.vertices[i]); - } - topPart.Add(seg.finalEndVertex); - topPart.Add(seg.finalStartVertex); - newPolys.Add(topPart); - - if (seg.start == Endpoint.VERTEX) { - newPolys.Add(new List() { + List topPart = new List(); + for (int i = (seg.startVertIdx + 1) % poly.vertices.Count; i != (seg.endVertIdx + 1) % poly.vertices.Count; i = (i + 1) % poly.vertices.Count) + { + topPart.Add(poly.vertices[i]); + } + topPart.Add(seg.finalEndVertex); + topPart.Add(seg.finalStartVertex); + newPolys.Add(topPart); + + if (seg.start == Endpoint.VERTEX) + { + newPolys.Add(new List() { poly.vertices[(seg.startVertIdx - 1 + poly.vertices.Count) % poly.vertices.Count], poly.vertices[seg.startVertIdx], seg.finalStartVertex }); - // bottom part goes until right before begin - List bottomPart = new List(); - for (int i = (seg.endVertIdx + 1) % poly.vertices.Count; i != seg.startVertIdx; i = (i + 1) % poly.vertices.Count) { - bottomPart.Add(poly.vertices[i]); - } - bottomPart.Add(seg.finalStartVertex); - bottomPart.Add(seg.finalEndVertex); - newPolys.Add(bottomPart); - } else { - List bottomPart = new List(); - for (int i = (seg.endVertIdx + 1) % poly.vertices.Count; i != (seg.startVertIdx + 1) % poly.vertices.Count; i = (i + 1) % poly.vertices.Count) { - bottomPart.Add(poly.vertices[i]); - } - bottomPart.Add(seg.finalStartVertex); - bottomPart.Add(seg.finalEndVertex); - newPolys.Add(bottomPart); - } - - obj.vertices.Add(seg.finalStartVertex); - obj.vertices.Add(seg.finalEndVertex); - - return SafeReplacePolys(seg, obj, poly, newPolys.ToArray()); - } - case Endpoint.V_F_F: { - // Fig 6.3 (g/h) - obj.vertices.Add(seg.finalEndVertex); - List topPart = new List(); - int topEndIdx = seg.end == Endpoint.VERTEX ? (seg.endVertIdx - 1 + poly.vertices.Count) - % poly.vertices.Count : seg.endVertIdx; - topPart.Add(seg.finalEndVertex); - for (int idx = seg.startVertIdx; idx != topEndIdx; idx = (idx + 1) % poly.vertices.Count) { - topPart.Add(poly.vertices[idx]); - } - topPart.Add(poly.vertices[topEndIdx]); - - List bottomPart = new List(); - bottomPart.Add(seg.finalEndVertex); - for (int idx = (seg.endVertIdx + 1) % poly.vertices.Count; idx != seg.startVertIdx; - idx = (idx + 1) % poly.vertices.Count) { - bottomPart.Add(poly.vertices[idx]); - } - bottomPart.Add(poly.vertices[seg.startVertIdx]); - - if (seg.end == Endpoint.VERTEX) { - // Fig 6.3 (g) - CsgVertex prevVert = poly.vertices[(seg.endVertIdx - 1 + poly.vertices.Count) % poly.vertices.Count]; - CsgVertex nextVert = poly.vertices[(seg.endVertIdx + 1) % poly.vertices.Count]; - - return SafeReplacePolys(seg, obj, poly, topPart, bottomPart, - new List() { seg.finalEndVertex, prevVert, seg.endVertex }, - new List() { seg.finalEndVertex, seg.endVertex, nextVert }); - } else { - // Fig 6.3 (h) - CsgVertex nextVert = poly.vertices[(seg.endVertIdx + 1) % poly.vertices.Count]; - return SafeReplacePolys(seg, obj, poly, topPart, bottomPart, - new List() { seg.finalEndVertex, poly.vertices[seg.endVertIdx], nextVert }); - } - } - case Endpoint.F_F_F: { - List> newPolys = new List>(); - - List interiorPoints = new List(); // may just be one point - interiorPoints.Add(seg.finalEndVertex); - if (Mathf.Abs(seg.finalEndVertex.loc.x - seg.finalStartVertex.loc.x) > CsgMath.EPSILON - || Mathf.Abs(seg.finalEndVertex.loc.y - seg.finalStartVertex.loc.y) > CsgMath.EPSILON - || Mathf.Abs(seg.finalEndVertex.loc.z - seg.finalStartVertex.loc.z) > CsgMath.EPSILON) { - interiorPoints.Add(seg.finalStartVertex); - } - - // begin part - newPolys.Add(new List() { + // bottom part goes until right before begin + List bottomPart = new List(); + for (int i = (seg.endVertIdx + 1) % poly.vertices.Count; i != seg.startVertIdx; i = (i + 1) % poly.vertices.Count) + { + bottomPart.Add(poly.vertices[i]); + } + bottomPart.Add(seg.finalStartVertex); + bottomPart.Add(seg.finalEndVertex); + newPolys.Add(bottomPart); + } + else + { + List bottomPart = new List(); + for (int i = (seg.endVertIdx + 1) % poly.vertices.Count; i != (seg.startVertIdx + 1) % poly.vertices.Count; i = (i + 1) % poly.vertices.Count) + { + bottomPart.Add(poly.vertices[i]); + } + bottomPart.Add(seg.finalStartVertex); + bottomPart.Add(seg.finalEndVertex); + newPolys.Add(bottomPart); + } + + obj.vertices.Add(seg.finalStartVertex); + obj.vertices.Add(seg.finalEndVertex); + + return SafeReplacePolys(seg, obj, poly, newPolys.ToArray()); + } + case Endpoint.V_F_F: + { + // Fig 6.3 (g/h) + obj.vertices.Add(seg.finalEndVertex); + List topPart = new List(); + int topEndIdx = seg.end == Endpoint.VERTEX ? (seg.endVertIdx - 1 + poly.vertices.Count) + % poly.vertices.Count : seg.endVertIdx; + topPart.Add(seg.finalEndVertex); + for (int idx = seg.startVertIdx; idx != topEndIdx; idx = (idx + 1) % poly.vertices.Count) + { + topPart.Add(poly.vertices[idx]); + } + topPart.Add(poly.vertices[topEndIdx]); + + List bottomPart = new List(); + bottomPart.Add(seg.finalEndVertex); + for (int idx = (seg.endVertIdx + 1) % poly.vertices.Count; idx != seg.startVertIdx; + idx = (idx + 1) % poly.vertices.Count) + { + bottomPart.Add(poly.vertices[idx]); + } + bottomPart.Add(poly.vertices[seg.startVertIdx]); + + if (seg.end == Endpoint.VERTEX) + { + // Fig 6.3 (g) + CsgVertex prevVert = poly.vertices[(seg.endVertIdx - 1 + poly.vertices.Count) % poly.vertices.Count]; + CsgVertex nextVert = poly.vertices[(seg.endVertIdx + 1) % poly.vertices.Count]; + + return SafeReplacePolys(seg, obj, poly, topPart, bottomPart, + new List() { seg.finalEndVertex, prevVert, seg.endVertex }, + new List() { seg.finalEndVertex, seg.endVertex, nextVert }); + } + else + { + // Fig 6.3 (h) + CsgVertex nextVert = poly.vertices[(seg.endVertIdx + 1) % poly.vertices.Count]; + return SafeReplacePolys(seg, obj, poly, topPart, bottomPart, + new List() { seg.finalEndVertex, poly.vertices[seg.endVertIdx], nextVert }); + } + } + case Endpoint.F_F_F: + { + List> newPolys = new List>(); + + List interiorPoints = new List(); // may just be one point + interiorPoints.Add(seg.finalEndVertex); + if (Mathf.Abs(seg.finalEndVertex.loc.x - seg.finalStartVertex.loc.x) > CsgMath.EPSILON + || Mathf.Abs(seg.finalEndVertex.loc.y - seg.finalStartVertex.loc.y) > CsgMath.EPSILON + || Mathf.Abs(seg.finalEndVertex.loc.z - seg.finalStartVertex.loc.z) > CsgMath.EPSILON) + { + interiorPoints.Add(seg.finalStartVertex); + } + + // begin part + newPolys.Add(new List() { poly.vertices[seg.startVertIdx], poly.vertices[(seg.startVertIdx + 1) % poly.vertices.Count], interiorPoints[interiorPoints.Count - 1], }); - if (seg.start == Endpoint.VERTEX) { - newPolys.Add(new List() { + if (seg.start == Endpoint.VERTEX) + { + newPolys.Add(new List() { poly.vertices[seg.startVertIdx], interiorPoints[interiorPoints.Count - 1], poly.vertices[(seg.startVertIdx - 1 + poly.vertices.Count) % poly.vertices.Count], }); - } - - // top part - List topPart = new List(); - int endIdx = seg.end == Endpoint.VERTEX ? seg.endVertIdx : (seg.endVertIdx + 1) % poly.vertices.Count; - for (int i = (seg.startVertIdx + 1) % poly.vertices.Count; i != endIdx; i = (i + 1) % poly.vertices.Count) { - topPart.Add(poly.vertices[i]); - } - topPart.AddRange(interiorPoints); - newPolys.Add(topPart); - - // end part - newPolys.Add(new List() { + } + + // top part + List topPart = new List(); + int endIdx = seg.end == Endpoint.VERTEX ? seg.endVertIdx : (seg.endVertIdx + 1) % poly.vertices.Count; + for (int i = (seg.startVertIdx + 1) % poly.vertices.Count; i != endIdx; i = (i + 1) % poly.vertices.Count) + { + topPart.Add(poly.vertices[i]); + } + topPart.AddRange(interiorPoints); + newPolys.Add(topPart); + + // end part + newPolys.Add(new List() { poly.vertices[seg.endVertIdx], poly.vertices[(seg.endVertIdx + 1) % poly.vertices.Count], interiorPoints[0], }); - if (seg.end == Endpoint.VERTEX) { - newPolys.Add(new List() { + if (seg.end == Endpoint.VERTEX) + { + newPolys.Add(new List() { poly.vertices[(seg.endVertIdx - 1 + poly.vertices.Count) % poly.vertices.Count], poly.vertices[seg.endVertIdx], interiorPoints[0], }); + } + + // bottom part + List bottomPart = new List(); + endIdx = seg.start == Endpoint.VERTEX ? seg.startVertIdx : (seg.startVertIdx + 1) % poly.vertices.Count; + for (int i = (seg.endVertIdx + 1) % poly.vertices.Count; i != endIdx; i = (i + 1) % poly.vertices.Count) + { + bottomPart.Add(poly.vertices[i]); + } + bottomPart.AddRange(interiorPoints.Reverse().ToList()); + newPolys.Add(bottomPart); + + foreach (CsgVertex interiorPoint in interiorPoints) + { + obj.vertices.Add(interiorPoint); + } + + return SafeReplacePolys(seg, obj, poly, newPolys.ToArray()); + } + default: + if (DEBUG) + { + Console.Write("Unimplemented split type: " + splitType); + List[] otherPolys = new List[2]; + otherPolys[0] = new List() { seg.finalStartVertex, seg.finalEndVertex }; + otherPolys[1] = new List() { seg.startVertex, seg.endVertex }; + DumpPolygonsForDebug(poly, otherPolys); + } + Debug.Log("!!!!!>>>>> Unhandled case: " + splitType); + break; } + return false; + } - // bottom part - List bottomPart = new List(); - endIdx = seg.start == Endpoint.VERTEX ? seg.startVertIdx : (seg.startVertIdx + 1) % poly.vertices.Count; - for (int i = (seg.endVertIdx + 1) % poly.vertices.Count; i != endIdx; i = (i + 1) % poly.vertices.Count) { - bottomPart.Add(poly.vertices[i]); - } - bottomPart.AddRange(interiorPoints.Reverse().ToList()); - newPolys.Add(bottomPart); - - foreach (CsgVertex interiorPoint in interiorPoints) { - obj.vertices.Add(interiorPoint); - } - - return SafeReplacePolys(seg, obj, poly, newPolys.ToArray()); - } - default: - if (DEBUG) { - Console.Write("Unimplemented split type: " + splitType); - List[] otherPolys = new List[2]; - otherPolys[0] = new List() { seg.finalStartVertex, seg.finalEndVertex }; - otherPolys[1] = new List() { seg.startVertex, seg.endVertex }; - DumpPolygonsForDebug(poly, otherPolys); - } - Debug.Log("!!!!!>>>>> Unhandled case: " + splitType); - break; - } - return false; - } + // Test helper, dumps info about a split to help generate a test case. + private static void DumpSplitInfo(CsgPolygon poly, SegmentDescriptor seg) + { + Console.Write("\nPoly:"); + foreach (CsgVertex vert in poly.vertices) + { + Console.Write(Str(vert)); + } + Console.Write("\nSeg:"); + Console.Write("descriptor.start = " + seg.start); + Console.Write("descriptor.middle = " + seg.middle); + Console.Write("descriptor.end = " + seg.end); + Console.Write("descriptor.finalStart = " + seg.finalStart); + Console.Write("descriptor.finalMiddle = " + seg.finalMiddle); + Console.Write("descriptor.finalEnd = " + seg.finalEnd); + Console.Write("descriptor.startVertex = " + Str(seg.startVertex)); + Console.Write("descriptor.endVertex = " + Str(seg.endVertex)); + Console.Write("descriptor.finalStartVertex = " + Str(seg.finalStartVertex)); + Console.Write("descriptor.finalEndVertex = " + Str(seg.finalEndVertex)); + Console.Write("descriptor.startVertIdx = " + seg.startVertIdx); + Console.Write("descriptor.endVertIdx = " + seg.endVertIdx); + } - // Test helper, dumps info about a split to help generate a test case. - private static void DumpSplitInfo(CsgPolygon poly, SegmentDescriptor seg) { - Console.Write("\nPoly:"); - foreach (CsgVertex vert in poly.vertices) { - Console.Write(Str(vert)); - } - Console.Write("\nSeg:"); - Console.Write("descriptor.start = " + seg.start); - Console.Write("descriptor.middle = " + seg.middle); - Console.Write("descriptor.end = " + seg.end); - Console.Write("descriptor.finalStart = " + seg.finalStart); - Console.Write("descriptor.finalMiddle = " + seg.finalMiddle); - Console.Write("descriptor.finalEnd = " + seg.finalEnd); - Console.Write("descriptor.startVertex = " + Str(seg.startVertex)); - Console.Write("descriptor.endVertex = " + Str(seg.endVertex)); - Console.Write("descriptor.finalStartVertex = " + Str(seg.finalStartVertex)); - Console.Write("descriptor.finalEndVertex = " + Str(seg.finalEndVertex)); - Console.Write("descriptor.startVertIdx = " + seg.startVertIdx); - Console.Write("descriptor.endVertIdx = " + seg.endVertIdx); - } + private static string Str(CsgVertex vert) + { + return vert.loc.x + "f, " + vert.loc.y + "f, " + vert.loc.z + "f"; + } - private static string Str(CsgVertex vert) { - return vert.loc.x + "f, " + vert.loc.y + "f, " + vert.loc.z + "f"; - } + // Replace the polygon in the object with its components. If the components have zero-area, then skip. If the + // result would be identical, don't bother doing anything. + // SegmentDescriptor param is only here for debugging. + private static bool SafeReplacePolys(SegmentDescriptor seg, CsgObject obj, CsgPolygon oldPoly, + params List[] newPolyVerts) + { + + List newPolys = new List(); + foreach (List verts in newPolyVerts) + { + newPolys.Add(new CsgPolygon(verts, oldPoly.faceProperties, oldPoly.plane.normal)); + } + + bool dumpPolygons = DEBUG; + if (VERIFY_POLY_SPLITS) + { + int numSplitEdges = 0; + numSplitEdges += seg.finalStart == Endpoint.EDGE ? 1 : 0; + numSplitEdges += seg.finalEnd == Endpoint.EDGE ? 1 : 0; + if (numSplitEdges == 2 && seg.finalMiddle == Endpoint.EDGE) + { + // Same edge, so only one edge is split. + numSplitEdges = 1; + } + if (!CsgUtil.IsValidPolygonSplit(oldPoly, newPolys, numSplitEdges)) + { + Console.Write("Invalid split for case: " + Endpoint.combine(seg.finalStart, seg.finalMiddle, seg.finalEnd)); + dumpPolygons = true; + } + } + + // Dump polygon and split info out for debugging. + if (dumpPolygons) + { + List[] otherPolys = new List[newPolyVerts.Length + 2]; + for (int i = 0; i < newPolyVerts.Length; i++) + { + otherPolys[i] = newPolyVerts[i]; + } + otherPolys[newPolyVerts.Length] = new List() { seg.finalStartVertex, seg.finalEndVertex }; + otherPolys[newPolyVerts.Length + 1] = new List() { seg.startVertex, seg.endVertex }; + DumpPolygonsForDebug(oldPoly, otherPolys); + DumpSplitInfo(oldPoly, seg); + } + + // If we only had one valid polygon, it will be the same as the original, so do nothing. + if (newPolys.Count > 1) + { + obj.polygons.Remove(oldPoly); + obj.polygons.AddRange(newPolys); + return true; + } - // Replace the polygon in the object with its components. If the components have zero-area, then skip. If the - // result would be identical, don't bother doing anything. - // SegmentDescriptor param is only here for debugging. - private static bool SafeReplacePolys(SegmentDescriptor seg, CsgObject obj, CsgPolygon oldPoly, - params List[] newPolyVerts) { - - List newPolys = new List(); - foreach (List verts in newPolyVerts) { - newPolys.Add(new CsgPolygon(verts, oldPoly.faceProperties, oldPoly.plane.normal)); - } - - bool dumpPolygons = DEBUG; - if (VERIFY_POLY_SPLITS) { - int numSplitEdges = 0; - numSplitEdges += seg.finalStart == Endpoint.EDGE ? 1 : 0; - numSplitEdges += seg.finalEnd == Endpoint.EDGE ? 1 : 0; - if (numSplitEdges == 2 && seg.finalMiddle == Endpoint.EDGE) { - // Same edge, so only one edge is split. - numSplitEdges = 1; + return false; } - if (!CsgUtil.IsValidPolygonSplit(oldPoly, newPolys, numSplitEdges)) { - Console.Write("Invalid split for case: " + Endpoint.combine(seg.finalStart, seg.finalMiddle, seg.finalEnd)); - dumpPolygons = true; + // Check if a polygon has a non-zero area and more than two vertices. + private static bool IsValidPolygon(List verts) + { + if (verts.Count < 3) + { + return false; + } + else if (verts.Count == 3) + { + // If the dot product is -1 or 1, it is a zero area triangle. + float dot = Vector3.Dot((verts[1].loc - verts[0].loc).normalized, (verts[2].loc - verts[0].loc).normalized); + return Mathf.Abs(1 - Mathf.Abs(dot)) > CsgMath.EPSILON; + } + else + { + // Not true in general, but true for the splits we perform. I think. + return true; + } } - } - // Dump polygon and split info out for debugging. - if (dumpPolygons) { - List[] otherPolys = new List[newPolyVerts.Length + 2]; - for (int i = 0; i < newPolyVerts.Length; i++) { - otherPolys[i] = newPolyVerts[i]; + // Helper method. Project the polygons on a plane and then write their 2d coords. + // Hacky: assumes last "polygon" is the split edge and labels it thusly + // (I always wanted to use 'thusly' in a code comment ;) + public static void DumpPolygonsForDebug(CsgPolygon oldPoly, params List[] polys) + { + Quaternion toPlane = Quaternion.Inverse(Quaternion.LookRotation(oldPoly.plane.normal, Vector3.right)); + Console.Write(""); + for (int i = -1; i < polys.Length; i++) + { + string name = "P" + (i + 1); + if (i == -1) + { + name = "Original"; + } + else if (i == (polys.Length - 2)) + { + name = "FinalSegment"; + } + else if (i == (polys.Length - 1)) + { + name = "Segment"; + } + List poly = i == -1 ? oldPoly.vertices : polys[i]; + foreach (CsgVertex vert in poly) + { + Vector3 projected = toPlane * vert.loc; + Console.Write(projected.x + ", " + projected.y + ", " + name); + } + } } - otherPolys[newPolyVerts.Length] = new List() { seg.finalStartVertex, seg.finalEndVertex }; - otherPolys[newPolyVerts.Length + 1] = new List() { seg.startVertex, seg.endVertex }; - DumpPolygonsForDebug(oldPoly, otherPolys); - DumpSplitInfo(oldPoly, seg); - } - - // If we only had one valid polygon, it will be the same as the original, so do nothing. - if (newPolys.Count > 1) { - obj.polygons.Remove(oldPoly); - obj.polygons.AddRange(newPolys); - return true; - } - - return false; - } - // Check if a polygon has a non-zero area and more than two vertices. - private static bool IsValidPolygon(List verts) { - if (verts.Count < 3) { - return false; - } else if (verts.Count == 3) { - // If the dot product is -1 or 1, it is a zero area triangle. - float dot = Vector3.Dot((verts[1].loc - verts[0].loc).normalized, (verts[2].loc - verts[0].loc).normalized); - return Mathf.Abs(1 - Mathf.Abs(dot)) > CsgMath.EPSILON; - } else { - // Not true in general, but true for the splits we perform. I think. - return true; - } - } - // Helper method. Project the polygons on a plane and then write their 2d coords. - // Hacky: assumes last "polygon" is the split edge and labels it thusly - // (I always wanted to use 'thusly' in a code comment ;) - public static void DumpPolygonsForDebug(CsgPolygon oldPoly, params List[] polys) { - Quaternion toPlane = Quaternion.Inverse(Quaternion.LookRotation(oldPoly.plane.normal, Vector3.right)); - Console.Write(""); - for (int i = -1; i < polys.Length; i++) { - string name = "P" + (i + 1); - if (i == -1) { - name = "Original"; - } else if (i == (polys.Length - 2)) { - name = "FinalSegment"; - } else if (i == (polys.Length - 1)) { - name = "Segment"; - } - List poly = i == -1 ? oldPoly.vertices : polys[i]; - foreach (CsgVertex vert in poly) { - Vector3 projected = toPlane * vert.loc; - Console.Write(projected.x + ", " + projected.y + ", " + name); - } - } - } + // Section 5: Calculate the segment descriptor for a given polygon and a line that splits it. + // Public for testing. + public static SegmentDescriptor CalcSegmentDescriptor(CsgContext ctx, + Vector3 linePt, Vector3 lineDir, float[] distToPlane, CsgPolygon poly) + { + SegmentDescriptor descriptor = new SegmentDescriptor(); + bool foundFirst = false; + bool foundSecond = false; + + for (int i = 0; i < distToPlane.Length; i++) + { + int j = (i + 1) % distToPlane.Length; + if (Math.Abs(distToPlane[i]) < CsgMath.EPSILON) + { + if (!foundFirst) + { + descriptor.startVertIdx = i; + descriptor.start = Endpoint.VERTEX; + descriptor.startDist = SignedDistance(linePt, lineDir, poly.vertices[i].loc); + descriptor.startVertex = poly.vertices[i]; + foundFirst = true; + } + else + { + descriptor.endVertIdx = i; + descriptor.end = Endpoint.VERTEX; + descriptor.endDist = SignedDistance(linePt, lineDir, poly.vertices[i].loc); + descriptor.endVertex = poly.vertices[i]; + foundSecond = true; + } + } + else if (Math.Abs(distToPlane[i]) > CsgMath.EPSILON && Math.Abs(distToPlane[j]) > CsgMath.EPSILON && Mathf.Sign(distToPlane[i]) != Mathf.Sign(distToPlane[j])) + { + // Crosses plane. + float t = distToPlane[i] / (distToPlane[i] - distToPlane[j]); + Vector3 midPoint = Vector3.Lerp(poly.vertices[i].loc, poly.vertices[j].loc, t); + // Project back onto our plane: + float dist = poly.plane.GetDistanceToPoint(midPoint); + midPoint -= (dist * poly.plane.normal); + if (!foundFirst) + { + descriptor.startVertIdx = i; + descriptor.start = Endpoint.EDGE; + descriptor.startDist = SignedDistance(linePt, lineDir, midPoint); + descriptor.startVertex = ctx.CreateOrGetVertexAt(midPoint); + descriptor.startVertex.status = VertexStatus.BOUNDARY; + foundFirst = true; + } + else + { + descriptor.endVertIdx = i; + descriptor.end = Endpoint.EDGE; + descriptor.endDist = SignedDistance(linePt, lineDir, midPoint); + descriptor.endVertex = ctx.CreateOrGetVertexAt(midPoint); + descriptor.endVertex.status = VertexStatus.BOUNDARY; + foundSecond = true; + } + } + } + + if (!foundFirst) + { + return null; + } + + if (!foundSecond) + { + descriptor.end = descriptor.start; + descriptor.endDist = descriptor.startDist; + descriptor.endVertIdx = descriptor.startVertIdx; + descriptor.endVertex = descriptor.startVertex; + } + + // Put the start and end in order of distance from linePt. + if (descriptor.startDist > descriptor.endDist) + { + int vertIdSave = descriptor.startVertIdx; + float distSave = descriptor.startDist; + int typeSave = descriptor.start; + CsgVertex vertSave = descriptor.startVertex; + + descriptor.startVertIdx = descriptor.endVertIdx; + descriptor.startDist = descriptor.endDist; + descriptor.start = descriptor.end; + descriptor.startVertex = descriptor.endVertex; + + descriptor.endVertIdx = vertIdSave; + descriptor.endDist = distSave; + descriptor.end = typeSave; + descriptor.endVertex = vertSave; + } - // Section 5: Calculate the segment descriptor for a given polygon and a line that splits it. - // Public for testing. - public static SegmentDescriptor CalcSegmentDescriptor(CsgContext ctx, - Vector3 linePt, Vector3 lineDir, float[] distToPlane, CsgPolygon poly) { - SegmentDescriptor descriptor = new SegmentDescriptor(); - bool foundFirst = false; - bool foundSecond = false; - - for (int i = 0; i < distToPlane.Length; i++) { - int j = (i + 1) % distToPlane.Length; - if (Math.Abs(distToPlane[i]) < CsgMath.EPSILON) { - if (!foundFirst) { - descriptor.startVertIdx = i; - descriptor.start = Endpoint.VERTEX; - descriptor.startDist = SignedDistance(linePt, lineDir, poly.vertices[i].loc); - descriptor.startVertex = poly.vertices[i]; - foundFirst = true; - } else { - descriptor.endVertIdx = i; - descriptor.end = Endpoint.VERTEX; - descriptor.endDist = SignedDistance(linePt, lineDir, poly.vertices[i].loc); - descriptor.endVertex = poly.vertices[i]; - foundSecond = true; - } - } else if (Math.Abs(distToPlane[i]) > CsgMath.EPSILON && Math.Abs(distToPlane[j]) > CsgMath.EPSILON && Mathf.Sign(distToPlane[i]) != Mathf.Sign(distToPlane[j])) { - // Crosses plane. - float t = distToPlane[i] / (distToPlane[i] - distToPlane[j]); - Vector3 midPoint = Vector3.Lerp(poly.vertices[i].loc, poly.vertices[j].loc, t); - // Project back onto our plane: - float dist = poly.plane.GetDistanceToPoint(midPoint); - midPoint -= (dist * poly.plane.normal); - if (!foundFirst) { - descriptor.startVertIdx = i; - descriptor.start = Endpoint.EDGE; - descriptor.startDist = SignedDistance(linePt, lineDir, midPoint); - descriptor.startVertex = ctx.CreateOrGetVertexAt(midPoint); + if (descriptor.startVertIdx == descriptor.endVertIdx) + { + descriptor.middle = Endpoint.VERTEX; + } + else if (descriptor.start == Endpoint.VERTEX && descriptor.end == Endpoint.VERTEX + && (descriptor.startVertIdx == (descriptor.endVertIdx + 1) % distToPlane.Length + || (descriptor.startVertIdx + 1) % distToPlane.Length == descriptor.endVertIdx)) + { + descriptor.middle = Endpoint.EDGE; + } + else + { + descriptor.middle = Endpoint.FACE; + } + + // Mark endpoints as boundary. descriptor.startVertex.status = VertexStatus.BOUNDARY; - foundFirst = true; - } else { - descriptor.endVertIdx = i; - descriptor.end = Endpoint.EDGE; - descriptor.endDist = SignedDistance(linePt, lineDir, midPoint); - descriptor.endVertex = ctx.CreateOrGetVertexAt(midPoint); descriptor.endVertex.status = VertexStatus.BOUNDARY; - foundSecond = true; - } - } - } - - if (!foundFirst) { - return null; - } - - if (!foundSecond) { - descriptor.end = descriptor.start; - descriptor.endDist = descriptor.startDist; - descriptor.endVertIdx = descriptor.startVertIdx; - descriptor.endVertex = descriptor.startVertex; - } - - // Put the start and end in order of distance from linePt. - if (descriptor.startDist > descriptor.endDist) { - int vertIdSave = descriptor.startVertIdx; - float distSave = descriptor.startDist; - int typeSave = descriptor.start; - CsgVertex vertSave = descriptor.startVertex; - - descriptor.startVertIdx = descriptor.endVertIdx; - descriptor.startDist = descriptor.endDist; - descriptor.start = descriptor.end; - descriptor.startVertex = descriptor.endVertex; - - descriptor.endVertIdx = vertIdSave; - descriptor.endDist = distSave; - descriptor.end = typeSave; - descriptor.endVertex = vertSave; - } - - if (descriptor.startVertIdx == descriptor.endVertIdx) { - descriptor.middle = Endpoint.VERTEX; - } else if (descriptor.start == Endpoint.VERTEX && descriptor.end == Endpoint.VERTEX - && (descriptor.startVertIdx == (descriptor.endVertIdx +1) % distToPlane.Length - || (descriptor.startVertIdx + 1) % distToPlane.Length == descriptor.endVertIdx)) { - descriptor.middle = Endpoint.EDGE; - } else { - descriptor.middle = Endpoint.FACE; - } - - // Mark endpoints as boundary. - descriptor.startVertex.status = VertexStatus.BOUNDARY; - descriptor.endVertex.status = VertexStatus.BOUNDARY; - - return descriptor; - } - // Swap the endpoint descriptors. - private static void Swap(SegmentDescriptor descriptor) { - int vertIdSave = descriptor.startVertIdx; - float distSave = descriptor.startDist; - int typeSave = descriptor.start; - int finalTypeSave = descriptor.finalStart; - CsgVertex vertSave = descriptor.startVertex; - CsgVertex finalVertSave = descriptor.finalStartVertex; - - descriptor.startVertIdx = descriptor.endVertIdx; - descriptor.startDist = descriptor.endDist; - descriptor.start = descriptor.end; - descriptor.finalStart = descriptor.finalEnd; - descriptor.startVertex = descriptor.endVertex; - descriptor.finalStartVertex = descriptor.finalEndVertex; - - descriptor.endVertIdx = vertIdSave; - descriptor.endDist = distSave; - descriptor.end = typeSave; - descriptor.finalEnd = finalTypeSave; - descriptor.endVertex = vertSave; - descriptor.finalEndVertex = finalVertSave; - } + return descriptor; + } - // Get the signed distance from a ray to a point. - private static float SignedDistance(Vector3 rayStart, Vector3 rayNormal, Vector3 point) { - float d = Vector3.Distance(rayStart, point); - if (Vector3.Dot(rayNormal, point - rayStart) < 0) { - return -d; - } else { - return d; - } - } + // Swap the endpoint descriptors. + private static void Swap(SegmentDescriptor descriptor) + { + int vertIdSave = descriptor.startVertIdx; + float distSave = descriptor.startDist; + int typeSave = descriptor.start; + int finalTypeSave = descriptor.finalStart; + CsgVertex vertSave = descriptor.startVertex; + CsgVertex finalVertSave = descriptor.finalStartVertex; + + descriptor.startVertIdx = descriptor.endVertIdx; + descriptor.startDist = descriptor.endDist; + descriptor.start = descriptor.end; + descriptor.finalStart = descriptor.finalEnd; + descriptor.startVertex = descriptor.endVertex; + descriptor.finalStartVertex = descriptor.finalEndVertex; + + descriptor.endVertIdx = vertIdSave; + descriptor.endDist = distSave; + descriptor.end = typeSave; + descriptor.finalEnd = finalTypeSave; + descriptor.endVertex = vertSave; + descriptor.finalEndVertex = finalVertSave; + } - // Calculate the ray that is the intersection of two planes. - private static bool PlanePlaneIntersection( - out Vector3 rayStart, out Vector3 rayNormal, Plane plane1, Plane plane2) { - rayStart = Vector3.zero; - rayNormal = Vector3.Cross(plane1.normal, plane2.normal); - Vector3 ldir = Vector3.Cross(plane2.normal, rayNormal); - - float denominator = Vector3.Dot(plane1.normal, ldir); - - if (Mathf.Abs(denominator) > CsgMath.EPSILON) { - Vector3 plane1Position = CsgMath.PointOnPlane(plane1); - Vector3 plane2Position = CsgMath.PointOnPlane(plane2); - Vector3 plane1ToPlane2 = plane1Position - plane2Position; - float t = Vector3.Dot(plane1.normal, plane1ToPlane2) / denominator; - rayStart = plane2Position + t * ldir; - return true; - } else { - return false; - } - } + // Get the signed distance from a ray to a point. + private static float SignedDistance(Vector3 rayStart, Vector3 rayNormal, Vector3 point) + { + float d = Vector3.Distance(rayStart, point); + if (Vector3.Dot(rayNormal, point - rayStart) < 0) + { + return -d; + } + else + { + return d; + } + } - // Given the signed distances for a list of points to a plane, does the polygon cross the plane? - // It has if some points are positive and some are negative. Also considered true if some points - // are *on* the plane and others are not. - private static bool CrossesPlane(float[] dists) { - bool hasAbove = false; - bool hasBelow = false; - bool hasOn = false; - foreach (float dist in dists) { - if (dist < 0) { - hasBelow = true; - } else if (dist > 0) { - hasAbove = true; - } else { - hasOn = true; + // Calculate the ray that is the intersection of two planes. + private static bool PlanePlaneIntersection( + out Vector3 rayStart, out Vector3 rayNormal, Plane plane1, Plane plane2) + { + rayStart = Vector3.zero; + rayNormal = Vector3.Cross(plane1.normal, plane2.normal); + Vector3 ldir = Vector3.Cross(plane2.normal, rayNormal); + + float denominator = Vector3.Dot(plane1.normal, ldir); + + if (Mathf.Abs(denominator) > CsgMath.EPSILON) + { + Vector3 plane1Position = CsgMath.PointOnPlane(plane1); + Vector3 plane2Position = CsgMath.PointOnPlane(plane2); + Vector3 plane1ToPlane2 = plane1Position - plane2Position; + float t = Vector3.Dot(plane1.normal, plane1ToPlane2) / denominator; + rayStart = plane2Position + t * ldir; + return true; + } + else + { + return false; + } } - } - int count = 0; - count += hasAbove ? 1 : 0; - count += hasBelow ? 1 : 0; - count += hasOn ? 1 : 0; - return count > 1; - } + // Given the signed distances for a list of points to a plane, does the polygon cross the plane? + // It has if some points are positive and some are negative. Also considered true if some points + // are *on* the plane and others are not. + private static bool CrossesPlane(float[] dists) + { + bool hasAbove = false; + bool hasBelow = false; + bool hasOn = false; + foreach (float dist in dists) + { + if (dist < 0) + { + hasBelow = true; + } + else if (dist > 0) + { + hasAbove = true; + } + else + { + hasOn = true; + } + } + + int count = 0; + count += hasAbove ? 1 : 0; + count += hasBelow ? 1 : 0; + count += hasOn ? 1 : 0; + return count > 1; + } - // Given a polygon, find the distance from each of its vertices to a given plane. - private static float[] DistanceFromVertsToPlane(CsgPolygon poly, Plane plane) { - float[] dists = new float[poly.vertices.Count]; - for (int i = 0; i < poly.vertices.Count; i++) { - float dist = plane.GetDistanceToPoint(poly.vertices[i].loc); - dists[i] = Mathf.Abs(dist) < CsgMath.EPSILON ? 0 : dist; - } - return dists; + // Given a polygon, find the distance from each of its vertices to a given plane. + private static float[] DistanceFromVertsToPlane(CsgPolygon poly, Plane plane) + { + float[] dists = new float[poly.vertices.Count]; + for (int i = 0; i < poly.vertices.Count; i++) + { + float dist = plane.GetDistanceToPoint(poly.vertices[i].loc); + dists[i] = Mathf.Abs(dist) < CsgMath.EPSILON ? 0 : dist; + } + return dists; + } } - } } diff --git a/Assets/Scripts/model/csg/SolidVertex.cs b/Assets/Scripts/model/csg/SolidVertex.cs index ab1d078a..ee21db2a 100644 --- a/Assets/Scripts/model/csg/SolidVertex.cs +++ b/Assets/Scripts/model/csg/SolidVertex.cs @@ -16,27 +16,32 @@ using System.Collections.Generic; using UnityEngine; -namespace com.google.apps.peltzer.client.model.csg { - public struct SolidVertex { - public int vertexId { get; private set; } - internal Vector3 position; - internal Vector3 normal; +namespace com.google.apps.peltzer.client.model.csg +{ + public struct SolidVertex + { + public int vertexId { get; private set; } + internal Vector3 position; + internal Vector3 normal; - public SolidVertex(int vertexId, Vector3 pos, Vector3 norm) { - this.vertexId = vertexId; - this.position = pos; - this.normal = norm; - } + public SolidVertex(int vertexId, Vector3 pos, Vector3 norm) + { + this.vertexId = vertexId; + this.position = pos; + this.normal = norm; + } - public SolidVertex Flip() { - return new SolidVertex(vertexId, position, -normal); - } + public SolidVertex Flip() + { + return new SolidVertex(vertexId, position, -normal); + } - public SolidVertex Interpolate(int vertexId, SolidVertex other, float t) { - return new SolidVertex( - vertexId, - Vector3.Lerp(position, other.position, t), - Vector3.Lerp(normal, other.normal, t).normalized); + public SolidVertex Interpolate(int vertexId, SolidVertex other, float t) + { + return new SolidVertex( + vertexId, + Vector3.Lerp(position, other.position, t), + Vector3.Lerp(normal, other.normal, t).normalized); + } } - } } diff --git a/Assets/Scripts/model/export/AutoSave.cs b/Assets/Scripts/model/export/AutoSave.cs index 21484002..ec118554 100644 --- a/Assets/Scripts/model/export/AutoSave.cs +++ b/Assets/Scripts/model/export/AutoSave.cs @@ -23,164 +23,192 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.serialization; -namespace com.google.apps.peltzer.client.model.export { - public class AutoSave { - // The file pattern of autosave directories and filename-bases. Unique filenames have second-level granularity, - // but in case of a collision (which would be rare, as it would require two commands in the same second *and* - // the first auto-save to complete within that second), we just overwrite. - private static readonly string AUTO_SAVE_PATTERN = "Autosave_{0:yyyy-MM-dd_HH-mm-ss}"; - // The start of the auto-save pattern, such that we can cheaply check if a file looks like an auto-save file. - private static readonly string AUTO_SAVE_START = AUTO_SAVE_PATTERN.Substring(0, AUTO_SAVE_PATTERN.IndexOf("{")); - // The number of auto-saves we will keep at any given time. The more we have, the better our chances at - // least one is not corrupt. - private static readonly int AUTO_SAVE_FILE_COUNT = 5; - // A path to the user's Poly models auto-save data folder. - private string autoSavePath; - - // The directory of the current (next) autosave. - public string CurrentAutoSaveDirectory { get; private set; } - // The base of the filename of the current (next) autosave. - public string CurrentAutoSaveFilenameBase { get; private set; } - - // The last data that was auto-saved. - public SaveData MostRecentAutoSaveData { get; private set; } - - // Whether a save is currently in progress. We won't try and perform two saves at the same time. - public bool IsCurrentlySaving; - - // The model. - private Model model; - - /// - /// Creates a new AutoSave, with the given model. Creates the Auto Save directory, and any parents, if needed. - /// - public AutoSave(Model model, string modelsPath) { - this.model = model; +namespace com.google.apps.peltzer.client.model.export +{ + public class AutoSave + { + // The file pattern of autosave directories and filename-bases. Unique filenames have second-level granularity, + // but in case of a collision (which would be rare, as it would require two commands in the same second *and* + // the first auto-save to complete within that second), we just overwrite. + private static readonly string AUTO_SAVE_PATTERN = "Autosave_{0:yyyy-MM-dd_HH-mm-ss}"; + // The start of the auto-save pattern, such that we can cheaply check if a file looks like an auto-save file. + private static readonly string AUTO_SAVE_START = AUTO_SAVE_PATTERN.Substring(0, AUTO_SAVE_PATTERN.IndexOf("{")); + // The number of auto-saves we will keep at any given time. The more we have, the better our chances at + // least one is not corrupt. + private static readonly int AUTO_SAVE_FILE_COUNT = 5; + // A path to the user's Poly models auto-save data folder. + private string autoSavePath; + + // The directory of the current (next) autosave. + public string CurrentAutoSaveDirectory { get; private set; } + // The base of the filename of the current (next) autosave. + public string CurrentAutoSaveFilenameBase { get; private set; } + + // The last data that was auto-saved. + public SaveData MostRecentAutoSaveData { get; private set; } + + // Whether a save is currently in progress. We won't try and perform two saves at the same time. + public bool IsCurrentlySaving; + + // The model. + private Model model; + + /// + /// Creates a new AutoSave, with the given model. Creates the Auto Save directory, and any parents, if needed. + /// + public AutoSave(Model model, string modelsPath) + { + this.model = model; + + autoSavePath = Path.Combine(modelsPath, "Autosave"); + if (!Directory.Exists(autoSavePath)) + { + Directory.CreateDirectory(autoSavePath); + } + } - autoSavePath = Path.Combine(modelsPath, "Autosave"); - if (!Directory.Exists(autoSavePath)) { - Directory.CreateDirectory(autoSavePath); - } - } + /// + /// Gets a list of all auto-save directories. + /// + private IEnumerable GetAutoSaveDirectories() + { + return new DirectoryInfo(autoSavePath).GetDirectories() + .Where(x => x.Name.StartsWith(AUTO_SAVE_START)); + } - /// - /// Gets a list of all auto-save directories. - /// - private IEnumerable GetAutoSaveDirectories() { - return new DirectoryInfo(autoSavePath).GetDirectories() - .Where(x => x.Name.StartsWith(AUTO_SAVE_START)); - } + /// + /// Deletes the oldest auto-saves such that a maximum of AUTO_SAVE_FILE_COUNT remain. + /// + private void DeletePreviousSaves() + { + try + { + // Least recent first. + DirectoryInfo[] autoSaves = GetAutoSaveDirectories().OrderBy(x => x.LastWriteTimeUtc).ToArray(); + if (autoSaves.Length > AUTO_SAVE_FILE_COUNT) + { + for (int i = autoSaves.Length - AUTO_SAVE_FILE_COUNT; i >= 0; i--) + { + Directory.Delete(autoSaves[i].FullName, /* recursive */ true); + } + } + } + catch (Exception exception) + { + Debug.LogWarningFormat("Error deleting previous saves: {0}\n{1}", exception.Message, exception.StackTrace); + } + } - /// - /// Deletes the oldest auto-saves such that a maximum of AUTO_SAVE_FILE_COUNT remain. - /// - private void DeletePreviousSaves() { - try { - // Least recent first. - DirectoryInfo[] autoSaves = GetAutoSaveDirectories().OrderBy(x => x.LastWriteTimeUtc).ToArray(); - if (autoSaves.Length > AUTO_SAVE_FILE_COUNT) { - for (int i = autoSaves.Length - AUTO_SAVE_FILE_COUNT; i >= 0; i--) { - Directory.Delete(autoSaves[i].FullName, /* recursive */ true); - } + /// + /// Writes an auto-save to the user's local disk. Then, cleans up if we have too many auto-saves. + /// + /// A struct containing all the binary data for the auto-save + public void WriteAutoSave(SaveData saveData) + { + if (ExportUtils.SaveLocally(saveData, CurrentAutoSaveDirectory)) + { + MostRecentAutoSaveData = saveData; + DeletePreviousSaves(); + } } - } catch (Exception exception) { - Debug.LogWarningFormat("Error deleting previous saves: {0}\n{1}", exception.Message, exception.StackTrace); - } - } - /// - /// Writes an auto-save to the user's local disk. Then, cleans up if we have too many auto-saves. - /// - /// A struct containing all the binary data for the auto-save - public void WriteAutoSave(SaveData saveData) { - if (ExportUtils.SaveLocally(saveData, CurrentAutoSaveDirectory)) { - MostRecentAutoSaveData = saveData; - DeletePreviousSaves(); - } - } + /// + /// Returns whether at least one auto-save directory exists. + /// + public bool AutoSaveDirectoryExists() + { + return GetAutoSaveDirectories().Count() > 0; + } - /// - /// Returns whether at least one auto-save directory exists. - /// - public bool AutoSaveDirectoryExists() { - return GetAutoSaveDirectories().Count() > 0; - } + /// + /// Updates the current directory and filename base for the current (next) auto-save. + /// + public void UpdateCurrentAutoSavePath() + { + CurrentAutoSaveFilenameBase = string.Format(AUTO_SAVE_PATTERN, DateTime.Now); + CurrentAutoSaveDirectory = Path.Combine(autoSavePath, CurrentAutoSaveFilenameBase); + } - /// - /// Updates the current directory and filename base for the current (next) auto-save. - /// - public void UpdateCurrentAutoSavePath() { - CurrentAutoSaveFilenameBase = string.Format(AUTO_SAVE_PATTERN, DateTime.Now); - CurrentAutoSaveDirectory = Path.Combine(autoSavePath, CurrentAutoSaveFilenameBase); + /// + /// Loads the PeltzerFile for the most-recent auto-save (or returns 'false' and sets the peltzerFile to null). + /// + /// The PeltzerFile of the most-recent auto-save, or null on failure + /// If the file could be loaded + public bool LoadMostRecentAutoSave(out PeltzerFile peltzerFile) + { + // Most recent first. + IEnumerable autoSaveDirectories = GetAutoSaveDirectories(). + OrderByDescending(x => x.LastWriteTimeUtc); + if (autoSaveDirectories.Count() == 0) + { + Debug.Log("No autosave directories found"); + peltzerFile = null; + return false; + } + FileInfo[] autoSaveFile = autoSaveDirectories.First().GetFiles("*.poly"); + if (autoSaveFile.Count() == 0) + { + Debug.Log("No .poly file found in autosave directory"); + peltzerFile = null; + return false; + } + if (PeltzerFileHandler.PeltzerFileFromBytes(File.ReadAllBytes(autoSaveFile[0].FullName), out peltzerFile)) + { + return true; + } + else + { + // TODO(bug): Deal with corrupt files? Perhaps by iterating over directories until a + // non -failing case is found? + Debug.Log("Latest .poly file was corrupt"); + peltzerFile = null; + return false; + } + } } /// - /// Loads the PeltzerFile for the most-recent auto-save (or returns 'false' and sets the peltzerFile to null). + /// BackgroundWork for serializing a model into bytes (for saving). /// - /// The PeltzerFile of the most-recent auto-save, or null on failure - /// If the file could be loaded - public bool LoadMostRecentAutoSave(out PeltzerFile peltzerFile) { - // Most recent first. - IEnumerable autoSaveDirectories = GetAutoSaveDirectories(). - OrderByDescending(x => x.LastWriteTimeUtc); - if (autoSaveDirectories.Count() == 0) { - Debug.Log("No autosave directories found"); - peltzerFile = null; - return false; - } - FileInfo[] autoSaveFile = autoSaveDirectories.First().GetFiles("*.poly"); - if (autoSaveFile.Count() == 0) { - Debug.Log("No .poly file found in autosave directory"); - peltzerFile = null; - return false; - } - if (PeltzerFileHandler.PeltzerFileFromBytes(File.ReadAllBytes(autoSaveFile[0].FullName), out peltzerFile)) { - return true; - } else { - // TODO(bug): Deal with corrupt files? Perhaps by iterating over directories until a - // non -failing case is found? - Debug.Log("Latest .poly file was corrupt"); - peltzerFile = null; - return false; - } - } - } - - /// - /// BackgroundWork for serializing a model into bytes (for saving). - /// - public class AutoSaveWork : BackgroundWork { - private readonly AutoSave autoSave; - private readonly Model model; - private SaveData saveData; - private MeshRepresentationCache meshRepresentationCache; - private PolySerializer serializer; - - public AutoSaveWork(Model model, MeshRepresentationCache meshRepresentationCache, AutoSave autoSave, - PolySerializer serializer) { - this.model = model; - this.meshRepresentationCache = meshRepresentationCache; - this.autoSave = autoSave; - this.serializer = serializer; - } + public class AutoSaveWork : BackgroundWork + { + private readonly AutoSave autoSave; + private readonly Model model; + private SaveData saveData; + private MeshRepresentationCache meshRepresentationCache; + private PolySerializer serializer; + + public AutoSaveWork(Model model, MeshRepresentationCache meshRepresentationCache, AutoSave autoSave, + PolySerializer serializer) + { + this.model = model; + this.meshRepresentationCache = meshRepresentationCache; + this.autoSave = autoSave; + this.serializer = serializer; + } - public void BackgroundWork() { - // We expect autosave may fail (due to the mesh being modified as autosave reads it), which is - // acceptable -- it's not terrible to miss one autosave. See bug for details. - try { - autoSave.UpdateCurrentAutoSavePath(); - saveData = ExportUtils.SerializeModel(model, model.GetAllMeshes(), - /* saveGltf */ false, /* saveFbx */ false, /* saveTriangulatedObj */ false, - /* includeDisplayRotation */ false, serializer, saveSelected:false); - autoSave.WriteAutoSave(saveData); - } catch (Exception e) { - Debug.LogWarning(e); - PeltzerMain.Instance.LastAutoSaveDenied = true; - } - PeltzerMain.Instance.autoSave.IsCurrentlySaving = false; - } + public void BackgroundWork() + { + // We expect autosave may fail (due to the mesh being modified as autosave reads it), which is + // acceptable -- it's not terrible to miss one autosave. See bug for details. + try + { + autoSave.UpdateCurrentAutoSavePath(); + saveData = ExportUtils.SerializeModel(model, model.GetAllMeshes(), + /* saveGltf */ false, /* saveFbx */ false, /* saveTriangulatedObj */ false, + /* includeDisplayRotation */ false, serializer, saveSelected: false); + autoSave.WriteAutoSave(saveData); + } + catch (Exception e) + { + Debug.LogWarning(e); + PeltzerMain.Instance.LastAutoSaveDenied = true; + } + PeltzerMain.Instance.autoSave.IsCurrentlySaving = false; + } - public void PostWork() { + public void PostWork() + { + } } - } } diff --git a/Assets/Scripts/model/export/AutoThumbnailCamera.cs b/Assets/Scripts/model/export/AutoThumbnailCamera.cs index 365c7180..bab17399 100644 --- a/Assets/Scripts/model/export/AutoThumbnailCamera.cs +++ b/Assets/Scripts/model/export/AutoThumbnailCamera.cs @@ -17,96 +17,101 @@ using System.Collections; using UnityEngine; -namespace com.google.apps.peltzer.client.model.export { - /// - /// A camera script that auto-positions itself with a good view of a model, takes a picture of the model, - /// and returns it as the bytes of a PNG file. - /// - public class AutoThumbnailCamera : MonoBehaviour { - // Multiplier for the distance to move the camera. - private const float DISTANCE_SCALER = 2.5f; - - // Field of view of the camera (a too-large value might prevent small models from showing up). - private const float FIELD_OF_VIEW = 60.0f; - - // Background color for the thumbnail image. - private static readonly Color THUMBNAIL_BACKGROUND_COLOR = new Color(0.93f, 0.93f, 0.93f); - - private Camera thumbnailCamera; - - /// - /// Turn off the camera by default. - /// - void Start() { - thumbnailCamera = GetComponent(); - thumbnailCamera.fieldOfView = FIELD_OF_VIEW; - thumbnailCamera.clearFlags = CameraClearFlags.SolidColor; - thumbnailCamera.backgroundColor = THUMBNAIL_BACKGROUND_COLOR; - thumbnailCamera.gameObject.SetActive(false); - } - - /// - /// Positions ThumbnailCamera to get an appropriate view of the model. - /// - void PositionCamera() { - // Reposition camera to view the complete bounding box of the model. - Bounds modelBounds = PeltzerMain.Instance.model.FindBoundsOfAllMeshes(); - modelBounds.center = PeltzerMain.Instance.worldSpace.ModelToWorld(modelBounds.center); - modelBounds.size = PeltzerMain.Instance.worldSpace.scale * modelBounds.size; - - float distance = Mathf.Max(modelBounds.size.x, modelBounds.size.y, modelBounds.size.z); - distance /= (2.0f * Mathf.Tan(0.5f * thumbnailCamera.fieldOfView * Mathf.Deg2Rad)); - thumbnailCamera.transform.position = modelBounds.center - distance * Vector3.forward * DISTANCE_SCALER; - - // Look towards the center of the model. - Vector3 relativePos = modelBounds.center - transform.position; - Quaternion rotation = Quaternion.LookRotation(relativePos); - thumbnailCamera.transform.rotation = rotation; - } - +namespace com.google.apps.peltzer.client.model.export +{ /// - /// Takes a screenshot at the end of the current frame (such that everything on LateUpdate has been drawn), - /// and then calls a callback with the PNG bytes of the screenshot/ + /// A camera script that auto-positions itself with a good view of a model, takes a picture of the model, + /// and returns it as the bytes of a PNG file. /// - public IEnumerator TakeScreenShot(Action callback) { - // Wait. - yield return new WaitForEndOfFrame(); - - // Disable the environment and terrain so we don't get them on the screenshot. - GameObject envObj = ObjectFinder.ObjectById("ID_Environment"); - GameObject terrain = ObjectFinder.ObjectById("ID_TerrainLift"); - envObj.SetActive(false); - terrain.SetActive(false); - - // Activate the camera, and render to a texture. Zandria requires a 576x432px image per bug. - thumbnailCamera.gameObject.SetActive(true); - PositionCamera(); - - RenderTexture renderTexture = new RenderTexture(576, 432, 24); - thumbnailCamera.targetTexture = renderTexture; - - // Placeholder to save and then later restore whatever is the current active RenderTexture. This is - // necessary to make sure the view of the thumbnailCamera is rendered and not main camera, which is - // what RenderTexture.active defaults to. - RenderTexture activeRender = RenderTexture.active; - RenderTexture.active = thumbnailCamera.targetTexture; - thumbnailCamera.Render(); - - // Save to an image. - Texture2D imageOverview = new Texture2D(thumbnailCamera.targetTexture.width, thumbnailCamera.targetTexture.height, TextureFormat.RGB24, false); - imageOverview.ReadPixels(new Rect(0, 0, thumbnailCamera.targetTexture.width, thumbnailCamera.targetTexture.height), 0, 0); - imageOverview.Apply(); - byte[] bytes = imageOverview.EncodeToPNG(); - - // Deactivate the camera again. - thumbnailCamera.gameObject.SetActive(false); - RenderTexture.active = activeRender; - - // Re-enable the environment and terrain. - envObj.SetActive(true); - - // Encode texture into PNG and callback. - callback(bytes); + public class AutoThumbnailCamera : MonoBehaviour + { + // Multiplier for the distance to move the camera. + private const float DISTANCE_SCALER = 2.5f; + + // Field of view of the camera (a too-large value might prevent small models from showing up). + private const float FIELD_OF_VIEW = 60.0f; + + // Background color for the thumbnail image. + private static readonly Color THUMBNAIL_BACKGROUND_COLOR = new Color(0.93f, 0.93f, 0.93f); + + private Camera thumbnailCamera; + + /// + /// Turn off the camera by default. + /// + void Start() + { + thumbnailCamera = GetComponent(); + thumbnailCamera.fieldOfView = FIELD_OF_VIEW; + thumbnailCamera.clearFlags = CameraClearFlags.SolidColor; + thumbnailCamera.backgroundColor = THUMBNAIL_BACKGROUND_COLOR; + thumbnailCamera.gameObject.SetActive(false); + } + + /// + /// Positions ThumbnailCamera to get an appropriate view of the model. + /// + void PositionCamera() + { + // Reposition camera to view the complete bounding box of the model. + Bounds modelBounds = PeltzerMain.Instance.model.FindBoundsOfAllMeshes(); + modelBounds.center = PeltzerMain.Instance.worldSpace.ModelToWorld(modelBounds.center); + modelBounds.size = PeltzerMain.Instance.worldSpace.scale * modelBounds.size; + + float distance = Mathf.Max(modelBounds.size.x, modelBounds.size.y, modelBounds.size.z); + distance /= (2.0f * Mathf.Tan(0.5f * thumbnailCamera.fieldOfView * Mathf.Deg2Rad)); + thumbnailCamera.transform.position = modelBounds.center - distance * Vector3.forward * DISTANCE_SCALER; + + // Look towards the center of the model. + Vector3 relativePos = modelBounds.center - transform.position; + Quaternion rotation = Quaternion.LookRotation(relativePos); + thumbnailCamera.transform.rotation = rotation; + } + + /// + /// Takes a screenshot at the end of the current frame (such that everything on LateUpdate has been drawn), + /// and then calls a callback with the PNG bytes of the screenshot/ + /// + public IEnumerator TakeScreenShot(Action callback) + { + // Wait. + yield return new WaitForEndOfFrame(); + + // Disable the environment and terrain so we don't get them on the screenshot. + GameObject envObj = ObjectFinder.ObjectById("ID_Environment"); + GameObject terrain = ObjectFinder.ObjectById("ID_TerrainLift"); + envObj.SetActive(false); + terrain.SetActive(false); + + // Activate the camera, and render to a texture. Zandria requires a 576x432px image per bug. + thumbnailCamera.gameObject.SetActive(true); + PositionCamera(); + + RenderTexture renderTexture = new RenderTexture(576, 432, 24); + thumbnailCamera.targetTexture = renderTexture; + + // Placeholder to save and then later restore whatever is the current active RenderTexture. This is + // necessary to make sure the view of the thumbnailCamera is rendered and not main camera, which is + // what RenderTexture.active defaults to. + RenderTexture activeRender = RenderTexture.active; + RenderTexture.active = thumbnailCamera.targetTexture; + thumbnailCamera.Render(); + + // Save to an image. + Texture2D imageOverview = new Texture2D(thumbnailCamera.targetTexture.width, thumbnailCamera.targetTexture.height, TextureFormat.RGB24, false); + imageOverview.ReadPixels(new Rect(0, 0, thumbnailCamera.targetTexture.width, thumbnailCamera.targetTexture.height), 0, 0); + imageOverview.Apply(); + byte[] bytes = imageOverview.EncodeToPNG(); + + // Deactivate the camera again. + thumbnailCamera.gameObject.SetActive(false); + RenderTexture.active = activeRender; + + // Re-enable the environment and terrain. + envObj.SetActive(true); + + // Encode texture into PNG and callback. + callback(bytes); + } } - } } diff --git a/Assets/Scripts/model/export/ExportUtils.cs b/Assets/Scripts/model/export/ExportUtils.cs index 603f237e..ba4c9fd2 100644 --- a/Assets/Scripts/model/export/ExportUtils.cs +++ b/Assets/Scripts/model/export/ExportUtils.cs @@ -21,123 +21,143 @@ using System.IO; using UnityEngine; -namespace com.google.apps.peltzer.client.model.export { - class ExportUtils { - public static readonly string OBJ_FILENAME = "model.obj"; - public static readonly string TRIANGULATED_OBJ_FILENAME = "model-triangulated.obj"; - public static readonly string MTL_FILENAME = "materials.mtl"; - public static readonly string THUMBNAIL_FILENAME = "thumbnail.png"; - public static readonly string BLOCKS_FILENAME = "model.blocks"; - public static readonly string GLTF_FILENAME = "model.gltf"; - public static readonly string GLTF_BIN_FILENAME = "model.bin"; - public static readonly string FBX_FILENAME = "model.fbx"; +namespace com.google.apps.peltzer.client.model.export +{ + class ExportUtils + { + public static readonly string OBJ_FILENAME = "model.obj"; + public static readonly string TRIANGULATED_OBJ_FILENAME = "model-triangulated.obj"; + public static readonly string MTL_FILENAME = "materials.mtl"; + public static readonly string THUMBNAIL_FILENAME = "thumbnail.png"; + public static readonly string BLOCKS_FILENAME = "model.blocks"; + public static readonly string GLTF_FILENAME = "model.gltf"; + public static readonly string GLTF_BIN_FILENAME = "model.bin"; + public static readonly string FBX_FILENAME = "model.fbx"; - /// - /// Serialize the model to bytes in a range of formats. - /// - /// The model. - /// The meshes composing the content we would like to serialize and save. - /// If true, will serialize a .gltf file - /// If true, will serialize a .fbx file - /// If true, will serialize a triangulated .obj file - /// - /// Whether or not to include the recommended model display rotation in save. - /// - /// A serializer to perform the work for .blocks files. - public static SaveData SerializeModel(Model model, ICollection meshes, - bool saveGltf, bool saveFbx, bool saveTriangulatedObj, bool includeDisplayRotation, PolySerializer serializer, bool saveSelected) { + /// + /// Serialize the model to bytes in a range of formats. + /// + /// The model. + /// The meshes composing the content we would like to serialize and save. + /// If true, will serialize a .gltf file + /// If true, will serialize a .fbx file + /// If true, will serialize a triangulated .obj file + /// + /// Whether or not to include the recommended model display rotation in save. + /// + /// A serializer to perform the work for .blocks files. + public static SaveData SerializeModel(Model model, ICollection meshes, + bool saveGltf, bool saveFbx, bool saveTriangulatedObj, bool includeDisplayRotation, PolySerializer serializer, bool saveSelected) + { - // Serialize data. - SaveData saveData = new SaveData(); - HashSet materials = new HashSet(); - ObjFileExporter.ObjFileFromMeshes(meshes, MTL_FILENAME, model.meshRepresentationCache, ref materials, - /*triangulated*/ false, out saveData.objFile, out saveData.objPolyCount); - if (saveTriangulatedObj) { - ObjFileExporter.ObjFileFromMeshes(meshes, MTL_FILENAME, model.meshRepresentationCache, ref materials, - /*triangulated*/ true, out saveData.triangulatedObjFile, out saveData.triangulatedObjPolyCount); - } + // Serialize data. + SaveData saveData = new SaveData(); + HashSet materials = new HashSet(); + ObjFileExporter.ObjFileFromMeshes(meshes, MTL_FILENAME, model.meshRepresentationCache, ref materials, + /*triangulated*/ false, out saveData.objFile, out saveData.objPolyCount); + if (saveTriangulatedObj) + { + ObjFileExporter.ObjFileFromMeshes(meshes, MTL_FILENAME, model.meshRepresentationCache, ref materials, + /*triangulated*/ true, out saveData.triangulatedObjFile, out saveData.triangulatedObjPolyCount); + } - saveData.mtlFile = ObjFileExporter.MtlFileFromSet(materials); - if (saveGltf) { - ReMesher remesher; - if (saveSelected) { - remesher = new ReMesher(); - foreach (MMesh mesh in meshes) { - remesher.AddMesh(mesh); - } - remesher.Flush(); - remesher.UpdateTransforms(model); - } else { - remesher = model.GetReMesher(); + saveData.mtlFile = ObjFileExporter.MtlFileFromSet(materials); + if (saveGltf) + { + ReMesher remesher; + if (saveSelected) + { + remesher = new ReMesher(); + foreach (MMesh mesh in meshes) + { + remesher.AddMesh(mesh); + } + remesher.Flush(); + remesher.UpdateTransforms(model); + } + else + { + remesher = model.GetReMesher(); + } + saveData.GLTFfiles = PolyGLTFExporter.GLTFFileFromRemesher(remesher, + Path.Combine(PeltzerMain.Instance.modelsPath, GLTF_FILENAME), + Path.Combine(PeltzerMain.Instance.modelsPath, GLTF_BIN_FILENAME), + model.meshRepresentationCache); + } + saveData.fbxFile = FbxExporter.FbxFileFromMeshes(meshes, Path.Combine(PeltzerMain.Instance.modelsPath, + FBX_FILENAME)); + saveData.blocksFile = PeltzerFileHandler.PeltzerFileFromMeshes(meshes, includeDisplayRotation, serializer); + saveData.remixIds = model.GetAllRemixIds(meshes); + + return saveData; } - saveData.GLTFfiles = PolyGLTFExporter.GLTFFileFromRemesher(remesher, - Path.Combine(PeltzerMain.Instance.modelsPath, GLTF_FILENAME), - Path.Combine(PeltzerMain.Instance.modelsPath, GLTF_BIN_FILENAME), - model.meshRepresentationCache); - } - saveData.fbxFile = FbxExporter.FbxFileFromMeshes(meshes, Path.Combine(PeltzerMain.Instance.modelsPath, - FBX_FILENAME)); - saveData.blocksFile = PeltzerFileHandler.PeltzerFileFromMeshes(meshes, includeDisplayRotation, serializer); - saveData.remixIds = model.GetAllRemixIds(meshes); - return saveData; - } + /// + /// Writes SaveData to the user's local disk. + /// + /// A struct containing all the binary data for the save. + /// Where to save the data. + public static bool SaveLocally(SaveData saveData, string directory) + { + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } - /// - /// Writes SaveData to the user's local disk. - /// - /// A struct containing all the binary data for the save. - /// Where to save the data. - public static bool SaveLocally(SaveData saveData, string directory) { - if (!Directory.Exists(directory)) { - Directory.CreateDirectory(directory); - } + bool allSucceeded = true; - bool allSucceeded = true; + allSucceeded &= SaveBytesLocally(Path.Combine(directory, OBJ_FILENAME), saveData.objFile); + if (saveData.triangulatedObjFile != null) + { + allSucceeded &= SaveBytesLocally(Path.Combine(directory, TRIANGULATED_OBJ_FILENAME), + saveData.triangulatedObjFile); + } + allSucceeded &= SaveBytesLocally(Path.Combine(directory, MTL_FILENAME), saveData.mtlFile); + allSucceeded &= SaveBytesLocally(Path.Combine(directory, BLOCKS_FILENAME), saveData.blocksFile); + if (saveData.GLTFfiles != null) + { + allSucceeded &= SaveBytesLocally(Path.Combine(directory, GLTF_FILENAME), saveData.GLTFfiles.root.bytes); + foreach (FormatDataFile file in saveData.GLTFfiles.resources) + { + allSucceeded &= SaveBytesLocally(Path.Combine(directory, file.fileName), file.bytes); + } + } + if (saveData.thumbnailBytes != null) + { + allSucceeded &= SaveBytesLocally(Path.Combine(directory, THUMBNAIL_FILENAME), saveData.thumbnailBytes); + } + if (saveData.fbxFile != null) + { + allSucceeded &= SaveBytesLocally(Path.Combine(directory, FBX_FILENAME), saveData.fbxFile); + } - allSucceeded &= SaveBytesLocally(Path.Combine(directory, OBJ_FILENAME), saveData.objFile); - if (saveData.triangulatedObjFile != null) { - allSucceeded &= SaveBytesLocally(Path.Combine(directory, TRIANGULATED_OBJ_FILENAME), - saveData.triangulatedObjFile); - } - allSucceeded &= SaveBytesLocally(Path.Combine(directory, MTL_FILENAME), saveData.mtlFile); - allSucceeded &= SaveBytesLocally(Path.Combine(directory, BLOCKS_FILENAME), saveData.blocksFile); - if (saveData.GLTFfiles != null) { - allSucceeded &= SaveBytesLocally(Path.Combine(directory, GLTF_FILENAME), saveData.GLTFfiles.root.bytes); - foreach (FormatDataFile file in saveData.GLTFfiles.resources) { - allSucceeded &= SaveBytesLocally(Path.Combine(directory, file.fileName), file.bytes); + return allSucceeded; } - } - if (saveData.thumbnailBytes != null) { - allSucceeded &= SaveBytesLocally(Path.Combine(directory, THUMBNAIL_FILENAME), saveData.thumbnailBytes); - } - if (saveData.fbxFile != null) { - allSucceeded &= SaveBytesLocally(Path.Combine(directory, FBX_FILENAME), saveData.fbxFile); - } - return allSucceeded; - } + /// + /// Utility to save given bytes to a given path. + /// + /// Where to save the data + /// The data + private static bool SaveBytesLocally(string path, byte[] bytes) + { + try + { + // Open file for writing. + System.IO.FileStream fileStream = + new System.IO.FileStream(path, System.IO.FileMode.Create, System.IO.FileAccess.Write); + // Writes a block of bytes to this stream using data from a byte array. + fileStream.Write(bytes, 0, bytes.Length); - /// - /// Utility to save given bytes to a given path. - /// - /// Where to save the data - /// The data - private static bool SaveBytesLocally(string path, byte[] bytes) { - try { - // Open file for writing. - System.IO.FileStream fileStream = - new System.IO.FileStream(path, System.IO.FileMode.Create, System.IO.FileAccess.Write); - // Writes a block of bytes to this stream using data from a byte array. - fileStream.Write(bytes, 0, bytes.Length); - - // Close file stream - fileStream.Close(); - return true; - } catch (Exception exception) { - Debug.LogWarningFormat("{0}\n{1}", exception.Message, exception.StackTrace); - return false; - } + // Close file stream + fileStream.Close(); + return true; + } + catch (Exception exception) + { + Debug.LogWarningFormat("{0}\n{1}", exception.Message, exception.StackTrace); + return false; + } + } } - } } diff --git a/Assets/Scripts/model/export/Exporter.cs b/Assets/Scripts/model/export/Exporter.cs index 9525f907..3a71d471 100644 --- a/Assets/Scripts/model/export/Exporter.cs +++ b/Assets/Scripts/model/export/Exporter.cs @@ -20,54 +20,60 @@ using com.google.apps.peltzer.client.api_clients.assets_service_client; using com.google.apps.peltzer.client.zandria; -namespace com.google.apps.peltzer.client.model.export { - public class FormatSaveData { - public FormatDataFile root; - public List resources; - public Int64 triangleCount; - } +namespace com.google.apps.peltzer.client.model.export +{ + public class FormatSaveData + { + public FormatDataFile root; + public List resources; + public Int64 triangleCount; + } - public class FormatDataFile { - public String fileName; - public String mimeType; - public byte[] bytes; - public String tag; - public byte[] multipartBytes; - } + public class FormatDataFile + { + public String fileName; + public String mimeType; + public byte[] bytes; + public String tag; + public byte[] multipartBytes; + } - /// - /// A struct containing the serialized bytes of a model. - /// - public struct SaveData { - public string filenameBase; - public byte[] objFile; - public int objPolyCount; - public byte[] triangulatedObjFile; - public int triangulatedObjPolyCount; - public byte[] mtlFile; - public FormatSaveData GLTFfiles; - public byte[] fbxFile; - public byte[] blocksFile; - public byte[] thumbnailBytes; + /// + /// A struct containing the serialized bytes of a model. + /// + public struct SaveData + { + public string filenameBase; + public byte[] objFile; + public int objPolyCount; + public byte[] triangulatedObjFile; + public int triangulatedObjPolyCount; + public byte[] mtlFile; + public FormatSaveData GLTFfiles; + public byte[] fbxFile; + public byte[] blocksFile; + public byte[] thumbnailBytes; - // Note: this is computed from the model at serialization time (as the union of all remix IDs in all meshes). - public HashSet remixIds; - } + // Note: this is computed from the model at serialization time (as the union of all remix IDs in all meshes). + public HashSet remixIds; + } - /// - /// Handles exporting to the assets service. - /// - public class Exporter : MonoBehaviour { /// - /// Upload the serialized model as represented by SaveData to the assets service, opening a - /// window in the user's browser for them to complete publication, if 'publish' is true. + /// Handles exporting to the assets service. /// - public void UploadToVrAssetsService(SaveData saveData, bool publish, bool saveSelected) { - AssetsServiceClient assetsServiceClient = gameObject.AddComponent(); - AssetsServiceClientWork assetsServiceClientWork = gameObject.AddComponent(); - assetsServiceClientWork.Setup(assetsServiceClient, PeltzerMain.Instance.AssetId, - saveData.remixIds, saveData, publish, saveSelected); - PeltzerMain.Instance.DoPolyMenuBackgroundWork(assetsServiceClientWork); + public class Exporter : MonoBehaviour + { + /// + /// Upload the serialized model as represented by SaveData to the assets service, opening a + /// window in the user's browser for them to complete publication, if 'publish' is true. + /// + public void UploadToVrAssetsService(SaveData saveData, bool publish, bool saveSelected) + { + AssetsServiceClient assetsServiceClient = gameObject.AddComponent(); + AssetsServiceClientWork assetsServiceClientWork = gameObject.AddComponent(); + assetsServiceClientWork.Setup(assetsServiceClient, PeltzerMain.Instance.AssetId, + saveData.remixIds, saveData, publish, saveSelected); + PeltzerMain.Instance.DoPolyMenuBackgroundWork(assetsServiceClientWork); + } } - } } diff --git a/Assets/Scripts/model/export/FbxExporter.cs b/Assets/Scripts/model/export/FbxExporter.cs index 391cb011..e21bb398 100644 --- a/Assets/Scripts/model/export/FbxExporter.cs +++ b/Assets/Scripts/model/export/FbxExporter.cs @@ -21,159 +21,174 @@ using com.google.apps.peltzer.client.model.core; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.export { - /// - /// Class to handle exporting models to .fbx file format. This uses the BlocksFileExporter external - /// DLL located in Plugins to call the FBX SDK c++ code. - /// - public static class FbxExporter { +namespace com.google.apps.peltzer.client.model.export +{ /// - /// Print debug messages that originate from within the unmanaged DLL code. + /// Class to handle exporting models to .fbx file format. This uses the BlocksFileExporter external + /// DLL located in Plugins to call the FBX SDK c++ code. /// - /// Pointer to the debug function. - [DllImport("BlocksNativeLib")] - public static extern void SetDebugFunction(IntPtr fp); - - /// - /// Initializes the Fbx manager and scene. - /// - /// Path of the saved .fbx model. - [DllImport("BlocksNativeLib", EntryPoint = "StartExport")] - public static extern void StartExport(string filePath); - - /// - /// Responsible for calling FbxExporter.Export and saving the file, and performing necessary - /// cleanup. - /// - [DllImport("BlocksNativeLib", EntryPoint = "FinishExport")] - public static extern void FinishExport(); - - /// - /// Starts a new mesh node that holds a mesh and materials. If groupKey is nonzero, the mesh - /// will be added to the relevant group node; otherwise it will be added to the scene's - /// root node. It is not necessary to end a mesh node before starting a new one. - /// - /// ID of the mesh being exported. - /// Group ID of the mesh being exported. - [DllImport("BlocksNativeLib", EntryPoint = "StartMesh")] - public static extern void StartMesh(int meshId, int groupKey); - - /// - /// Adds vertices to the current mesh. - /// - /// All vertices of the mesh. - /// The number of vertices of the mesh. - [DllImport("BlocksNativeLib", EntryPoint = "AddMeshVertices")] - public static extern void AddMeshVertices(Vector3[] vertices, int numVerts); - - /// - /// Adds a new polygon to the current mesh. - /// - /// Index of the material the face uses. - /// Indices of the vertexes that make up this face (that map to the list - /// of mesh vertices). - /// Number of vertices belonging to this face. - /// The normal of this face. - [DllImport("BlocksNativeLib", EntryPoint = "AddFace")] - public static extern void AddFace(int matId, int[] vertexIndices, int numVertices, Vector3 normal); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void MyDelegate(string str); - - private static Mutex fbxExportMutex; - - public static void Setup() { - try { - MyDelegate del = new MyDelegate(CallBackFunction); - // Convert callback_delegate into a function pointer that can be - // used in unmanaged code. - IntPtr intptr_delegate = - Marshal.GetFunctionPointerForDelegate(del); - SetDebugFunction(intptr_delegate); - fbxExportMutex = new Mutex(); - } - catch (Exception ex) { - // Missing FBX DLL. - Debug.LogError("Unable to load FBXExporter DLL: " + ex.ToString()); - } - } - - /// - /// Debug function to print information from the c++ side. - /// - static void CallBackFunction(string str) { - Debug.Log("Callback " + str); - } + public static class FbxExporter + { + /// + /// Print debug messages that originate from within the unmanaged DLL code. + /// + /// Pointer to the debug function. + [DllImport("BlocksNativeLib")] + public static extern void SetDebugFunction(IntPtr fp); + + /// + /// Initializes the Fbx manager and scene. + /// + /// Path of the saved .fbx model. + [DllImport("BlocksNativeLib", EntryPoint = "StartExport")] + public static extern void StartExport(string filePath); + + /// + /// Responsible for calling FbxExporter.Export and saving the file, and performing necessary + /// cleanup. + /// + [DllImport("BlocksNativeLib", EntryPoint = "FinishExport")] + public static extern void FinishExport(); + + /// + /// Starts a new mesh node that holds a mesh and materials. If groupKey is nonzero, the mesh + /// will be added to the relevant group node; otherwise it will be added to the scene's + /// root node. It is not necessary to end a mesh node before starting a new one. + /// + /// ID of the mesh being exported. + /// Group ID of the mesh being exported. + [DllImport("BlocksNativeLib", EntryPoint = "StartMesh")] + public static extern void StartMesh(int meshId, int groupKey); + + /// + /// Adds vertices to the current mesh. + /// + /// All vertices of the mesh. + /// The number of vertices of the mesh. + [DllImport("BlocksNativeLib", EntryPoint = "AddMeshVertices")] + public static extern void AddMeshVertices(Vector3[] vertices, int numVerts); + + /// + /// Adds a new polygon to the current mesh. + /// + /// Index of the material the face uses. + /// Indices of the vertexes that make up this face (that map to the list + /// of mesh vertices). + /// Number of vertices belonging to this face. + /// The normal of this face. + [DllImport("BlocksNativeLib", EntryPoint = "AddFace")] + public static extern void AddFace(int matId, int[] vertexIndices, int numVertices, Vector3 normal); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void MyDelegate(string str); + + private static Mutex fbxExportMutex; + + public static void Setup() + { + try + { + MyDelegate del = new MyDelegate(CallBackFunction); + // Convert callback_delegate into a function pointer that can be + // used in unmanaged code. + IntPtr intptr_delegate = + Marshal.GetFunctionPointerForDelegate(del); + SetDebugFunction(intptr_delegate); + fbxExportMutex = new Mutex(); + } + catch (Exception ex) + { + // Missing FBX DLL. + Debug.LogError("Unable to load FBXExporter DLL: " + ex.ToString()); + } + } - /// - /// Exports the given meshes to .fbx and returns the bytes of the .fbx file. - /// - public static byte[] FbxFileFromMeshes(ICollection meshes, string fbxFileName) { - fbxExportMutex.WaitOne(); - try { - StartExport(fbxFileName); - - // Export all meshes. - foreach (MMesh mesh in meshes) { - ExportComponent(mesh, mesh.groupId); + /// + /// Debug function to print information from the c++ side. + /// + static void CallBackFunction(string str) + { + Debug.Log("Callback " + str); } - FinishExport(); - byte[] bytes = File.ReadAllBytes(fbxFileName); - return bytes; - } - catch (Exception ex) { - // When saving on the background thread, we can get out of sync exceptions - it's okay for save to fail in those - // scenarios so we just log the exception and release the mutex. - Debug.LogWarning("Error trying to export fbx: " + ex.Message + " at " + ex.StackTrace + " if this is from the" - + " background thread this is probably nothing to worry about."); - return null; - } - finally { - fbxExportMutex.ReleaseMutex(); - } - } + /// + /// Exports the given meshes to .fbx and returns the bytes of the .fbx file. + /// + public static byte[] FbxFileFromMeshes(ICollection meshes, string fbxFileName) + { + fbxExportMutex.WaitOne(); + try + { + StartExport(fbxFileName); + + // Export all meshes. + foreach (MMesh mesh in meshes) + { + ExportComponent(mesh, mesh.groupId); + } + + FinishExport(); + byte[] bytes = File.ReadAllBytes(fbxFileName); + return bytes; + } + catch (Exception ex) + { + // When saving on the background thread, we can get out of sync exceptions - it's okay for save to fail in those + // scenarios so we just log the exception and release the mutex. + Debug.LogWarning("Error trying to export fbx: " + ex.Message + " at " + ex.StackTrace + " if this is from the" + + " background thread this is probably nothing to worry about."); + return null; + } + finally + { + fbxExportMutex.ReleaseMutex(); + } + } - /// - /// Add a mesh node with vertices, normals, polygons, and materials. - /// - /// The mesh being exported to fbx. - /// The group id of the mesh, or GROUP_NONE value if the mesh is not in a group. - private static void ExportComponent(MMesh mesh, int groupId = MMesh.GROUP_NONE) { - // We do not wish to duplicate vertices that are shared across faces. As such, we maintain a dictionary from - // Vertex.id to the index in the vertices list this method updates. We only need to maintain this dictionary - // per mesh, as Vertex.id is only shared within a single MMesh. Similar to what the .obj exporter does to - // maintain non-duplicate vertices, but zero indexed. - List meshVertices = new List(); - List meshNormals = new List(); - Dictionary vertexIdToIndex = new Dictionary(mesh.vertexCount); - - // Start a new FBX MeshNode with its own vertices, polygons, and materials. - StartMesh(mesh.id, groupId); - - foreach (Face face in mesh.GetFaces()) { - List vertexIdsForFace = new List(face.vertexIds.Count); - for (int i = 0; i < face.vertexIds.Count; i++) { - int vertexId = face.vertexIds[i]; - int vertexIndex; - Vector3 modelCoords = mesh.VertexPositionInModelCoords(face.vertexIds[i]); - if (!vertexIdToIndex.TryGetValue(vertexId, out vertexIndex)) { - meshVertices.Add(modelCoords); - vertexIndex = meshVertices.Count - 1; - vertexIdToIndex.Add(vertexId, vertexIndex); - } - vertexIdsForFace.Add(vertexIndex); + /// + /// Add a mesh node with vertices, normals, polygons, and materials. + /// + /// The mesh being exported to fbx. + /// The group id of the mesh, or GROUP_NONE value if the mesh is not in a group. + private static void ExportComponent(MMesh mesh, int groupId = MMesh.GROUP_NONE) + { + // We do not wish to duplicate vertices that are shared across faces. As such, we maintain a dictionary from + // Vertex.id to the index in the vertices list this method updates. We only need to maintain this dictionary + // per mesh, as Vertex.id is only shared within a single MMesh. Similar to what the .obj exporter does to + // maintain non-duplicate vertices, but zero indexed. + List meshVertices = new List(); + List meshNormals = new List(); + Dictionary vertexIdToIndex = new Dictionary(mesh.vertexCount); + + // Start a new FBX MeshNode with its own vertices, polygons, and materials. + StartMesh(mesh.id, groupId); + + foreach (Face face in mesh.GetFaces()) + { + List vertexIdsForFace = new List(face.vertexIds.Count); + for (int i = 0; i < face.vertexIds.Count; i++) + { + int vertexId = face.vertexIds[i]; + int vertexIndex; + Vector3 modelCoords = mesh.VertexPositionInModelCoords(face.vertexIds[i]); + if (!vertexIdToIndex.TryGetValue(vertexId, out vertexIndex)) + { + meshVertices.Add(modelCoords); + vertexIndex = meshVertices.Count - 1; + vertexIdToIndex.Add(vertexId, vertexIndex); + } + vertexIdsForFace.Add(vertexIndex); + } + // Because Unity is a left handed coordinate system and .fbx is right handed we must reverse + // the winding order of the vertices -- also to this effect, the .dll code negates the x + // coordinate of each vertex when adding it to the mesh. + vertexIdsForFace.Reverse(); + meshNormals.Add((mesh.rotation * face.normal).normalized); + AddFace(face.properties.materialId, vertexIdsForFace.ToArray(), face.vertexIds.Count, + (mesh.rotation * face.normal).normalized); + } + + AddMeshVertices(meshVertices.ToArray(), meshVertices.Count); } - // Because Unity is a left handed coordinate system and .fbx is right handed we must reverse - // the winding order of the vertices -- also to this effect, the .dll code negates the x - // coordinate of each vertex when adding it to the mesh. - vertexIdsForFace.Reverse(); - meshNormals.Add((mesh.rotation * face.normal).normalized); - AddFace(face.properties.materialId, vertexIdsForFace.ToArray(), face.vertexIds.Count, - (mesh.rotation * face.normal).normalized); - } - - AddMeshVertices(meshVertices.ToArray(), meshVertices.Count); } - } } diff --git a/Assets/Scripts/model/export/ObjFileExporter.cs b/Assets/Scripts/model/export/ObjFileExporter.cs index 31a06489..378a0dc4 100644 --- a/Assets/Scripts/model/export/ObjFileExporter.cs +++ b/Assets/Scripts/model/export/ObjFileExporter.cs @@ -22,408 +22,459 @@ using System.Text; using UnityEngine; -namespace com.google.apps.peltzer.client.model.export { - /// - /// Simple Obj file exporter. - /// - public class ObjFileExporter { - public static string RandomOpaqueId() { - StringBuilder sb = new StringBuilder(""); - System.Random r = new System.Random(); - // 300,000,000 combinations. Hopefully no collisions :) - for (int i = 0; i < 6; i++) { - sb.Append((char)('a' + r.Next(26))); - } - return sb.ToString(); - } - +namespace com.google.apps.peltzer.client.model.export +{ /// - /// Generate an MTL for our materials from a set of material ids. + /// Simple Obj file exporter. /// - /// - public static byte[] MtlFileFromSet(HashSet materialIds) { - MemoryStream stream = new MemoryStream(); - StreamWriter sw = new StreamWriter(stream); - - foreach (int matId in materialIds) { - // Don't try and export wireframes, bug - if (matId == MaterialRegistry.GREEN_WIREFRAME_ID || matId == MaterialRegistry.PINK_WIREFRAME_ID) { - continue; - } - sw.WriteLine("newmtl mat" + matId); - if (matId == MaterialRegistry.GLASS_ID || matId == MaterialRegistry.GEM_ID) { - if (matId == MaterialRegistry.GLASS_ID) { - sw.WriteLine(" Ka 0.58 0.65 1.00"); - } else if (matId == MaterialRegistry.GEM_ID) { - sw.WriteLine(" Ka 1.00 0.65 0.67"); - } - - sw.WriteLine(" Kd 0.92 0.95 0.94"); - sw.WriteLine(" Ks 1 1 1"); - sw.WriteLine(" illum 9"); - sw.WriteLine(" Ns 300"); - sw.WriteLine(" d 0.4"); - sw.WriteLine(" Ni 1.5"); - } else { - Color c = MaterialRegistry.GetMaterialColorById(matId); - sw.WriteLine(" Kd " + c.r.ToString("0.00") + " " + c.g.ToString("0.00") + " " + c.b.ToString("0.00")); + public class ObjFileExporter + { + public static string RandomOpaqueId() + { + StringBuilder sb = new StringBuilder(""); + System.Random r = new System.Random(); + // 300,000,000 combinations. Hopefully no collisions :) + for (int i = 0; i < 6; i++) + { + sb.Append((char)('a' + r.Next(26))); + } + return sb.ToString(); } - sw.WriteLine(""); - } - sw.Close(); - - return stream.ToArray(); - } - - // These are two vectors that don't point in the same direction - meaning that at least one of them won't be - // parallel to any given face normal. We use these to generate UVs using tangent space coordinates. - private static Vector3 arbitraryVector; - private static Vector3 alternateArbitraryVector; - - private class MeshExportInfo { - public int meshId; - public List meshFaceColors; - public List meshFaceVerts; - public List meshNormals; - public List meshFaceUvs; - - public MeshExportInfo(int id) { - meshId = id; - meshFaceColors = new List(); - meshFaceVerts = new List(); - meshFaceUvs = new List(); - meshNormals = new List(); - } - } - - /// - /// Write collection of meshes into an obj file (as a byte array). The vertex coordinates in the OBJ file will be - /// generated such that the centroid of the given meshes is at the origin. This addresses the problem of OBJ files - /// having seemingly arbitrary origin points that are sometimes outside the object, making it difficult - /// to import/position them in Unity and other tools. - /// - /// Sets the output bytes and polyCount as out parameters. - /// - public static void ObjFileFromMeshes(ICollection meshes, string mtlFileName, - MeshRepresentationCache meshRepresentationCache, ref HashSet materials, bool triangulated, - out byte[] bytes, out int polyCount) { - List vertices = new List(); - List uvs = new List(); - Dictionary exportInfos = new Dictionary(); - List polyNormals = new List(); - polyCount = 0; - - // Initialize arbitrary vectors once per export. Only criteria is that they are not parallel. - arbitraryVector = new Vector3(0.42f, -0.21f, 0.15f).normalized; - alternateArbitraryVector = new Vector3(0.43f, 1.5f, 0.15f).normalized; - - MemoryStream stream = new MemoryStream(); - StreamWriter sw = new StreamWriter(stream); - - StringBuilder sb = new StringBuilder("mtllib "); - sb.Append(mtlFileName); - sw.WriteLine(sb.ToString()); - - HashSet ungroupedMeshes = new HashSet(); - Dictionary> groupedMeshes = new Dictionary>(); - foreach (MMesh mesh in meshes) { - if (mesh.groupId != MMesh.GROUP_NONE) { - if (!groupedMeshes.ContainsKey(mesh.groupId)) { - groupedMeshes[mesh.groupId] = new List(); - } - groupedMeshes[mesh.groupId].Add(mesh); - } - else { - ungroupedMeshes.Add(mesh); + /// + /// Generate an MTL for our materials from a set of material ids. + /// + /// + public static byte[] MtlFileFromSet(HashSet materialIds) + { + MemoryStream stream = new MemoryStream(); + StreamWriter sw = new StreamWriter(stream); + + foreach (int matId in materialIds) + { + // Don't try and export wireframes, bug + if (matId == MaterialRegistry.GREEN_WIREFRAME_ID || matId == MaterialRegistry.PINK_WIREFRAME_ID) + { + continue; + } + sw.WriteLine("newmtl mat" + matId); + if (matId == MaterialRegistry.GLASS_ID || matId == MaterialRegistry.GEM_ID) + { + if (matId == MaterialRegistry.GLASS_ID) + { + sw.WriteLine(" Ka 0.58 0.65 1.00"); + } + else if (matId == MaterialRegistry.GEM_ID) + { + sw.WriteLine(" Ka 1.00 0.65 0.67"); + } + + sw.WriteLine(" Kd 0.92 0.95 0.94"); + sw.WriteLine(" Ks 1 1 1"); + sw.WriteLine(" illum 9"); + sw.WriteLine(" Ns 300"); + sw.WriteLine(" d 0.4"); + sw.WriteLine(" Ni 1.5"); + } + else + { + Color c = MaterialRegistry.GetMaterialColorById(matId); + sw.WriteLine(" Kd " + c.r.ToString("0.00") + " " + c.g.ToString("0.00") + " " + c.b.ToString("0.00")); + } + sw.WriteLine(""); + } + + sw.Close(); + + return stream.ToArray(); } - } - - foreach (MMesh mesh in meshes) { - MeshExportInfo info = new MeshExportInfo(mesh.id); - info.meshFaceVerts = new List(); - if (triangulated) { - AddTriangulatedMesh(mesh, meshRepresentationCache, ref vertices, ref uvs, ref info, ref polyNormals, - ref materials, ref polyCount); - } else { - AddMesh(mesh, ref vertices, ref uvs, ref info, ref polyNormals, ref materials, ref polyCount); - } - exportInfos[mesh.id] = info; - } - - Vector3 centroid = Math3d.FindCentroid(meshes); - for (int i = 0; i < vertices.Count; i++) { - // Translate vertex such that centroid is at the origin. To do this, all we have to do is subtract the - // centroid position from the vertex coordinates. - Vector3 vert = vertices[i] - centroid; - - // Swap X for OBJ file(?) - sb = new StringBuilder("v "); - sb.Append(-vert.x).Append(" ").Append(vert.y).Append(" ").Append(vert.z); - sw.WriteLine(sb.ToString()); - } - for (int i = 0; i < polyNormals.Count; i++) { - Vector3 polyNormal = polyNormals[i]; - - // Swap X for OBJ file(?) - sb = new StringBuilder("vn "); - sb.Append(-polyNormal.x).Append(" ").Append(polyNormal.y).Append(" ").Append(polyNormal.z); - sw.WriteLine(sb.ToString()); - } - for (int i = 0; i < uvs.Count; i++) { - Vector2 uv = uvs[i]; - - sb = new StringBuilder("vt "); - sb.Append(uv.x).Append(" ").Append(uv.y); - sw.WriteLine(sb.ToString()); - } - - - List singleElementMesh = new List(1); - //Make sure it has a single element - we're going to keep on replacing that element to export all single meshes. - singleElementMesh.Add(null); - foreach (MMesh mesh in ungroupedMeshes) { - sw.WriteLine(new StringBuilder("o group").Append(mesh.id)); - sw.WriteLine(new StringBuilder("g mesh").Append(mesh.id)); - singleElementMesh[0] = mesh; - WriteMesh(singleElementMesh, exportInfos, sw); - } - foreach (int key in groupedMeshes.Keys) { - sw.WriteLine(new StringBuilder("o group").Append(key)); - sw.WriteLine(new StringBuilder("g mesh").Append(key)); - WriteMesh(groupedMeshes[key], exportInfos, sw); - } - - sw.Close(); - bytes = stream.ToArray(); - } - private static void WriteMesh(List inputMeshes, - Dictionary exportInfos, - StreamWriter sw) { - Dictionary> materialsAndFaces = new Dictionary>(); - - foreach (MMesh mmesh in inputMeshes) { - List faceColors = exportInfos[mmesh.id].meshFaceColors; - List polys = exportInfos[mmesh.id].meshFaceVerts; - List normals = exportInfos[mmesh.id].meshNormals; - List uvs = exportInfos[mmesh.id].meshFaceUvs; - - for (int i = 0; i < polys.Count; i++) { - int[] verts = polys[i]; - int[] rawUvs = uvs[i]; - StringBuilder faceSB = new StringBuilder("f"); - for (int j = 0; j < verts.Length; j++) { - // Swapping X, need to reverse order of poly vertices. - int idx = verts.Length - j - 1; - faceSB.Append(" ").Append(verts[idx]); - // Append the poly normal to each vertex reference. Note that .obj is 1-indexed. - faceSB.Append("/").Append(rawUvs[idx]).Append("/").Append(normals[i] + 1); - } - - string usemtl = "usemtl mat" + faceColors[i]; - if (!materialsAndFaces.ContainsKey(usemtl)) { - materialsAndFaces.Add(usemtl, new List()); - } - materialsAndFaces[usemtl].Add(faceSB.ToString()); + // These are two vectors that don't point in the same direction - meaning that at least one of them won't be + // parallel to any given face normal. We use these to generate UVs using tangent space coordinates. + private static Vector3 arbitraryVector; + private static Vector3 alternateArbitraryVector; + + private class MeshExportInfo + { + public int meshId; + public List meshFaceColors; + public List meshFaceVerts; + public List meshNormals; + public List meshFaceUvs; + + public MeshExportInfo(int id) + { + meshId = id; + meshFaceColors = new List(); + meshFaceVerts = new List(); + meshFaceUvs = new List(); + meshNormals = new List(); + } } - } - foreach (KeyValuePair> pair in materialsAndFaces) { - sw.WriteLine(pair.Key); - foreach (string face in pair.Value) { - sw.WriteLine(face); + /// + /// Write collection of meshes into an obj file (as a byte array). The vertex coordinates in the OBJ file will be + /// generated such that the centroid of the given meshes is at the origin. This addresses the problem of OBJ files + /// having seemingly arbitrary origin points that are sometimes outside the object, making it difficult + /// to import/position them in Unity and other tools. + /// + /// Sets the output bytes and polyCount as out parameters. + /// + public static void ObjFileFromMeshes(ICollection meshes, string mtlFileName, + MeshRepresentationCache meshRepresentationCache, ref HashSet materials, bool triangulated, + out byte[] bytes, out int polyCount) + { + List vertices = new List(); + List uvs = new List(); + Dictionary exportInfos = new Dictionary(); + List polyNormals = new List(); + polyCount = 0; + + // Initialize arbitrary vectors once per export. Only criteria is that they are not parallel. + arbitraryVector = new Vector3(0.42f, -0.21f, 0.15f).normalized; + alternateArbitraryVector = new Vector3(0.43f, 1.5f, 0.15f).normalized; + + MemoryStream stream = new MemoryStream(); + StreamWriter sw = new StreamWriter(stream); + + StringBuilder sb = new StringBuilder("mtllib "); + sb.Append(mtlFileName); + sw.WriteLine(sb.ToString()); + + HashSet ungroupedMeshes = new HashSet(); + Dictionary> groupedMeshes = new Dictionary>(); + foreach (MMesh mesh in meshes) + { + if (mesh.groupId != MMesh.GROUP_NONE) + { + if (!groupedMeshes.ContainsKey(mesh.groupId)) + { + groupedMeshes[mesh.groupId] = new List(); + } + groupedMeshes[mesh.groupId].Add(mesh); + } + else + { + ungroupedMeshes.Add(mesh); + } + } + + foreach (MMesh mesh in meshes) + { + MeshExportInfo info = new MeshExportInfo(mesh.id); + info.meshFaceVerts = new List(); + if (triangulated) + { + AddTriangulatedMesh(mesh, meshRepresentationCache, ref vertices, ref uvs, ref info, ref polyNormals, + ref materials, ref polyCount); + } + else + { + AddMesh(mesh, ref vertices, ref uvs, ref info, ref polyNormals, ref materials, ref polyCount); + } + exportInfos[mesh.id] = info; + } + + Vector3 centroid = Math3d.FindCentroid(meshes); + for (int i = 0; i < vertices.Count; i++) + { + // Translate vertex such that centroid is at the origin. To do this, all we have to do is subtract the + // centroid position from the vertex coordinates. + Vector3 vert = vertices[i] - centroid; + + // Swap X for OBJ file(?) + sb = new StringBuilder("v "); + sb.Append(-vert.x).Append(" ").Append(vert.y).Append(" ").Append(vert.z); + sw.WriteLine(sb.ToString()); + } + for (int i = 0; i < polyNormals.Count; i++) + { + Vector3 polyNormal = polyNormals[i]; + + // Swap X for OBJ file(?) + sb = new StringBuilder("vn "); + sb.Append(-polyNormal.x).Append(" ").Append(polyNormal.y).Append(" ").Append(polyNormal.z); + sw.WriteLine(sb.ToString()); + } + for (int i = 0; i < uvs.Count; i++) + { + Vector2 uv = uvs[i]; + + sb = new StringBuilder("vt "); + sb.Append(uv.x).Append(" ").Append(uv.y); + sw.WriteLine(sb.ToString()); + } + + + List singleElementMesh = new List(1); + //Make sure it has a single element - we're going to keep on replacing that element to export all single meshes. + singleElementMesh.Add(null); + foreach (MMesh mesh in ungroupedMeshes) + { + sw.WriteLine(new StringBuilder("o group").Append(mesh.id)); + sw.WriteLine(new StringBuilder("g mesh").Append(mesh.id)); + singleElementMesh[0] = mesh; + WriteMesh(singleElementMesh, exportInfos, sw); + } + foreach (int key in groupedMeshes.Keys) + { + sw.WriteLine(new StringBuilder("o group").Append(key)); + sw.WriteLine(new StringBuilder("g mesh").Append(key)); + WriteMesh(groupedMeshes[key], exportInfos, sw); + } + + sw.Close(); + bytes = stream.ToArray(); } - } - } - /// - /// Calculate a dummy UV value for the vertex. - /// - /// A vector tangent to the vertex in an arbitrary direction. - /// A vector orthogonal to both the tangent and normal vectors. - /// The vertex we are calculating a UV value for. - /// - private static Vector2 GetVertexUv(Vector3 tangent, Vector3 binormal, Vector3 vertex) { - // Divide by 20 and add 0.5 to ideally keep the UV values scaled between 0 and 1, as some applications - // assume UVs between 0 and 1. We assume the original vertex coordinates will be within the -10, 10 range - // because this is the size of the workspace. TODO(bug) tracks improving this system. - return new Vector2(Vector3.Dot(tangent, vertex) / 20f + 0.5f, Vector3.Dot(binormal, vertex) / 20f + 0.5f); - } - - /// - /// Calculate the tangent space of the vertex's UV value. - /// - /// The normal of the vertex we are calculating a UV for. - /// A vector tangent to the vertex in an arbitrary direction. - /// A vector orthogonal to both the tangent and normal vectors. - private static void GetTangentSpaceBasis(Vector3 normal, out Vector3 tangent, out Vector3 binormal) { - // If arbitrary vector is parallel to the normal, choose a different one. - if (Mathf.Abs(Vector3.Dot(normal, arbitraryVector)) < 1f) { - tangent = Vector3.Cross(normal, arbitraryVector).normalized; - binormal = Vector3.Cross(normal, tangent).normalized; - } else { - tangent = Vector3.Cross(normal, alternateArbitraryVector).normalized; - binormal = Vector3.Cross(normal, tangent).normalized; - } - } + private static void WriteMesh(List inputMeshes, + Dictionary exportInfos, + StreamWriter sw) + { + Dictionary> materialsAndFaces = new Dictionary>(); + + foreach (MMesh mmesh in inputMeshes) + { + List faceColors = exportInfos[mmesh.id].meshFaceColors; + List polys = exportInfos[mmesh.id].meshFaceVerts; + List normals = exportInfos[mmesh.id].meshNormals; + List uvs = exportInfos[mmesh.id].meshFaceUvs; + + for (int i = 0; i < polys.Count; i++) + { + int[] verts = polys[i]; + int[] rawUvs = uvs[i]; + StringBuilder faceSB = new StringBuilder("f"); + for (int j = 0; j < verts.Length; j++) + { + // Swapping X, need to reverse order of poly vertices. + int idx = verts.Length - j - 1; + faceSB.Append(" ").Append(verts[idx]); + // Append the poly normal to each vertex reference. Note that .obj is 1-indexed. + faceSB.Append("/").Append(rawUvs[idx]).Append("/").Append(normals[i] + 1); + } + + string usemtl = "usemtl mat" + faceColors[i]; + if (!materialsAndFaces.ContainsKey(usemtl)) + { + materialsAndFaces.Add(usemtl, new List()); + } + materialsAndFaces[usemtl].Add(faceSB.ToString()); + } + } + + foreach (KeyValuePair> pair in materialsAndFaces) + { + sw.WriteLine(pair.Key); + foreach (string face in pair.Value) + { + sw.WriteLine(face); + } + } + } - /// - /// Returns the index of the vertex in the vertices list, inserting the vertex if it is not already there. - /// Note that vertex indices are 1-indexed as per the OBJ file spec. - /// - /// The Vector3 position of the vertex whose index is being sought. - /// The dictionary maintaining a record of - /// (vertex position, vertex list index). - /// The list of vertices in the mesh being processed. - private static int GetVertexIndex(Vector3 vertex, ref Dictionary vertexPositionToIndex, - ref List vertices) { - int vertexIndex; - if (!vertexPositionToIndex.TryGetValue(vertex, out vertexIndex)) { - vertices.Add(vertex); - vertexIndex = vertices.Count; - vertexPositionToIndex.Add(vertex, vertexIndex); - } - return vertexIndex; - } + /// + /// Calculate a dummy UV value for the vertex. + /// + /// A vector tangent to the vertex in an arbitrary direction. + /// A vector orthogonal to both the tangent and normal vectors. + /// The vertex we are calculating a UV value for. + /// + private static Vector2 GetVertexUv(Vector3 tangent, Vector3 binormal, Vector3 vertex) + { + // Divide by 20 and add 0.5 to ideally keep the UV values scaled between 0 and 1, as some applications + // assume UVs between 0 and 1. We assume the original vertex coordinates will be within the -10, 10 range + // because this is the size of the workspace. TODO(bug) tracks improving this system. + return new Vector2(Vector3.Dot(tangent, vertex) / 20f + 0.5f, Vector3.Dot(binormal, vertex) / 20f + 0.5f); + } - /// - /// Adds a triangulated MMesh's export information to the given lists of vertices, uvs, mesh normals, and materials. - /// - /// The MMesh to be added. - /// A cache of triangulated meshes. - /// The list of vertices to be updated. - /// The UV vertices to be calculated. - /// Export information about the mesh faces, including face normals, colors, uvs, - /// and vertices to be calculated. - /// The face normals of the mesh to be calculated. - /// The set of materials that have been seen and should be added to the .mtl file. - /// A running total of the poly count of this export. - private static void AddTriangulatedMesh(MMesh mesh, MeshRepresentationCache meshRepresentationCache, - ref List vertices, - ref List uvs, - ref MeshExportInfo meshExportInfo, - ref List meshNormals, - ref HashSet materials, - ref int polyCount) { - meshExportInfo.meshFaceColors = new List(); - - // Maintain a dictionary of vertex position keyed to its index in the vertices list this method updates. - // We use because vertices are stored in the triangulated MeshGenContext as Vector3s, - // not Blocks vertex ids. This lets us not duplicate vertices that are shared across faces in a mesh. - Dictionary vertexPositionToIndex = new Dictionary(); - // We attempt to look up the triangulation in a cache for efficiency; if it is not there it is triangulated - // on the fly. - Dictionary meshInfoByMaterial = - meshRepresentationCache.FetchComponentsForMesh(mesh.id, /* abortOnCacheMiss */ false); - // Note that meshInfoByMaterial contains a "sub-mesh" for each material. Next we will process each of these - // sub-meshes in turn. - foreach (KeyValuePair pair in meshInfoByMaterial) { - int materialId = pair.Key; - MeshGenContext meshGenContext = pair.Value; - - List triangles = meshGenContext.triangles; - for (int i = 0; i < triangles.Count; i += 3) { - polyCount++; - int vertexIndex1 = GetVertexIndex(meshGenContext.verts[triangles[i]], ref vertexPositionToIndex, - ref vertices); - int vertexIndex2 = GetVertexIndex(meshGenContext.verts[triangles[i + 1]], ref vertexPositionToIndex, - ref vertices); - int vertexIndex3 = GetVertexIndex(meshGenContext.verts[triangles[i + 2]], ref vertexPositionToIndex, - ref vertices); - - meshExportInfo.meshFaceVerts.Add(new int[] { vertexIndex1, vertexIndex2, vertexIndex3 }); - meshExportInfo.meshFaceColors.Add(materialId); - materials.Add(materialId); - - // TODO (bug) This normal calculation is duplicate work (calculating the normal for - // every triangle in a triangulated face) and should be made more efficient. - Vector3 normal = MeshMath.CalculateNormal( - meshGenContext.verts[triangles[i]], - meshGenContext.verts[triangles[i + 1]], - meshGenContext.verts[triangles[i + 2]]); - - Vector3 tangent, binormal; - GetTangentSpaceBasis(normal, out tangent, out binormal); - - uvs.Add(GetVertexUv(tangent, binormal, meshGenContext.verts[triangles[i]])); - uvs.Add(GetVertexUv(tangent, binormal, meshGenContext.verts[triangles[i + 1]])); - uvs.Add(GetVertexUv(tangent, binormal, meshGenContext.verts[triangles[i + 2]])); - - int[] uvsIdsForFace = new int[] { uvs.Count - 2, uvs.Count - 1, uvs.Count }; - meshExportInfo.meshFaceUvs.Add(uvsIdsForFace); - meshExportInfo.meshNormals.Add(meshNormals.Count); - meshNormals.Add(normal); + /// + /// Calculate the tangent space of the vertex's UV value. + /// + /// The normal of the vertex we are calculating a UV for. + /// A vector tangent to the vertex in an arbitrary direction. + /// A vector orthogonal to both the tangent and normal vectors. + private static void GetTangentSpaceBasis(Vector3 normal, out Vector3 tangent, out Vector3 binormal) + { + // If arbitrary vector is parallel to the normal, choose a different one. + if (Mathf.Abs(Vector3.Dot(normal, arbitraryVector)) < 1f) + { + tangent = Vector3.Cross(normal, arbitraryVector).normalized; + binormal = Vector3.Cross(normal, tangent).normalized; + } + else + { + tangent = Vector3.Cross(normal, alternateArbitraryVector).normalized; + binormal = Vector3.Cross(normal, tangent).normalized; + } } - } - } - /// - /// Adds a MMesh's export information to the given lists of vertices, uvs, mesh normals, and materials. - /// - /// The given MMesh - /// The list of vertices to be updated. - /// The UV vertices to be calculated. - /// Export information about the mesh faces, including face normals, colors, uvs, - /// and vertices to be calculated. - /// The face normals of the mesh to be calculated. - /// The set of materials that have been seen and should be added to the .mtl file. - /// A running total of the poly count of this export. - private static void AddMesh(MMesh mesh, - ref List vertices, - ref List uvs, - ref MeshExportInfo meshExportInfo, - ref List meshNormals, - ref HashSet materials, - ref int polyCount) { - meshExportInfo.meshFaceColors = new List(mesh.faceCount); - - // We do not wish to duplicate vertices that are shared across faces. As such, we maintain a dictionary from - // Vertex.id to the index in the vertices list this method updates. We only need to maintain this dictionary - // per mesh, as Vertex.id is only shared within a single MMesh. - Dictionary vertexIdToIndex = new Dictionary(mesh.vertexCount); - - foreach (Face face in mesh.GetFaces()) { - polyCount++; - meshExportInfo.meshFaceColors.Add(face.properties.materialId); - // Record face color for .mtl file, if not seen already. - materials.Add(face.properties.materialId); - // We cannot use poly vertex ids, we need the position of the vertex in 'vertices' as the identifier. - int[] vertexIdsForFace = new int[face.vertexIds.Count]; - int[] uvsIdsForFace = new int[face.vertexIds.Count]; - - // TODO(64715939): Calculate the normal for each face - once we're more confident in the normals stored in each - // face, we can switch to using them directly. - List faceVertexPositions = new List(face.vertexIds.Count); - for (int i = 0; i < face.vertexIds.Count; i++) { - faceVertexPositions.Add(mesh.VertexPositionInMeshCoords(face.vertexIds[i])); + /// + /// Returns the index of the vertex in the vertices list, inserting the vertex if it is not already there. + /// Note that vertex indices are 1-indexed as per the OBJ file spec. + /// + /// The Vector3 position of the vertex whose index is being sought. + /// The dictionary maintaining a record of + /// (vertex position, vertex list index). + /// The list of vertices in the mesh being processed. + private static int GetVertexIndex(Vector3 vertex, ref Dictionary vertexPositionToIndex, + ref List vertices) + { + int vertexIndex; + if (!vertexPositionToIndex.TryGetValue(vertex, out vertexIndex)) + { + vertices.Add(vertex); + vertexIndex = vertices.Count; + vertexPositionToIndex.Add(vertex, vertexIndex); + } + return vertexIndex; } - Vector3 curNormal = (mesh.rotation * MeshMath.CalculateNormal(faceVertexPositions)).normalized; - - Vector3 tangent; - Vector3 binormal; - GetTangentSpaceBasis(curNormal, out tangent, out binormal); - - for (int i = 0; i < face.vertexIds.Count; i++) { - int vertexId = face.vertexIds[i]; - int vertexIndex; - Vector3 modelCoords = mesh.VertexPositionInModelCoords(face.vertexIds[i]); - if (!vertexIdToIndex.TryGetValue(vertexId, out vertexIndex)) { - vertices.Add(modelCoords); - vertexIndex = vertices.Count; - vertexIdToIndex.Add(vertexId, vertexIndex); - } - - uvs.Add(new Vector2(Vector3.Dot(tangent, modelCoords) / 10f, Vector3.Dot(binormal, modelCoords)/ 10f)); - vertexIdsForFace[i] = vertexIndex; - uvsIdsForFace[i] = uvs.Count; + + /// + /// Adds a triangulated MMesh's export information to the given lists of vertices, uvs, mesh normals, and materials. + /// + /// The MMesh to be added. + /// A cache of triangulated meshes. + /// The list of vertices to be updated. + /// The UV vertices to be calculated. + /// Export information about the mesh faces, including face normals, colors, uvs, + /// and vertices to be calculated. + /// The face normals of the mesh to be calculated. + /// The set of materials that have been seen and should be added to the .mtl file. + /// A running total of the poly count of this export. + private static void AddTriangulatedMesh(MMesh mesh, MeshRepresentationCache meshRepresentationCache, + ref List vertices, + ref List uvs, + ref MeshExportInfo meshExportInfo, + ref List meshNormals, + ref HashSet materials, + ref int polyCount) + { + meshExportInfo.meshFaceColors = new List(); + + // Maintain a dictionary of vertex position keyed to its index in the vertices list this method updates. + // We use because vertices are stored in the triangulated MeshGenContext as Vector3s, + // not Blocks vertex ids. This lets us not duplicate vertices that are shared across faces in a mesh. + Dictionary vertexPositionToIndex = new Dictionary(); + // We attempt to look up the triangulation in a cache for efficiency; if it is not there it is triangulated + // on the fly. + Dictionary meshInfoByMaterial = + meshRepresentationCache.FetchComponentsForMesh(mesh.id, /* abortOnCacheMiss */ false); + // Note that meshInfoByMaterial contains a "sub-mesh" for each material. Next we will process each of these + // sub-meshes in turn. + foreach (KeyValuePair pair in meshInfoByMaterial) + { + int materialId = pair.Key; + MeshGenContext meshGenContext = pair.Value; + + List triangles = meshGenContext.triangles; + for (int i = 0; i < triangles.Count; i += 3) + { + polyCount++; + int vertexIndex1 = GetVertexIndex(meshGenContext.verts[triangles[i]], ref vertexPositionToIndex, + ref vertices); + int vertexIndex2 = GetVertexIndex(meshGenContext.verts[triangles[i + 1]], ref vertexPositionToIndex, + ref vertices); + int vertexIndex3 = GetVertexIndex(meshGenContext.verts[triangles[i + 2]], ref vertexPositionToIndex, + ref vertices); + + meshExportInfo.meshFaceVerts.Add(new int[] { vertexIndex1, vertexIndex2, vertexIndex3 }); + meshExportInfo.meshFaceColors.Add(materialId); + materials.Add(materialId); + + // TODO (bug) This normal calculation is duplicate work (calculating the normal for + // every triangle in a triangulated face) and should be made more efficient. + Vector3 normal = MeshMath.CalculateNormal( + meshGenContext.verts[triangles[i]], + meshGenContext.verts[triangles[i + 1]], + meshGenContext.verts[triangles[i + 2]]); + + Vector3 tangent, binormal; + GetTangentSpaceBasis(normal, out tangent, out binormal); + + uvs.Add(GetVertexUv(tangent, binormal, meshGenContext.verts[triangles[i]])); + uvs.Add(GetVertexUv(tangent, binormal, meshGenContext.verts[triangles[i + 1]])); + uvs.Add(GetVertexUv(tangent, binormal, meshGenContext.verts[triangles[i + 2]])); + + int[] uvsIdsForFace = new int[] { uvs.Count - 2, uvs.Count - 1, uvs.Count }; + meshExportInfo.meshFaceUvs.Add(uvsIdsForFace); + meshExportInfo.meshNormals.Add(meshNormals.Count); + meshNormals.Add(normal); + } + } } - meshExportInfo.meshFaceVerts.Add(vertexIdsForFace); - meshExportInfo.meshFaceUvs.Add(uvsIdsForFace); - meshExportInfo.meshNormals.Add(meshNormals.Count); - meshNormals.Add(curNormal); - } + /// + /// Adds a MMesh's export information to the given lists of vertices, uvs, mesh normals, and materials. + /// + /// The given MMesh + /// The list of vertices to be updated. + /// The UV vertices to be calculated. + /// Export information about the mesh faces, including face normals, colors, uvs, + /// and vertices to be calculated. + /// The face normals of the mesh to be calculated. + /// The set of materials that have been seen and should be added to the .mtl file. + /// A running total of the poly count of this export. + private static void AddMesh(MMesh mesh, + ref List vertices, + ref List uvs, + ref MeshExportInfo meshExportInfo, + ref List meshNormals, + ref HashSet materials, + ref int polyCount) + { + meshExportInfo.meshFaceColors = new List(mesh.faceCount); + + // We do not wish to duplicate vertices that are shared across faces. As such, we maintain a dictionary from + // Vertex.id to the index in the vertices list this method updates. We only need to maintain this dictionary + // per mesh, as Vertex.id is only shared within a single MMesh. + Dictionary vertexIdToIndex = new Dictionary(mesh.vertexCount); + + foreach (Face face in mesh.GetFaces()) + { + polyCount++; + meshExportInfo.meshFaceColors.Add(face.properties.materialId); + // Record face color for .mtl file, if not seen already. + materials.Add(face.properties.materialId); + // We cannot use poly vertex ids, we need the position of the vertex in 'vertices' as the identifier. + int[] vertexIdsForFace = new int[face.vertexIds.Count]; + int[] uvsIdsForFace = new int[face.vertexIds.Count]; + + // TODO(64715939): Calculate the normal for each face - once we're more confident in the normals stored in each + // face, we can switch to using them directly. + List faceVertexPositions = new List(face.vertexIds.Count); + for (int i = 0; i < face.vertexIds.Count; i++) + { + faceVertexPositions.Add(mesh.VertexPositionInMeshCoords(face.vertexIds[i])); + } + Vector3 curNormal = (mesh.rotation * MeshMath.CalculateNormal(faceVertexPositions)).normalized; + + Vector3 tangent; + Vector3 binormal; + GetTangentSpaceBasis(curNormal, out tangent, out binormal); + + for (int i = 0; i < face.vertexIds.Count; i++) + { + int vertexId = face.vertexIds[i]; + int vertexIndex; + Vector3 modelCoords = mesh.VertexPositionInModelCoords(face.vertexIds[i]); + if (!vertexIdToIndex.TryGetValue(vertexId, out vertexIndex)) + { + vertices.Add(modelCoords); + vertexIndex = vertices.Count; + vertexIdToIndex.Add(vertexId, vertexIndex); + } + + uvs.Add(new Vector2(Vector3.Dot(tangent, modelCoords) / 10f, Vector3.Dot(binormal, modelCoords) / 10f)); + vertexIdsForFace[i] = vertexIndex; + uvsIdsForFace[i] = uvs.Count; + } + + meshExportInfo.meshFaceVerts.Add(vertexIdsForFace); + meshExportInfo.meshFaceUvs.Add(uvsIdsForFace); + meshExportInfo.meshNormals.Add(meshNormals.Count); + meshNormals.Add(curNormal); + } + } } - } } diff --git a/Assets/Scripts/model/export/PeltzerFile.cs b/Assets/Scripts/model/export/PeltzerFile.cs index c5f1efb3..345b20c0 100644 --- a/Assets/Scripts/model/export/PeltzerFile.cs +++ b/Assets/Scripts/model/export/PeltzerFile.cs @@ -20,171 +20,188 @@ using System.Linq; using UnityEngine; -namespace com.google.apps.peltzer.client.model.export { - // The top-level definition of a Peltzer file, and subject classes: go/peltzer-file-format - public class PeltzerFile { - /// - /// File format version when serializing via PolySerializer. Increase this when making incompatible changes to - /// the serialization format, that is, when making changes that older versions won't be able to parse. - /// - /// Remember that small (additive) changes do not need a file format version change, as you can just - /// add new chunks to PolySerializer that will be skipped by older versions. - /// - /// This version number should be increased RARELY. Most incremental file format changes should be - /// implemented as new chunks in PolySerializer that can be safely ignored by older versions. - /// Remember that if you increment this, files generated by your new version WILL NOT LOAD AT ALL in - /// older Poly versions. - /// - private const int FILE_FORMAT_VERSION = 1; - - public Metadata metadata; - public float zoomFactor; - public List materials; - public List meshes; - - public PeltzerFile(Metadata metadata, float zoomFactor, List materials, - List meshes) { - this.metadata = metadata; - this.zoomFactor = zoomFactor; - this.materials = materials; - this.meshes = meshes; +namespace com.google.apps.peltzer.client.model.export +{ + // The top-level definition of a Peltzer file, and subject classes: go/peltzer-file-format + public class PeltzerFile + { + /// + /// File format version when serializing via PolySerializer. Increase this when making incompatible changes to + /// the serialization format, that is, when making changes that older versions won't be able to parse. + /// + /// Remember that small (additive) changes do not need a file format version change, as you can just + /// add new chunks to PolySerializer that will be skipped by older versions. + /// + /// This version number should be increased RARELY. Most incremental file format changes should be + /// implemented as new chunks in PolySerializer that can be safely ignored by older versions. + /// Remember that if you increment this, files generated by your new version WILL NOT LOAD AT ALL in + /// older Poly versions. + /// + private const int FILE_FORMAT_VERSION = 1; + + public Metadata metadata; + public float zoomFactor; + public List materials; + public List meshes; + + public PeltzerFile(Metadata metadata, float zoomFactor, List materials, + List meshes) + { + this.metadata = metadata; + this.zoomFactor = zoomFactor; + this.materials = materials; + this.meshes = meshes; + } + + /// + // Serialize to PolySerializer. + /// + /// Whether or not to include the recommended model display rotation + /// in save. + public void Serialize(PolySerializer serializer, bool includeDisplayRotation = false) + { + serializer.StartWritingChunk(SerializationConsts.CHUNK_PELTZER); + serializer.WriteInt(FILE_FORMAT_VERSION); + serializer.WriteString(metadata.creatorName); + serializer.WriteString(metadata.creationDate); + serializer.WriteString(metadata.version); + serializer.WriteFloat(zoomFactor); + serializer.WriteCount(materials.Count); + for (int i = 0; i < materials.Count; i++) + { + serializer.WriteInt(materials[i].materialId); + serializer.WriteInt(materials[i].color); + } + serializer.WriteCount(meshes.Count); + serializer.FinishWritingChunk(SerializationConsts.CHUNK_PELTZER); + + // Write the meshes. Each mesh is written as a separate chunk. + for (int i = 0; i < meshes.Count; i++) + { + meshes[i].Serialize(serializer); + } + + // Write the angle of rotation of the model that orients it towards the user, for a more intelligent + // rotation on the Poly menu when this model is loaded. + if (includeDisplayRotation) + { + Vector3 centroid = Math3d.FindCentroid(meshes); + float recommendedRotation = GetUserFacingModelRotation(PeltzerMain.Instance.worldSpace.ModelToWorld(centroid)); + + // Write rotation to new chunk. + serializer.StartWritingChunk(SerializationConsts.CHUNK_PELTZER_EXT_MODEL_ROTATION); + serializer.WriteFloat(recommendedRotation); + serializer.FinishWritingChunk(SerializationConsts.CHUNK_PELTZER_EXT_MODEL_ROTATION); + } + } + + /// + /// Returns the rotation about the y-axis that maintains the current orientation between the user + /// and the model, accounting for world space rotation. + /// + /// The centroid of the model, in world-space coordinates. + float GetUserFacingModelRotation(Vector3 centroid) + { + // Calculate the desired forward; in this case, the relative position of the centroid to the camera. + Vector3 relativePos = centroid - PeltzerMain.Instance.eyeCameraPosition; + Quaternion rotationAngle = Quaternion.LookRotation(relativePos); + // Negate the rotation to get the correct forward for the model when positioned on the menu, + // such that it maintains its orientation towards the user. Use only the Y-axis rotation because + // worldspace is fixed on the y-axis, so the majority of models will have the correct up orientation. + rotationAngle.eulerAngles = new Vector3(0f, -rotationAngle.eulerAngles.y, 0f); + rotationAngle *= PeltzerMain.Instance.worldSpace.rotation; + return rotationAngle.eulerAngles.y; + } + + /// + /// Gets a (generous) estimate on how large the serialized file will be. + /// Although not 100% guaranteed, this is designed to OVERESTIMATE the size, such that if this value is + /// used to allocate a buffer, the buffer should be large enough to hold the serialization so there shouldn't + /// be a need to re-allocate it. + /// + /// The (over-)estimated size of the serialized file. + public int GetSerializedSizeEstimate() + { + int estimate = + // A more than generous estimate for headers, metadata and such: + 32768 + + // Materials are 2 ints each: + materials.Count() * 8; + + // Calculate estimate for meshes: + for (int i = 0; i < meshes.Count; i++) + { + estimate += meshes[i].GetSerializedSizeEstimate(); + } + + // TODO: estimate size of commands as well. + + return estimate; + } + + // Deserialize from PolySerializer. + public PeltzerFile(PolySerializer serializer) + { + serializer.StartReadingChunk(SerializationConsts.CHUNK_PELTZER); + int formatVersion = serializer.ReadInt(); + AssertOrThrow.True(formatVersion == FILE_FORMAT_VERSION, + "Wrong file format version: " + formatVersion + ", expected " + FILE_FORMAT_VERSION); + string creatorName = serializer.ReadString(); + string creationDate = serializer.ReadString(); + string version = serializer.ReadString(); + metadata = new Metadata(creatorName, creationDate, version); + zoomFactor = serializer.ReadFloat(); + + int materialCount = serializer.ReadCount(0, SerializationConsts.MAX_MATERIALS_PER_FILE, "materialCount"); + materials = new List(materialCount); + for (int i = 0; i < materialCount; i++) + { + int materialId = serializer.ReadInt(); + int color = serializer.ReadInt(); + materials.Add(new PeltzerMaterial(materialId, color)); + } + int meshCount = serializer.ReadCount(0, SerializationConsts.MAX_MESHES_PER_FILE, "meshCount"); + serializer.FinishReadingChunk(SerializationConsts.CHUNK_PELTZER); + + // Read meshes. + meshes = new List(); + for (int i = 0; i < meshCount; i++) + { + meshes.Add(new MMesh(serializer)); + } + + // If the recommended model rotation is present (it's optional), read it. + if (serializer.GetNextChunkLabel() == SerializationConsts.CHUNK_PELTZER_EXT_MODEL_ROTATION) + { + serializer.StartReadingChunk(SerializationConsts.CHUNK_PELTZER_EXT_MODEL_ROTATION); + metadata.recommendedRotation = serializer.ReadFloat(); + serializer.FinishReadingChunk(SerializationConsts.CHUNK_PELTZER_EXT_MODEL_ROTATION); + } + } } - /// - // Serialize to PolySerializer. - /// - /// Whether or not to include the recommended model display rotation - /// in save. - public void Serialize(PolySerializer serializer, bool includeDisplayRotation = false) { - serializer.StartWritingChunk(SerializationConsts.CHUNK_PELTZER); - serializer.WriteInt(FILE_FORMAT_VERSION); - serializer.WriteString(metadata.creatorName); - serializer.WriteString(metadata.creationDate); - serializer.WriteString(metadata.version); - serializer.WriteFloat(zoomFactor); - serializer.WriteCount(materials.Count); - for (int i = 0; i < materials.Count; i++) { - serializer.WriteInt(materials[i].materialId); - serializer.WriteInt(materials[i].color); - } - serializer.WriteCount(meshes.Count); - serializer.FinishWritingChunk(SerializationConsts.CHUNK_PELTZER); - - // Write the meshes. Each mesh is written as a separate chunk. - for (int i = 0; i < meshes.Count; i++) { - meshes[i].Serialize(serializer); - } - - // Write the angle of rotation of the model that orients it towards the user, for a more intelligent - // rotation on the Poly menu when this model is loaded. - if (includeDisplayRotation) { - Vector3 centroid = Math3d.FindCentroid(meshes); - float recommendedRotation = GetUserFacingModelRotation(PeltzerMain.Instance.worldSpace.ModelToWorld(centroid)); - - // Write rotation to new chunk. - serializer.StartWritingChunk(SerializationConsts.CHUNK_PELTZER_EXT_MODEL_ROTATION); - serializer.WriteFloat(recommendedRotation); - serializer.FinishWritingChunk(SerializationConsts.CHUNK_PELTZER_EXT_MODEL_ROTATION); - } + // Basic metadata about a Peltzer file. + public class Metadata + { + public string creatorName; + public string creationDate; + public string version; + public float recommendedRotation; + public Metadata(string creatorName, string creationDate, string version) + { + this.creatorName = creatorName; this.creationDate = creationDate; this.version = version; + this.recommendedRotation = 0f; + } } - /// - /// Returns the rotation about the y-axis that maintains the current orientation between the user - /// and the model, accounting for world space rotation. - /// - /// The centroid of the model, in world-space coordinates. - float GetUserFacingModelRotation(Vector3 centroid) { - // Calculate the desired forward; in this case, the relative position of the centroid to the camera. - Vector3 relativePos = centroid - PeltzerMain.Instance.eyeCameraPosition; - Quaternion rotationAngle = Quaternion.LookRotation(relativePos); - // Negate the rotation to get the correct forward for the model when positioned on the menu, - // such that it maintains its orientation towards the user. Use only the Y-axis rotation because - // worldspace is fixed on the y-axis, so the majority of models will have the correct up orientation. - rotationAngle.eulerAngles = new Vector3(0f, -rotationAngle.eulerAngles.y, 0f); - rotationAngle *= PeltzerMain.Instance.worldSpace.rotation; - return rotationAngle.eulerAngles.y; + // A material used in a Peltzer file. + public class PeltzerMaterial + { + // oneof materialId/color + public int materialId; + public int color; // e.g. 0xF06292 + public PeltzerMaterial(int materialId) { this.materialId = materialId; } + public PeltzerMaterial(int color, bool ignored) { this.color = color; } + public PeltzerMaterial(int materialId, int color) { this.materialId = materialId; this.color = color; } } - - /// - /// Gets a (generous) estimate on how large the serialized file will be. - /// Although not 100% guaranteed, this is designed to OVERESTIMATE the size, such that if this value is - /// used to allocate a buffer, the buffer should be large enough to hold the serialization so there shouldn't - /// be a need to re-allocate it. - /// - /// The (over-)estimated size of the serialized file. - public int GetSerializedSizeEstimate() { - int estimate = - // A more than generous estimate for headers, metadata and such: - 32768 + - // Materials are 2 ints each: - materials.Count() * 8; - - // Calculate estimate for meshes: - for (int i = 0; i < meshes.Count; i++) { - estimate += meshes[i].GetSerializedSizeEstimate(); - } - - // TODO: estimate size of commands as well. - - return estimate; - } - - // Deserialize from PolySerializer. - public PeltzerFile(PolySerializer serializer) { - serializer.StartReadingChunk(SerializationConsts.CHUNK_PELTZER); - int formatVersion = serializer.ReadInt(); - AssertOrThrow.True(formatVersion == FILE_FORMAT_VERSION, - "Wrong file format version: " + formatVersion + ", expected " + FILE_FORMAT_VERSION); - string creatorName = serializer.ReadString(); - string creationDate = serializer.ReadString(); - string version = serializer.ReadString(); - metadata = new Metadata(creatorName, creationDate, version); - zoomFactor = serializer.ReadFloat(); - - int materialCount = serializer.ReadCount(0, SerializationConsts.MAX_MATERIALS_PER_FILE, "materialCount"); - materials = new List(materialCount); - for (int i = 0; i < materialCount; i++) { - int materialId = serializer.ReadInt(); - int color = serializer.ReadInt(); - materials.Add(new PeltzerMaterial(materialId, color)); - } - int meshCount = serializer.ReadCount(0, SerializationConsts.MAX_MESHES_PER_FILE, "meshCount"); - serializer.FinishReadingChunk(SerializationConsts.CHUNK_PELTZER); - - // Read meshes. - meshes = new List(); - for (int i = 0; i < meshCount; i++) { - meshes.Add(new MMesh(serializer)); - } - - // If the recommended model rotation is present (it's optional), read it. - if (serializer.GetNextChunkLabel() == SerializationConsts.CHUNK_PELTZER_EXT_MODEL_ROTATION) { - serializer.StartReadingChunk(SerializationConsts.CHUNK_PELTZER_EXT_MODEL_ROTATION); - metadata.recommendedRotation = serializer.ReadFloat(); - serializer.FinishReadingChunk(SerializationConsts.CHUNK_PELTZER_EXT_MODEL_ROTATION); - } - } - } - - // Basic metadata about a Peltzer file. - public class Metadata { - public string creatorName; - public string creationDate; - public string version; - public float recommendedRotation; - public Metadata(string creatorName, string creationDate, string version) { - this.creatorName = creatorName; this.creationDate = creationDate; this.version = version; - this.recommendedRotation = 0f; - } - } - - // A material used in a Peltzer file. - public class PeltzerMaterial { - // oneof materialId/color - public int materialId; - public int color; // e.g. 0xF06292 - public PeltzerMaterial(int materialId) { this.materialId = materialId; } - public PeltzerMaterial(int color, bool ignored) { this.color = color; } - public PeltzerMaterial(int materialId, int color) { this.materialId = materialId; this.color = color; } - } } diff --git a/Assets/Scripts/model/export/PeltzerFileHandler.cs b/Assets/Scripts/model/export/PeltzerFileHandler.cs index 2dbab5ca..f4e7d3f5 100644 --- a/Assets/Scripts/model/export/PeltzerFileHandler.cs +++ b/Assets/Scripts/model/export/PeltzerFileHandler.cs @@ -23,131 +23,154 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.serialization; -namespace com.google.apps.peltzer.client.model.export { - /// - /// Export & import logic for Peltzer files: go/peltzer-file-format - /// - public class PeltzerFileHandler { +namespace com.google.apps.peltzer.client.model.export +{ /// - /// Generates a Peltzer file from the given meshes. + /// Export & import logic for Peltzer files: go/peltzer-file-format /// - /// The meshes to serialize. - /// Whether or not to include the recommended model display rotation - /// in save. - /// Optionally, the Serializer to use (used for the V3_POLY file format only). - /// The bytes of the Peltzer file - public static byte[] PeltzerFileFromMeshes(ICollection meshes, bool includeDisplayRotation = false, PolySerializer serializer = null) { - // TODO(bug): Get the username/date. - Metadata metadata = new Metadata("temp_username", "temp_creation_date", Config.Instance.version); - // TODO(bug): Get the real zoom factor. - float zoomFactor = 1; + public class PeltzerFileHandler + { + /// + /// Generates a Peltzer file from the given meshes. + /// + /// The meshes to serialize. + /// Whether or not to include the recommended model display rotation + /// in save. + /// Optionally, the Serializer to use (used for the V3_POLY file format only). + /// The bytes of the Peltzer file + public static byte[] PeltzerFileFromMeshes(ICollection meshes, bool includeDisplayRotation = false, PolySerializer serializer = null) + { + // TODO(bug): Get the username/date. + Metadata metadata = new Metadata("temp_username", "temp_creation_date", Config.Instance.version); + // TODO(bug): Get the real zoom factor. + float zoomFactor = 1; - // Find all materials used in the given meshes. - HashSet materialsUsed = new HashSet(); - foreach (MMesh mesh in meshes) { - foreach (Face face in mesh.GetFaces()) { - materialsUsed.Add(face.properties.materialId); - } - } - - List materials = new List(); - foreach (int materialId in materialsUsed) { - // TODO(bug): Deal with saving custom materials. - materials.Add(new PeltzerMaterial(materialId)); - } + // Find all materials used in the given meshes. + HashSet materialsUsed = new HashSet(); + foreach (MMesh mesh in meshes) + { + foreach (Face face in mesh.GetFaces()) + { + materialsUsed.Add(face.properties.materialId); + } + } - PeltzerFile peltzerFile = new PeltzerFile(metadata, zoomFactor, materials, meshes.ToList()); + List materials = new List(); + foreach (int materialId in materialsUsed) + { + // TODO(bug): Deal with saving custom materials. + materials.Add(new PeltzerMaterial(materialId)); + } - if (serializer == null) { - serializer = new PolySerializer(); - } - int estimate = peltzerFile.GetSerializedSizeEstimate(); - serializer.SetupForWriting(/* minInitialCapacity */ estimate); - peltzerFile.Serialize(serializer, includeDisplayRotation); - serializer.FinishWriting(); - byte[] result = serializer.ToByteArray(); + PeltzerFile peltzerFile = new PeltzerFile(metadata, zoomFactor, materials, meshes.ToList()); - if (result.Length > estimate) { - // This indicates a bug in the estimation logic. It's not a serious bug because the only consequence - // is the reallocation of the buffer on save. But let's print an error just so we know something - // is wrong: - Debug.LogError("Actual serialized length was above estimate. Estimate " + estimate + - ", actual " + result.Length + ". No harm done, but loading code may be generating more " + - "garbage than necessary."); - } + if (serializer == null) + { + serializer = new PolySerializer(); + } + int estimate = peltzerFile.GetSerializedSizeEstimate(); + serializer.SetupForWriting(/* minInitialCapacity */ estimate); + peltzerFile.Serialize(serializer, includeDisplayRotation); + serializer.FinishWriting(); + byte[] result = serializer.ToByteArray(); - return result; - } + if (result.Length > estimate) + { + // This indicates a bug in the estimation logic. It's not a serious bug because the only consequence + // is the reallocation of the buffer on save. But let's print an error just so we know something + // is wrong: + Debug.LogError("Actual serialized length was above estimate. Estimate " + estimate + + ", actual " + result.Length + ". No harm done, but loading code may be generating more " + + "garbage than necessary."); + } - /// - /// Filters out all commands associated with ReferenceImages. - /// - /// - /// - private static List FilterOutReferenceImageCommands(List commands) { - List filtered = new List(); - foreach (Command command in commands) { - if (command.GetType() == typeof(AddReferenceImageCommand) || command.GetType() == typeof(DeleteReferenceImageCommand)) { - continue; - } - if (command.GetType() == typeof(CompositeCommand)) { - List filteredCompositeCommand = FilterOutReferenceImageCommands((command as CompositeCommand).GetCommands()); - if (filteredCompositeCommand.Count > 0) { - filtered.Add(new CompositeCommand(filteredCompositeCommand)); - } - } else { - filtered.Add(command); + return result; } - } - return filtered; - } - /// - /// Deserializes a Peltzer file. - /// - /// The bytes of the Peltzer file. - /// The decoded model. - /// True if the file wasn't corrupt. - public static bool PeltzerFileFromBytes(byte[] peltzerFileBytes, out PeltzerFile peltzerFile) { - if (peltzerFileBytes.Length == 0) { - Debug.LogErrorFormat("No bytes loaded for peltzerFile."); - peltzerFile = null; - return false; - } + /// + /// Filters out all commands associated with ReferenceImages. + /// + /// + /// + private static List FilterOutReferenceImageCommands(List commands) + { + List filtered = new List(); + foreach (Command command in commands) + { + if (command.GetType() == typeof(AddReferenceImageCommand) || command.GetType() == typeof(DeleteReferenceImageCommand)) + { + continue; + } + if (command.GetType() == typeof(CompositeCommand)) + { + List filteredCompositeCommand = FilterOutReferenceImageCommands((command as CompositeCommand).GetCommands()); + if (filteredCompositeCommand.Count > 0) + { + filtered.Add(new CompositeCommand(filteredCompositeCommand)); + } + } + else + { + filtered.Add(command); + } + } + return filtered; + } - if (!LoadPolyFileFormat(peltzerFileBytes, out peltzerFile)) { - Debug.LogErrorFormat("Failed to load PeltzerFile. Invalid format."); - peltzerFile = null; - return false; - } - return true; - } + /// + /// Deserializes a Peltzer file. + /// + /// The bytes of the Peltzer file. + /// The decoded model. + /// True if the file wasn't corrupt. + public static bool PeltzerFileFromBytes(byte[] peltzerFileBytes, out PeltzerFile peltzerFile) + { + if (peltzerFileBytes.Length == 0) + { + Debug.LogErrorFormat("No bytes loaded for peltzerFile."); + peltzerFile = null; + return false; + } - private static bool LoadPolyFileFormat(byte[] peltzerFileBytes, out PeltzerFile peltzerFile) { - try { - // Note: since we potentially support different file formats, at this point for all we know the - // byte buffer might not even be in the right format, so we first check to see if it has a valid - // header before proceeding. We could skip this check and proceed anyway and we would fail later, - // but it would be more noisy and look like a fatal error when in fact it's just a case of "oops, - // we chose the wrong file format, let's try another one". - if (!PolySerializer.HasValidHeader(peltzerFileBytes, 0, peltzerFileBytes.Length)) { - // Not in the Poly file format. - peltzerFile = null; - return false; + if (!LoadPolyFileFormat(peltzerFileBytes, out peltzerFile)) + { + Debug.LogErrorFormat("Failed to load PeltzerFile. Invalid format."); + peltzerFile = null; + return false; + } + return true; } - // Note: we don't mind creating a new serializer here instead of trying to re-use an existing one because - // there is negligible overhead in creating a serializer for READING (since it doesn't allocate a buffer). - // So it's pretty cheap to create a new one every time we load a file. - PolySerializer serializer = new PolySerializer(); - serializer.SetupForReading(peltzerFileBytes, 0, peltzerFileBytes.Length); - peltzerFile = new PeltzerFile(serializer); - return true; - } catch (System.Exception ex) { - Debug.LogError("Error while reading Poly file format: " + ex); - peltzerFile = null; - return false; - } + private static bool LoadPolyFileFormat(byte[] peltzerFileBytes, out PeltzerFile peltzerFile) + { + try + { + // Note: since we potentially support different file formats, at this point for all we know the + // byte buffer might not even be in the right format, so we first check to see if it has a valid + // header before proceeding. We could skip this check and proceed anyway and we would fail later, + // but it would be more noisy and look like a fatal error when in fact it's just a case of "oops, + // we chose the wrong file format, let's try another one". + if (!PolySerializer.HasValidHeader(peltzerFileBytes, 0, peltzerFileBytes.Length)) + { + // Not in the Poly file format. + peltzerFile = null; + return false; + } + + // Note: we don't mind creating a new serializer here instead of trying to re-use an existing one because + // there is negligible overhead in creating a serializer for READING (since it doesn't allocate a buffer). + // So it's pretty cheap to create a new one every time we load a file. + PolySerializer serializer = new PolySerializer(); + serializer.SetupForReading(peltzerFileBytes, 0, peltzerFileBytes.Length); + peltzerFile = new PeltzerFile(serializer); + return true; + } + catch (System.Exception ex) + { + Debug.LogError("Error while reading Poly file format: " + ex); + peltzerFile = null; + return false; + } + } } - } } diff --git a/Assets/Scripts/model/export/PolyGLTFExporter.cs b/Assets/Scripts/model/export/PolyGLTFExporter.cs index 58dea041..7b00fab2 100644 --- a/Assets/Scripts/model/export/PolyGLTFExporter.cs +++ b/Assets/Scripts/model/export/PolyGLTFExporter.cs @@ -21,241 +21,256 @@ using com.google.apps.peltzer.client.model.render; using UnityEngine; -namespace com.google.apps.peltzer.client.model.export { - /// - /// Container class for functions that implement gltf export. Contains no state. - /// - public class PolyGLTFExporter { +namespace com.google.apps.peltzer.client.model.export +{ + /// + /// Container class for functions that implement gltf export. Contains no state. + /// + public class PolyGLTFExporter + { - private static readonly string POLY_RESOURCE_PATH = "https://vr.google.com/shaders/w/"; + private static readonly string POLY_RESOURCE_PATH = "https://vr.google.com/shaders/w/"; - private static readonly string opaqueVSPath = POLY_RESOURCE_PATH + "vs.glsl"; - private static readonly string opaqueFSPath = POLY_RESOURCE_PATH + "fs.glsl"; + private static readonly string opaqueVSPath = POLY_RESOURCE_PATH + "vs.glsl"; + private static readonly string opaqueFSPath = POLY_RESOURCE_PATH + "fs.glsl"; - private static readonly string glassVSPath = POLY_RESOURCE_PATH + "glassVS.glsl"; - private static readonly string glassFSPath = POLY_RESOURCE_PATH + "glassFS.glsl"; + private static readonly string glassVSPath = POLY_RESOURCE_PATH + "glassVS.glsl"; + private static readonly string glassFSPath = POLY_RESOURCE_PATH + "glassFS.glsl"; - private static readonly string gemVSPath = POLY_RESOURCE_PATH + "gemVS.glsl"; - private static readonly string gemFSPath = POLY_RESOURCE_PATH + "gemFS.glsl"; - private static readonly string gemTexPath = POLY_RESOURCE_PATH + "GemRefractions.png"; + private static readonly string gemVSPath = POLY_RESOURCE_PATH + "gemVS.glsl"; + private static readonly string gemFSPath = POLY_RESOURCE_PATH + "gemFS.glsl"; + private static readonly string gemTexPath = POLY_RESOURCE_PATH + "GemRefractions.png"; - private static readonly string reflectionProbePath = "ReflectionProbe.png"; - private static readonly string reflectionProbeBlurPath = "ReflectionProbeBlur.png"; + private static readonly string reflectionProbePath = "ReflectionProbe.png"; + private static readonly string reflectionProbeBlurPath = "ReflectionProbeBlur.png"; - /// - /// Creates gltf save data for the meshes in the given remesher, and populates the raw bytes for the main - /// gltf file, the bin file, and all resources required to render the gltf (primarily shaders). - /// - /// We use data from a remesher as that is already optimized for rendering, which is the intent of our glTF export. - /// - public static FormatSaveData GLTFFileFromRemesher(ReMesher remesher, - string gltfFileName, - string gltfBinFileName, - MeshRepresentationCache meshRepresentationCache) { - GlTFScriptableExporter gltfExporter = new GlTFScriptableExporter(); - int meshNum = 0; - HashSet objectsToClean = new HashSet(); - HashSet usedMaterialIds = new HashSet(); - objectsToClean = new HashSet(); - GameObject rootNode = new GameObject(); - int triangleCount = 0; + /// + /// Creates gltf save data for the meshes in the given remesher, and populates the raw bytes for the main + /// gltf file, the bin file, and all resources required to render the gltf (primarily shaders). + /// + /// We use data from a remesher as that is already optimized for rendering, which is the intent of our glTF export. + /// + public static FormatSaveData GLTFFileFromRemesher(ReMesher remesher, + string gltfFileName, + string gltfBinFileName, + MeshRepresentationCache meshRepresentationCache) + { + GlTFScriptableExporter gltfExporter = new GlTFScriptableExporter(); + int meshNum = 0; + HashSet objectsToClean = new HashSet(); + HashSet usedMaterialIds = new HashSet(); + objectsToClean = new HashSet(); + GameObject rootNode = new GameObject(); + int triangleCount = 0; - // The preset object is used to tell the gltfExporter which materials exist and which shaders they use. - GlTF_Technique.States baseState = new GlTF_Technique.States(); - baseState.enable = new List {GlTF_Technique.Enable.DEPTH_TEST, GlTF_Technique.Enable.CULL_FACE}; - baseState.functions["cullFace"] = new GlTF_Technique.Value(1029); + // The preset object is used to tell the gltfExporter which materials exist and which shaders they use. + GlTF_Technique.States baseState = new GlTF_Technique.States(); + baseState.enable = new List { GlTF_Technique.Enable.DEPTH_TEST, GlTF_Technique.Enable.CULL_FACE }; + baseState.functions["cullFace"] = new GlTF_Technique.Value(1029); - GlTF_Technique.States softAdditive = new GlTF_Technique.States(); - softAdditive.enable = new List {GlTF_Technique.Enable.DEPTH_TEST, + GlTF_Technique.States softAdditive = new GlTF_Technique.States(); + softAdditive.enable = new List {GlTF_Technique.Enable.DEPTH_TEST, GlTF_Technique.Enable.BLEND}; - // Blend array format: [srcRGB, dstRGB, srcAlpha, dstAlpha] - // https://github.com/KhronosGroup/glTF/blob/master/specification/1.0/schema/technique.states.functions.schema.json#L40 - softAdditive.functions["blendFuncSeparate"] = - new GlTF_Technique.Value(new Vector4(775.0f, 774.0f, 773.0f, 772.0f)); // Alpha, OneMinusAlpha blending. - softAdditive.functions["depthMask"] = new GlTF_Technique.Value(true); // No depth write. - Preset matPresets = new Preset(); - matPresets.techniqueStates["mat" + MaterialRegistry.GLASS_ID] = softAdditive; - matPresets.techniqueStates["mat" + MaterialRegistry.GEM_ID] = baseState; + // Blend array format: [srcRGB, dstRGB, srcAlpha, dstAlpha] + // https://github.com/KhronosGroup/glTF/blob/master/specification/1.0/schema/technique.states.functions.schema.json#L40 + softAdditive.functions["blendFuncSeparate"] = + new GlTF_Technique.Value(new Vector4(775.0f, 774.0f, 773.0f, 772.0f)); // Alpha, OneMinusAlpha blending. + softAdditive.functions["depthMask"] = new GlTF_Technique.Value(true); // No depth write. + Preset matPresets = new Preset(); + matPresets.techniqueStates["mat" + MaterialRegistry.GLASS_ID] = softAdditive; + matPresets.techniqueStates["mat" + MaterialRegistry.GEM_ID] = baseState; - matPresets.techniqueExtras["mat" + MaterialRegistry.GLASS_ID] = - "{\"gvrss\" : \"https://vr.google.com/shaders/w/gvrss/glass.json\"}"; + matPresets.techniqueExtras["mat" + MaterialRegistry.GLASS_ID] = + "{\"gvrss\" : \"https://vr.google.com/shaders/w/gvrss/glass.json\"}"; - matPresets.techniqueExtras["mat" + MaterialRegistry.GEM_ID] = - "{\"gvrss\" : \"https://vr.google.com/shaders/w/gvrss/gem.json\"}"; + matPresets.techniqueExtras["mat" + MaterialRegistry.GEM_ID] = + "{\"gvrss\" : \"https://vr.google.com/shaders/w/gvrss/gem.json\"}"; - // Set shaders for all materials. - Material[] exportableMaterialList = MaterialRegistry.GetExportableMaterialList(); - for(int i = 0; i < MaterialRegistry.rawColors.Length; i++) { - matPresets.SetShaders("mat" + i, opaqueVSPath, opaqueFSPath); - matPresets.techniqueStates["mat" + i] = baseState; - matPresets.techniqueExtras["mat" + i] = - "{\"gvrss\" : \"https://vr.google.com/shaders/w/gvrss/paper.json\"}"; - } - matPresets.SetShaders("mat" + MaterialRegistry.GLASS_ID, glassVSPath, glassFSPath); - matPresets.SetShaders("mat" + MaterialRegistry.GEM_ID, gemVSPath, gemFSPath); + // Set shaders for all materials. + Material[] exportableMaterialList = MaterialRegistry.GetExportableMaterialList(); + for (int i = 0; i < MaterialRegistry.rawColors.Length; i++) + { + matPresets.SetShaders("mat" + i, opaqueVSPath, opaqueFSPath); + matPresets.techniqueStates["mat" + i] = baseState; + matPresets.techniqueExtras["mat" + i] = + "{\"gvrss\" : \"https://vr.google.com/shaders/w/gvrss/paper.json\"}"; + } + matPresets.SetShaders("mat" + MaterialRegistry.GLASS_ID, glassVSPath, glassFSPath); + matPresets.SetShaders("mat" + MaterialRegistry.GEM_ID, gemVSPath, gemFSPath); - GlTFScriptableExporter.TransformFilter filter = (Transform tr) => Matrix4x4.identity; + GlTFScriptableExporter.TransformFilter filter = (Transform tr) => Matrix4x4.identity; - gltfExporter.BeginExport(gltfFileName, matPresets, filter); - gltfExporter.SetMetadata(Config.Instance.appName + " " + Config.Instance.version, "1.1", "Unknown"); - // Export each piece of mesh geometry - foreach (ReMesher.MeshInfo polyMeshInfo in remesher.GetAllMeshInfos()) { - meshNum = ExportMeshInfo(gltfExporter, - polyMeshInfo, - rootNode, - ref usedMaterialIds, - ref objectsToClean, - ref triangleCount, - meshNum); - } + gltfExporter.BeginExport(gltfFileName, matPresets, filter); + gltfExporter.SetMetadata(Config.Instance.appName + " " + Config.Instance.version, "1.1", "Unknown"); + // Export each piece of mesh geometry + foreach (ReMesher.MeshInfo polyMeshInfo in remesher.GetAllMeshInfos()) + { + meshNum = ExportMeshInfo(gltfExporter, + polyMeshInfo, + rootNode, + ref usedMaterialIds, + ref objectsToClean, + ref triangleCount, + meshNum); + } - GameObject lightingObject = ObjectFinder.ObjectById("ID_Lighting"); - Vector3 lightingPosition = Vector3.zero; - Lighting lighting = null; - if (lightingObject != null) { - lightingPosition = lightingObject.transform.position; - lighting = lightingObject.GetComponent(); - } - // Then for each material that is actually used, set material properties. - foreach(int i in usedMaterialIds) { - gltfExporter.BaseName = "mat" + i; - gltfExporter.AddMaterialWithDependencies(); - MaterialAndColor mat = MaterialRegistry.GetMaterialAndColorById(i); - Color col = mat.color; - // Make sure the material sets the shader uniform for color correctly since gltf isn't exporting vertex colors. - gltfExporter.ExportShaderUniform("color", col); - float roughness = mat.material.GetFloat("_Roughness"); - gltfExporter.ExportShaderUniform("roughness", roughness); - float metallic = mat.material.GetFloat("_Metallic"); - gltfExporter.ExportShaderUniform("metallic", metallic); - if (i == MaterialRegistry.GEM_ID) { - //u_ will be automatically prepended to gem - so shader uniform should be 'u_gem' - gltfExporter.ExportTexture("gem", gemTexPath); - } - //u_ will be automatically prepended - so shader uniform should be 'u_reflectionCube' - //gltfExporter.ExportTexture("reflectionCube", "ReflectionProbe.png"); - //u_ will be automatically prepended - so shader uniform should be 'u_reflectionCubeBlur' - //gltfExporter.ExportTexture("reflectionCubeBlur", "ReflectionProbeBlur.png"); - if (lighting != null) { - gltfExporter.ExportShaderUniform("light0Pos", lightingPosition); - Vector3 light0Color = new Vector3(lighting.lightColor.r, lighting.lightColor.g, lighting.lightColor.b) * - lighting.lightStrength; - gltfExporter.ExportShaderUniform("light0Color", light0Color * 0.8f); - gltfExporter.ExportShaderUniform("light1Pos", -lightingPosition); - Vector3 light1Color = - new Vector3(lighting.fillLightColor.r, lighting.fillLightColor.g, lighting.fillLightColor.b) * - lighting.fillLightStrength; - gltfExporter.ExportShaderUniform("light1Color", light1Color * 0.8f); - } - } - gltfExporter.EndExport(); + GameObject lightingObject = ObjectFinder.ObjectById("ID_Lighting"); + Vector3 lightingPosition = Vector3.zero; + Lighting lighting = null; + if (lightingObject != null) + { + lightingPosition = lightingObject.transform.position; + lighting = lightingObject.GetComponent(); + } + // Then for each material that is actually used, set material properties. + foreach (int i in usedMaterialIds) + { + gltfExporter.BaseName = "mat" + i; + gltfExporter.AddMaterialWithDependencies(); + MaterialAndColor mat = MaterialRegistry.GetMaterialAndColorById(i); + Color col = mat.color; + // Make sure the material sets the shader uniform for color correctly since gltf isn't exporting vertex colors. + gltfExporter.ExportShaderUniform("color", col); + float roughness = mat.material.GetFloat("_Roughness"); + gltfExporter.ExportShaderUniform("roughness", roughness); + float metallic = mat.material.GetFloat("_Metallic"); + gltfExporter.ExportShaderUniform("metallic", metallic); + if (i == MaterialRegistry.GEM_ID) + { + //u_ will be automatically prepended to gem - so shader uniform should be 'u_gem' + gltfExporter.ExportTexture("gem", gemTexPath); + } + //u_ will be automatically prepended - so shader uniform should be 'u_reflectionCube' + //gltfExporter.ExportTexture("reflectionCube", "ReflectionProbe.png"); + //u_ will be automatically prepended - so shader uniform should be 'u_reflectionCubeBlur' + //gltfExporter.ExportTexture("reflectionCubeBlur", "ReflectionProbeBlur.png"); + if (lighting != null) + { + gltfExporter.ExportShaderUniform("light0Pos", lightingPosition); + Vector3 light0Color = new Vector3(lighting.lightColor.r, lighting.lightColor.g, lighting.lightColor.b) * + lighting.lightStrength; + gltfExporter.ExportShaderUniform("light0Color", light0Color * 0.8f); + gltfExporter.ExportShaderUniform("light1Pos", -lightingPosition); + Vector3 light1Color = + new Vector3(lighting.fillLightColor.r, lighting.fillLightColor.g, lighting.fillLightColor.b) * + lighting.fillLightStrength; + gltfExporter.ExportShaderUniform("light1Color", light1Color * 0.8f); + } + } + gltfExporter.EndExport(); - // Read the bytes of our exported files into FormatSaveData - FormatSaveData gltfSaveData = new FormatSaveData(); - gltfSaveData.root = new FormatDataFile(); - gltfSaveData.root.fileName = ExportUtils.GLTF_FILENAME; - gltfSaveData.root.bytes = File.ReadAllBytes(gltfFileName); - gltfSaveData.root.mimeType = "model/gltf+json"; - gltfSaveData.root.tag = "gltf"; + // Read the bytes of our exported files into FormatSaveData + FormatSaveData gltfSaveData = new FormatSaveData(); + gltfSaveData.root = new FormatDataFile(); + gltfSaveData.root.fileName = ExportUtils.GLTF_FILENAME; + gltfSaveData.root.bytes = File.ReadAllBytes(gltfFileName); + gltfSaveData.root.mimeType = "model/gltf+json"; + gltfSaveData.root.tag = "gltf"; - // Export the triangle count so it can be uploaded to the asset service - gltfSaveData.triangleCount = triangleCount; + // Export the triangle count so it can be uploaded to the asset service + gltfSaveData.triangleCount = triangleCount; - // Export all required resource files. - // This capacity is just counted by hand for now. - // 1. gltf bin file - // 2. default pixel shader - // 3. default fragment shader - // This will be of variable length in the future. + // Export all required resource files. + // This capacity is just counted by hand for now. + // 1. gltf bin file + // 2. default pixel shader + // 3. default fragment shader + // This will be of variable length in the future. - // Export the bytes of the bin file for gltf. - gltfSaveData.resources = new List(3); - FormatDataFile binFile = new FormatDataFile(); - binFile.fileName = ExportUtils.GLTF_BIN_FILENAME; - binFile.bytes = File.ReadAllBytes(gltfBinFileName); - binFile.mimeType = "application/octet-stream"; - binFile.tag = "bin"; - gltfSaveData.resources.Add(binFile); - // Clean up game objects - foreach(GameObject obj in objectsToClean) - { - GameObject.Destroy(obj); - } + // Export the bytes of the bin file for gltf. + gltfSaveData.resources = new List(3); + FormatDataFile binFile = new FormatDataFile(); + binFile.fileName = ExportUtils.GLTF_BIN_FILENAME; + binFile.bytes = File.ReadAllBytes(gltfBinFileName); + binFile.mimeType = "application/octet-stream"; + binFile.tag = "bin"; + gltfSaveData.resources.Add(binFile); + // Clean up game objects + foreach (GameObject obj in objectsToClean) + { + GameObject.Destroy(obj); + } - return gltfSaveData; - } + return gltfSaveData; + } - private static FormatDataFile addTextResource(String name, String path) { - TextAsset shaderAsset = Resources.Load(path); - FormatDataFile shaderFile = new FormatDataFile(); - shaderFile.fileName = name; - shaderFile.bytes = shaderAsset.bytes; - shaderFile.mimeType = "text/plain"; - shaderFile.tag = name; - return shaderFile; - } + private static FormatDataFile addTextResource(String name, String path) + { + TextAsset shaderAsset = Resources.Load(path); + FormatDataFile shaderFile = new FormatDataFile(); + shaderFile.fileName = name; + shaderFile.bytes = shaderAsset.bytes; + shaderFile.mimeType = "text/plain"; + shaderFile.tag = name; + return shaderFile; + } - private static FormatDataFile addTextureResource(String name, String path) { - Texture2D shaderAsset = Resources.Load(path); - FormatDataFile shaderFile = new FormatDataFile(); - shaderFile.fileName = name; - shaderFile.bytes = shaderAsset.EncodeToPNG(); - shaderFile.mimeType = "image/png"; - shaderFile.tag = name; - return shaderFile; - } + private static FormatDataFile addTextureResource(String name, String path) + { + Texture2D shaderAsset = Resources.Load(path); + FormatDataFile shaderFile = new FormatDataFile(); + shaderFile.fileName = name; + shaderFile.bytes = shaderAsset.EncodeToPNG(); + shaderFile.mimeType = "image/png"; + shaderFile.tag = name; + return shaderFile; + } - private static int ExportMeshInfo(GlTFScriptableExporter gltfExporter, - ReMesher.MeshInfo meshInfo, - GameObject rootNode, - ref HashSet usedMaterialIds, - ref HashSet objectsToClean, - ref int triangleCount, - int meshNum) { + private static int ExportMeshInfo(GlTFScriptableExporter gltfExporter, + ReMesher.MeshInfo meshInfo, + GameObject rootNode, + ref HashSet usedMaterialIds, + ref HashSet objectsToClean, + ref int triangleCount, + int meshNum) + { - Mesh exportableMesh = ReMesher.MeshInfo.BuildExportableMeshFromMeshInfo(meshInfo); - int matId = meshInfo.materialAndColor.matId; + Mesh exportableMesh = ReMesher.MeshInfo.BuildExportableMeshFromMeshInfo(meshInfo); + int matId = meshInfo.materialAndColor.matId; - // The TiltBrush gltfExporter doesn't seem to work properly for me without every mesh in its own transform - // (even if they're all identity transforms) - so for now we workaround this with a dummy transform. - GameObject tempObject = new GameObject(); - tempObject.transform.SetParent(rootNode.transform); - String matPrefix = "PolyPaper"; - if (matId == MaterialRegistry.GLASS_ID) { - matPrefix = "PolyGlass"; - } else if (matId == MaterialRegistry.GEM_ID) { - matPrefix = "PolyGem"; - } - tempObject.name = "MeshObject" + meshInfo.GetHashCode() + "-" + matPrefix + matId; - objectsToClean.Add(tempObject); - usedMaterialIds.Add(matId); + // The TiltBrush gltfExporter doesn't seem to work properly for me without every mesh in its own transform + // (even if they're all identity transforms) - so for now we workaround this with a dummy transform. + GameObject tempObject = new GameObject(); + tempObject.transform.SetParent(rootNode.transform); + String matPrefix = "PolyPaper"; + if (matId == MaterialRegistry.GLASS_ID) + { + matPrefix = "PolyGlass"; + } + else if (matId == MaterialRegistry.GEM_ID) + { + matPrefix = "PolyGem"; + } + tempObject.name = "MeshObject" + meshInfo.GetHashCode() + "-" + matPrefix + matId; + objectsToClean.Add(tempObject); + usedMaterialIds.Add(matId); - // context.triangles is a list of the vertices in the triangles. Each triple - // of elements is one triangle, so the total number of triangles in this mesh - // is one third the size of the triangles list. - triangleCount += meshInfo.triangles.Count / 3; + // context.triangles is a list of the vertices in the triangles. Each triple + // of elements is one triangle, so the total number of triangles in this mesh + // is one third the size of the triangles list. + triangleCount += meshInfo.triangles.Count / 3; - // The actual geometry is exported in the .bin file for efficiency - // This sets up the structure of the vertex data in the .bin file - GlTF_VertexLayout layout = new GlTF_VertexLayout(); - layout.hasColors = true; - layout.hasNormals = true; - layout.uv0 = GlTF_VertexLayout.UvElementCount.None; - layout.uv1 = GlTF_VertexLayout.UvElementCount.None; - layout.uv2 = GlTF_VertexLayout.UvElementCount.None; - layout.uv3 = GlTF_VertexLayout.UvElementCount.None; - layout.hasTangents = false; - layout.hasVertexIds = false; - // Give it a unique name - exportableMesh.name = "m" + meshNum + "-" + matPrefix + matId; - // Exporter needs us to set the active material name before we export the mesh - gltfExporter.BaseName = "mat" + matId; - gltfExporter.ExportSimpleMesh(exportableMesh, tempObject.transform, layout); - meshNum++; + // The actual geometry is exported in the .bin file for efficiency + // This sets up the structure of the vertex data in the .bin file + GlTF_VertexLayout layout = new GlTF_VertexLayout(); + layout.hasColors = true; + layout.hasNormals = true; + layout.uv0 = GlTF_VertexLayout.UvElementCount.None; + layout.uv1 = GlTF_VertexLayout.UvElementCount.None; + layout.uv2 = GlTF_VertexLayout.UvElementCount.None; + layout.uv3 = GlTF_VertexLayout.UvElementCount.None; + layout.hasTangents = false; + layout.hasVertexIds = false; + // Give it a unique name + exportableMesh.name = "m" + meshNum + "-" + matPrefix + matId; + // Exporter needs us to set the active material name before we export the mesh + gltfExporter.BaseName = "mat" + matId; + gltfExporter.ExportSimpleMesh(exportableMesh, tempObject.transform, layout); + meshNum++; - return meshNum; + return meshNum; + } } - } } diff --git a/Assets/Scripts/model/export/SerializableQuaternion.cs b/Assets/Scripts/model/export/SerializableQuaternion.cs index 84f3ce0b..43c94abd 100644 --- a/Assets/Scripts/model/export/SerializableQuaternion.cs +++ b/Assets/Scripts/model/export/SerializableQuaternion.cs @@ -16,33 +16,38 @@ using System.Runtime.Serialization; using UnityEngine; -namespace com.google.apps.peltzer.client.model.export { - /// - /// Unity's Quaternion is not serializable by default and we can't mess with their code. So, we wrap it. - /// - [Serializable] - class SerializableQuaternion : ISerializable { - public Quaternion quaternion = Quaternion.identity; +namespace com.google.apps.peltzer.client.model.export +{ + /// + /// Unity's Quaternion is not serializable by default and we can't mess with their code. So, we wrap it. + /// + [Serializable] + class SerializableQuaternion : ISerializable + { + public Quaternion quaternion = Quaternion.identity; - public SerializableQuaternion(Quaternion quaternion) { - this.quaternion = quaternion; - } + public SerializableQuaternion(Quaternion quaternion) + { + this.quaternion = quaternion; + } - // Serialize - public void GetObjectData(SerializationInfo info, StreamingContext context) { - info.AddValue("x", quaternion.x); - info.AddValue("y", quaternion.y); - info.AddValue("z", quaternion.z); - info.AddValue("w", quaternion.w); - } + // Serialize + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("x", quaternion.x); + info.AddValue("y", quaternion.y); + info.AddValue("z", quaternion.z); + info.AddValue("w", quaternion.w); + } - // Deserialize - public SerializableQuaternion(SerializationInfo info, StreamingContext context) { - float x = (float)info.GetValue("x", typeof(float)); - float y = (float)info.GetValue("y", typeof(float)); - float z = (float)info.GetValue("z", typeof(float)); - float w = (float)info.GetValue("w", typeof(float)); - quaternion = new Quaternion(x, y, z, w); + // Deserialize + public SerializableQuaternion(SerializationInfo info, StreamingContext context) + { + float x = (float)info.GetValue("x", typeof(float)); + float y = (float)info.GetValue("y", typeof(float)); + float z = (float)info.GetValue("z", typeof(float)); + float w = (float)info.GetValue("w", typeof(float)); + quaternion = new Quaternion(x, y, z, w); + } } - } } diff --git a/Assets/Scripts/model/export/SerializableVector3.cs b/Assets/Scripts/model/export/SerializableVector3.cs index 3a955808..90d49f76 100644 --- a/Assets/Scripts/model/export/SerializableVector3.cs +++ b/Assets/Scripts/model/export/SerializableVector3.cs @@ -17,47 +17,56 @@ using System.Runtime.Serialization; using UnityEngine; -namespace com.google.apps.peltzer.client.model.export { - /// - /// Unity's Vector3 is not serializable by default and we can't mess with their code. So, we wrap it. - /// - [Serializable] - class SerializableVector3 : ISerializable { - public Vector3 vector3; +namespace com.google.apps.peltzer.client.model.export +{ + /// + /// Unity's Vector3 is not serializable by default and we can't mess with their code. So, we wrap it. + /// + [Serializable] + class SerializableVector3 : ISerializable + { + public Vector3 vector3; - public SerializableVector3(Vector3 vector3) { - this.vector3 = vector3; - } + public SerializableVector3(Vector3 vector3) + { + this.vector3 = vector3; + } - public static List CreateSerializableList(IEnumerable vector3s) { - List serializableVector3s = new List(); - foreach (Vector3 vector3 in vector3s) { - serializableVector3s.Add(new SerializableVector3(vector3)); - } - return serializableVector3s; - } + public static List CreateSerializableList(IEnumerable vector3s) + { + List serializableVector3s = new List(); + foreach (Vector3 vector3 in vector3s) + { + serializableVector3s.Add(new SerializableVector3(vector3)); + } + return serializableVector3s; + } - public static List CreateUnserializedList(IEnumerable serializedVector3s) { - List vector3s = new List(); - foreach (SerializableVector3 serializedVector3 in serializedVector3s) { - vector3s.Add(serializedVector3.vector3); - } - return vector3s; - } + public static List CreateUnserializedList(IEnumerable serializedVector3s) + { + List vector3s = new List(); + foreach (SerializableVector3 serializedVector3 in serializedVector3s) + { + vector3s.Add(serializedVector3.vector3); + } + return vector3s; + } - // Serialize - public void GetObjectData(SerializationInfo info, StreamingContext context) { - info.AddValue("x", vector3.x); - info.AddValue("y", vector3.y); - info.AddValue("z", vector3.z); - } + // Serialize + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("x", vector3.x); + info.AddValue("y", vector3.y); + info.AddValue("z", vector3.z); + } - // Deserialize - public SerializableVector3(SerializationInfo info, StreamingContext context) { - float x = (float)info.GetValue("x", typeof(float)); - float y = (float)info.GetValue("y", typeof(float)); - float z = (float)info.GetValue("z", typeof(float)); - vector3 = new Vector3(x, y, z); + // Deserialize + public SerializableVector3(SerializationInfo info, StreamingContext context) + { + float x = (float)info.GetValue("x", typeof(float)); + float y = (float)info.GetValue("y", typeof(float)); + float z = (float)info.GetValue("z", typeof(float)); + vector3 = new Vector3(x, y, z); + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/import/MeshVerticesAndTriangles.cs b/Assets/Scripts/model/import/MeshVerticesAndTriangles.cs index 513834fb..f02e8ad4 100644 --- a/Assets/Scripts/model/import/MeshVerticesAndTriangles.cs +++ b/Assets/Scripts/model/import/MeshVerticesAndTriangles.cs @@ -14,27 +14,31 @@ using UnityEngine; -namespace com.google.apps.peltzer.client.model.import { - /// - /// A helper class to hold vertices and triangles for a mesh, and convert them to a Mesh if needed. - /// - public class MeshVerticesAndTriangles { - public Vector3[] meshVertices; - public int[] triangles; +namespace com.google.apps.peltzer.client.model.import +{ + /// + /// A helper class to hold vertices and triangles for a mesh, and convert them to a Mesh if needed. + /// + public class MeshVerticesAndTriangles + { + public Vector3[] meshVertices; + public int[] triangles; - public MeshVerticesAndTriangles(Vector3[] meshVertices, int[] triangles) { - this.meshVertices = meshVertices; - this.triangles = triangles; - } + public MeshVerticesAndTriangles(Vector3[] meshVertices, int[] triangles) + { + this.meshVertices = meshVertices; + this.triangles = triangles; + } - // Must be called on main thread. - public Mesh ToMesh() { - Mesh mesh = new Mesh(); - mesh.vertices = meshVertices; - mesh.triangles = triangles; - mesh.RecalculateBounds(); - mesh.RecalculateNormals(); - return mesh; + // Must be called on main thread. + public Mesh ToMesh() + { + Mesh mesh = new Mesh(); + mesh.vertices = meshVertices; + mesh.triangles = triangles; + mesh.RecalculateBounds(); + mesh.RecalculateNormals(); + return mesh; + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/import/ObjImporter.cs b/Assets/Scripts/model/import/ObjImporter.cs index 6777d1a6..c29756bb 100644 --- a/Assets/Scripts/model/import/ObjImporter.cs +++ b/Assets/Scripts/model/import/ObjImporter.cs @@ -20,246 +20,307 @@ using System.Linq; using System; -namespace com.google.apps.peltzer.client.model.import { - /// - /// Imports obj and mtl files. - /// - public static class ObjImporter { +namespace com.google.apps.peltzer.client.model.import +{ /// - /// Creates an MMesh from the contents of a .obj file and the contents of a .mtl file, with the given id. - /// Generally an OBJ file will not create meshes that are "topologically correct", so they won't work right - /// with a lot of our tools, but should at least be moveable if nothing else. + /// Imports obj and mtl files. /// - /// The contents of a .obj file. - /// The contents of a .mtl file. - /// The id of the new MMesh. - /// The created mesh, or null if it could not be created. - /// Whether the MMesh could be created. - public static bool MMeshFromObjFile(string objFileContents, string mtlFileContents, int id, out MMesh result) { - Dictionary materials = ImportMaterials(mtlFileContents); - Dictionary> materialsAndMeshes; - if (ImportMeshes(objFileContents, materials, out materialsAndMeshes)) { - result = MeshHelper.MMeshFromMeshes(id, materialsAndMeshes); - return true; - } - result = null; - return false; - } + public static class ObjImporter + { + /// + /// Creates an MMesh from the contents of a .obj file and the contents of a .mtl file, with the given id. + /// Generally an OBJ file will not create meshes that are "topologically correct", so they won't work right + /// with a lot of our tools, but should at least be moveable if nothing else. + /// + /// The contents of a .obj file. + /// The contents of a .mtl file. + /// The id of the new MMesh. + /// The created mesh, or null if it could not be created. + /// Whether the MMesh could be created. + public static bool MMeshFromObjFile(string objFileContents, string mtlFileContents, int id, out MMesh result) + { + Dictionary materials = ImportMaterials(mtlFileContents); + Dictionary> materialsAndMeshes; + if (ImportMeshes(objFileContents, materials, out materialsAndMeshes)) + { + result = MeshHelper.MMeshFromMeshes(id, materialsAndMeshes); + return true; + } + result = null; + return false; + } - public static Dictionary ImportMaterials(string materialsString) { - Dictionary materials = new Dictionary(); - if (materialsString == null || materialsString.Length == 0) - return materials; + public static Dictionary ImportMaterials(string materialsString) + { + Dictionary materials = new Dictionary(); + if (materialsString == null || materialsString.Length == 0) + return materials; - using (StringReader reader = new StringReader(materialsString)) { - string currentText = reader.ReadLine().Trim(); - while (currentText != null) { - if (currentText.StartsWith("newmtl")) { - string materialName = currentText.Split(' ')[1]; - Color materialColor = Color.white; + using (StringReader reader = new StringReader(materialsString)) + { + string currentText = reader.ReadLine().Trim(); + while (currentText != null) + { + if (currentText.StartsWith("newmtl")) + { + string materialName = currentText.Split(' ')[1]; + Color materialColor = Color.white; - currentText = reader.ReadLine(); - while (currentText != null && !currentText.StartsWith("newmtl")) { - currentText = currentText.Trim(); - if (currentText.StartsWith("Ka")) { - string[] colorString = currentText.Split(' '); - materialColor = new Color(float.Parse(colorString[1]), float.Parse(colorString[2]), - float.Parse(colorString[3])); - } else if (currentText.StartsWith("Kd")) { - string[] colorString = currentText.Split(' '); - materialColor = new Color(float.Parse(colorString[1]), float.Parse(colorString[2]), - float.Parse(colorString[3])); - } - currentText = reader.ReadLine(); - } + currentText = reader.ReadLine(); + while (currentText != null && !currentText.StartsWith("newmtl")) + { + currentText = currentText.Trim(); + if (currentText.StartsWith("Ka")) + { + string[] colorString = currentText.Split(' '); + materialColor = new Color(float.Parse(colorString[1]), float.Parse(colorString[2]), + float.Parse(colorString[3])); + } + else if (currentText.StartsWith("Kd")) + { + string[] colorString = currentText.Split(' '); + materialColor = new Color(float.Parse(colorString[1]), float.Parse(colorString[2]), + float.Parse(colorString[3])); + } + currentText = reader.ReadLine(); + } - Material material = null; - if (materialName.StartsWith("mat")) { - int potentialMaterialId; - if (int.TryParse(materialName.Substring("mat".Length), out potentialMaterialId)) { - material = MaterialRegistry.GetMaterialAndColorById(potentialMaterialId).material; - material.name = materialName; - } - } - if (material == null) { - material = new Material(Shader.Find("Diffuse")); - material.name = materialName; - material.color = materialColor; - } + Material material = null; + if (materialName.StartsWith("mat")) + { + int potentialMaterialId; + if (int.TryParse(materialName.Substring("mat".Length), out potentialMaterialId)) + { + material = MaterialRegistry.GetMaterialAndColorById(potentialMaterialId).material; + material.name = materialName; + } + } + if (material == null) + { + material = new Material(Shader.Find("Diffuse")); + material.name = materialName; + material.color = materialColor; + } - materials.Add(materialName, material); - } else { - currentText = reader.ReadLine(); - } + materials.Add(materialName, material); + } + else + { + currentText = reader.ReadLine(); + } + } + } + return materials; } - } - return materials; - } - private class Face { - public List vertexIds = new List(); - } + private class Face + { + public List vertexIds = new List(); + } - public static bool ImportMeshes(string objFileContents, - Dictionary materials, out Dictionary> meshes) { - meshes = new Dictionary>(); - if (objFileContents == null || objFileContents.Length == 0) { - return false; - } + public static bool ImportMeshes(string objFileContents, + Dictionary materials, out Dictionary> meshes) + { + meshes = new Dictionary>(); + if (objFileContents == null || objFileContents.Length == 0) + { + return false; + } - // Default current material, in case they don't have an MTL file. - bool mtlFileWasSupplied = materials.Count > 0; - string currentMaterial = "mat0"; - if (!mtlFileWasSupplied) { - materials.Add(currentMaterial, MaterialRegistry.GetMaterialAndColorById(0).material); - } + // Default current material, in case they don't have an MTL file. + bool mtlFileWasSupplied = materials.Count > 0; + string currentMaterial = "mat0"; + if (!mtlFileWasSupplied) + { + materials.Add(currentMaterial, MaterialRegistry.GetMaterialAndColorById(0).material); + } - List allVertices = new List(); - List allTexVertices = new List(); - Dictionary> faces = new Dictionary>(); + List allVertices = new List(); + List allTexVertices = new List(); + Dictionary> faces = new Dictionary>(); - string[] parts; - char[] sep = { ' ' }; - char[] sep2 = { ':' }; - char[] sep3 = { '/' }; - string[] sep4 = { "//" }; - using (StringReader reader = new StringReader(objFileContents)) { - string line = reader.ReadLine(); - while (line != null) { - if (line.StartsWith("v ")) { - parts = line.Trim().Split(sep); - if (parts.Count() < 4) { - Debug.Log("Not enough vertex values"); - Debug.Log(line); - return false; - } - try { - allVertices.Add(new Vector3(Convert.ToSingle(parts[1]), Convert.ToSingle(parts[2]), Convert.ToSingle(parts[3]))); - } catch (FormatException) { - Debug.Log("Unexpected vertex value"); - Debug.Log(line); - return false; - } - } - else if (line.StartsWith("vt ")) { - parts = line.Trim().Split(sep); - if (parts.Count() < 3) { - Debug.Log("Not enough tex vertex values"); - Debug.Log(line); - return false; - } - try { - allTexVertices.Add(new Vector2(Convert.ToSingle(parts[1]), Convert.ToSingle(parts[2]))); - } catch (FormatException) { - Debug.Log("Unexpected tex vertex value"); - Debug.Log(line); - return false; + string[] parts; + char[] sep = { ' ' }; + char[] sep2 = { ':' }; + char[] sep3 = { '/' }; + string[] sep4 = { "//" }; + using (StringReader reader = new StringReader(objFileContents)) + { + string line = reader.ReadLine(); + while (line != null) + { + if (line.StartsWith("v ")) + { + parts = line.Trim().Split(sep); + if (parts.Count() < 4) + { + Debug.Log("Not enough vertex values"); + Debug.Log(line); + return false; + } + try + { + allVertices.Add(new Vector3(Convert.ToSingle(parts[1]), Convert.ToSingle(parts[2]), Convert.ToSingle(parts[3]))); + } + catch (FormatException) + { + Debug.Log("Unexpected vertex value"); + Debug.Log(line); + return false; + } + } + else if (line.StartsWith("vt ")) + { + parts = line.Trim().Split(sep); + if (parts.Count() < 3) + { + Debug.Log("Not enough tex vertex values"); + Debug.Log(line); + return false; + } + try + { + allTexVertices.Add(new Vector2(Convert.ToSingle(parts[1]), Convert.ToSingle(parts[2]))); + } + catch (FormatException) + { + Debug.Log("Unexpected tex vertex value"); + Debug.Log(line); + return false; + } + } + else if (line.StartsWith("usemtl ") && mtlFileWasSupplied) + { + parts = line.Trim().Split(sep); + if (parts[1].Contains(sep2[0])) + { + currentMaterial = parts[1].Split(sep2)[1]; + } + else + { + currentMaterial = parts[1]; + } + } + else if (line.StartsWith("f ")) + { + parts = line.Trim().Split(sep); + if (parts.Length < 4) + { + Debug.Log("Not vertex values in a face"); + Debug.Log(line); + return false; + } + Face face = new Face(); + for (int i = 1; i < parts.Length; i++) + { + if (parts[i].Contains(sep4[0])) + { + // -1 as vertices are 0-indexed when read but 1-indexed when referenced. + face.vertexIds.Add(int.Parse(parts[i].Split(sep4, StringSplitOptions.None)[0]) - 1); + } + else if (parts[i].Contains(sep3[0])) + { + // -1 as vertices are 0-indexed when read but 1-indexed when referenced. + face.vertexIds.Add(int.Parse(parts[i].Split(sep3)[0]) - 1); + //face.uvIds.Add(int.Parse(vIds[1])); + } + else + { + // -1 as vertices are 0-indexed when read but 1-indexed when referenced. + face.vertexIds.Add(int.Parse(parts[i]) - 1); + } + } + if (!faces.ContainsKey(currentMaterial)) + { + faces.Add(currentMaterial, new List()); + } + faces[currentMaterial].Add(face); + } + line = reader.ReadLine(); + } } - } else if (line.StartsWith("usemtl ") && mtlFileWasSupplied) { - parts = line.Trim().Split(sep); - if (parts[1].Contains(sep2[0])) { - currentMaterial = parts[1].Split(sep2)[1]; - } else { - currentMaterial = parts[1]; - } - } else if (line.StartsWith("f ")) { - parts = line.Trim().Split(sep); - if (parts.Length < 4) { - Debug.Log("Not vertex values in a face"); - Debug.Log(line); - return false; - } - Face face = new Face(); - for (int i = 1; i < parts.Length; i++) { - if (parts[i].Contains(sep4[0])) { - // -1 as vertices are 0-indexed when read but 1-indexed when referenced. - face.vertexIds.Add(int.Parse(parts[i].Split(sep4, StringSplitOptions.None)[0]) - 1); - } else if (parts[i].Contains(sep3[0])) { - // -1 as vertices are 0-indexed when read but 1-indexed when referenced. - face.vertexIds.Add(int.Parse(parts[i].Split(sep3)[0]) - 1); - //face.uvIds.Add(int.Parse(vIds[1])); - } else { - // -1 as vertices are 0-indexed when read but 1-indexed when referenced. - face.vertexIds.Add(int.Parse(parts[i]) - 1); - } - } - if (!faces.ContainsKey(currentMaterial)) { - faces.Add(currentMaterial, new List()); - } - faces[currentMaterial].Add(face); - } - line = reader.ReadLine(); - } - } - // Create one mesh per entry in faceList, as all faces will have the same material. - foreach (KeyValuePair> faceList in faces) { - // A list of vertics in this mesh. - List meshVertices = new List(); - // Used to translate a vertex id from the obj file to an index into meshVertices. - Dictionary localVertexIds = new Dictionary(); - // A list of triangles in this mesh. - List triangles = new List(); + // Create one mesh per entry in faceList, as all faces will have the same material. + foreach (KeyValuePair> faceList in faces) + { + // A list of vertics in this mesh. + List meshVertices = new List(); + // Used to translate a vertex id from the obj file to an index into meshVertices. + Dictionary localVertexIds = new Dictionary(); + // A list of triangles in this mesh. + List triangles = new List(); - foreach (Face face in faceList.Value) { - foreach (int idx in face.vertexIds) { - if (!localVertexIds.ContainsKey(idx)) { - localVertexIds.Add(idx, meshVertices.Count); - meshVertices.Add(allVertices[idx]); - } - } - for (int i = 2; i < face.vertexIds.Count; i++) { - triangles.Add(localVertexIds[face.vertexIds[i - 2]]); - triangles.Add(localVertexIds[face.vertexIds[i - 1]]); - triangles.Add(localVertexIds[face.vertexIds[i]]); - } - } // foreach face - meshes.Add(materials[faceList.Key], BreakIntoMultipleMeshes(meshVertices, triangles)); - } // foreach facelist - return true; - } + foreach (Face face in faceList.Value) + { + foreach (int idx in face.vertexIds) + { + if (!localVertexIds.ContainsKey(idx)) + { + localVertexIds.Add(idx, meshVertices.Count); + meshVertices.Add(allVertices[idx]); + } + } + for (int i = 2; i < face.vertexIds.Count; i++) + { + triangles.Add(localVertexIds[face.vertexIds[i - 2]]); + triangles.Add(localVertexIds[face.vertexIds[i - 1]]); + triangles.Add(localVertexIds[face.vertexIds[i]]); + } + } // foreach face + meshes.Add(materials[faceList.Key], BreakIntoMultipleMeshes(meshVertices, triangles)); + } // foreach facelist + return true; + } - private static List - BreakIntoMultipleMeshes(List meshVertices, List triangles) { - if (meshVertices.Count < 65000) { - return new List() { + private static List + BreakIntoMultipleMeshes(List meshVertices, List triangles) + { + if (meshVertices.Count < 65000) + { + return new List() { new MeshVerticesAndTriangles(meshVertices.ToArray(), triangles.ToArray()) }; - } - List subMeshes = new List(); - List subMeshTriangles = new List(); - List subMeshVertices = new List(); - Dictionary triangleMapping = new Dictionary(); - for (int i = 0; i < triangles.Count; i += 3) { - int t1 = triangles[i]; - int t2 = triangles[i + 1]; - int t3 = triangles[i + 2]; - if (!triangleMapping.ContainsKey(t1)) { - triangleMapping.Add(t1, subMeshVertices.Count); - subMeshVertices.Add(meshVertices[t1]); - } - if (!triangleMapping.ContainsKey(t2)) { - triangleMapping.Add(t2, subMeshVertices.Count); - subMeshVertices.Add(meshVertices[t2]); - } - if (!triangleMapping.ContainsKey(t3)) { - triangleMapping.Add(t3, subMeshVertices.Count); - subMeshVertices.Add(meshVertices[t3]); - } - subMeshTriangles.Add(triangleMapping[t1]); - subMeshTriangles.Add(triangleMapping[t2]); - subMeshTriangles.Add(triangleMapping[t3]); - if (subMeshVertices.Count > 64000) { - subMeshes.Add(new MeshVerticesAndTriangles(subMeshVertices.ToArray(), subMeshTriangles.ToArray())); - subMeshVertices = new List(); - subMeshTriangles = new List(); - triangleMapping.Clear(); + } + List subMeshes = new List(); + List subMeshTriangles = new List(); + List subMeshVertices = new List(); + Dictionary triangleMapping = new Dictionary(); + for (int i = 0; i < triangles.Count; i += 3) + { + int t1 = triangles[i]; + int t2 = triangles[i + 1]; + int t3 = triangles[i + 2]; + if (!triangleMapping.ContainsKey(t1)) + { + triangleMapping.Add(t1, subMeshVertices.Count); + subMeshVertices.Add(meshVertices[t1]); + } + if (!triangleMapping.ContainsKey(t2)) + { + triangleMapping.Add(t2, subMeshVertices.Count); + subMeshVertices.Add(meshVertices[t2]); + } + if (!triangleMapping.ContainsKey(t3)) + { + triangleMapping.Add(t3, subMeshVertices.Count); + subMeshVertices.Add(meshVertices[t3]); + } + subMeshTriangles.Add(triangleMapping[t1]); + subMeshTriangles.Add(triangleMapping[t2]); + subMeshTriangles.Add(triangleMapping[t3]); + if (subMeshVertices.Count > 64000) + { + subMeshes.Add(new MeshVerticesAndTriangles(subMeshVertices.ToArray(), subMeshTriangles.ToArray())); + subMeshVertices = new List(); + subMeshTriangles = new List(); + triangleMapping.Clear(); + } + } + if (subMeshVertices.Count > 0) + { + subMeshes.Add(new MeshVerticesAndTriangles(subMeshVertices.ToArray(), subMeshTriangles.ToArray())); + } + return subMeshes; } - } - if (subMeshVertices.Count > 0) { - subMeshes.Add(new MeshVerticesAndTriangles(subMeshVertices.ToArray(), subMeshTriangles.ToArray())); - } - return subMeshes; } - } } diff --git a/Assets/Scripts/model/main/Features.cs b/Assets/Scripts/model/main/Features.cs index 96d3707c..6614c0ef 100644 --- a/Assets/Scripts/model/main/Features.cs +++ b/Assets/Scripts/model/main/Features.cs @@ -16,128 +16,135 @@ using System.Collections.Generic; using System.Reflection; -namespace com.google.apps.peltzer.client.model.main { - /// - /// Peltzer features that may or may not be enabled. - /// Flags that are not marked as readonly can be also set from the debug console at runtime - /// (this is done by reflection). - /// - public class Features { - // If true, CSG subtraction (subtracting one shape from another, also known as "carving") is enabled. - public static bool csgSubtractEnabled = false; - - // If true, saves creations in the Mogwai object store. - public static bool saveToMogwaiObjectStore = true; - - // If true, stamping (also known as "custom primitives") are enabled in the Volume Inserter. - public static bool stampingEnabled = false; - - // If true, enable the debug console. - public static readonly bool enableDebugConsole = true; - - // If true, enable controller swapping by bumping controllers together (like TiltBrush). - public static bool enableControllerSwapping = true; - - // If true, enable deletion of parts (vertices, edges, and faces) - public static bool enablePartDeletion = false; - - // If true, try to merge adjacent coplanar faces (to remove unnecessary face splits). - public static bool mergeAdjacentCoplanarFaces = false; - - // If true, clicking the trigger far from the selected items during a move/copy/reshape/extrude operation will - // just deselect them. - public static bool clickAwayToDeselect = true; - - // If true, force first-time users into tutorial. - public static bool forceFirstTimeUsersIntoTutorial = false; - - // If true, publish to Zandria prod (else autopush). - public static bool useZandriaProd = false; - - // Show ruler for volume inserter - #if UNITY_EDITOR - public static bool showVolumeInserterRuler = true; - #else - public static bool showVolumeInserterRuler = false; - #endif - - // If true, use the new expanded radius for rendering wireframes. - public static bool expandedWireframeRadius = false; - - // If true, adjust the world space for editing convenience after opening a creation. - // If false, don't (start with the identity world space). - public static bool adjustWorldSpaceOnOpen = true; - - // If true, trigger haptic feedback on hover. If false, don't (only in multiselect). - public static bool vibrateOnHover = false; - - // If true, selectively divert global undo/redo stack to local undo/redo stacks. - public static bool localUndoRedoEnabled = true; - - // If true, enable click to select functionality. - public static bool clickToSelectEnabled = false; +namespace com.google.apps.peltzer.client.model.main +{ + /// + /// Peltzer features that may or may not be enabled. + /// Flags that are not marked as readonly can be also set from the debug console at runtime + /// (this is done by reflection). + /// + public class Features + { + // If true, CSG subtraction (subtracting one shape from another, also known as "carving") is enabled. + public static bool csgSubtractEnabled = false; - // If true, undo redo works with click to select. Only true if local undo/redo is enabled and if click to select is enabled. - public static bool clickToSelectWithUndoRedoEnabled = clickToSelectEnabled && localUndoRedoEnabled; + // If true, saves creations in the Mogwai object store. + public static bool saveToMogwaiObjectStore = true; - // If true, shows tooltips when the user touches the touchpad/thumbstick whilst on the Poly menu. - public static bool showModelsMenuTooltips = false; + // If true, stamping (also known as "custom primitives") are enabled in the Volume Inserter. + public static bool stampingEnabled = false; - // If true, the subdivide tool will turn into the experimental loop subdivide form. - // Incompatible with planeSubdivideEnabled. - public static bool loopSubdivideEnabled = false; + // If true, enable the debug console. + public static readonly bool enableDebugConsole = true; - // If true, the subdivide tool will turn into the experimental plane subdivide form. - // Incompatible with loopSubdivideEnabled. - public static bool planeSubdivideEnabled = false; + // If true, enable controller swapping by bumping controllers together (like TiltBrush). + public static bool enableControllerSwapping = true; - // If true, allow noncoplanar faces to remain during mesh fixing. - public static bool allowNoncoplanarFaces = false; + // If true, enable deletion of parts (vertices, edges, and faces) + public static bool enablePartDeletion = false; - // If true, show tooltips and ropes for multi-selecting. - public static bool showMultiselectTooltip = false; + // If true, try to merge adjacent coplanar faces (to remove unnecessary face splits). + public static bool mergeAdjacentCoplanarFaces = false; - // If true, show rope guides for snapping. - public static bool showSnappingGuides = true; + // If true, clicking the trigger far from the selected items during a move/copy/reshape/extrude operation will + // just deselect them. + public static bool clickAwayToDeselect = true; - // If true, use the new continuous snap detection; - public static bool useContinuousSnapDetection = true; + // If true, force first-time users into tutorial. + public static bool forceFirstTimeUsersIntoTutorial = false; - // If true, enable world space grid planes. - public static bool enableWorldSpaceGridPlanes = false; + // If true, publish to Zandria prod (else autopush). + public static bool useZandriaProd = false; - // If true, show previews of Poly models on the menu instead of thumbnails. - public static bool showPolyMenuModelPreviews = false; - - /// - /// This function takes a comma delimited string containing feature names prepended with a '+' or a '-' and turns - /// those features on or off respectively. - /// - public static void ToggleFeatureString(String featureString) { - Dictionary fields = new Dictionary(); - foreach (FieldInfo fieldInfo in typeof(Features).GetFields(BindingFlags.Static | BindingFlags.Public)) { - // Only get fields that bool and not read-only. - if (fieldInfo.FieldType == typeof(bool) && fieldInfo.MemberType == MemberTypes.Field && - !fieldInfo.IsInitOnly) { - fields[fieldInfo.Name.ToLower()] = fieldInfo; - } - } - - string[] features = featureString.Split(','); - foreach (string feature in features) { - Char prefix = feature[0]; - string featureName = feature.Substring(1, feature.Length - 1); - switch (prefix) { - case '+': - fields[featureName.ToLower()].SetValue(null, true); - break; - case '-': - fields[featureName.ToLower()].SetValue(null, false); - break; - default: - break; + // Show ruler for volume inserter +#if UNITY_EDITOR + public static bool showVolumeInserterRuler = true; +#else + public static bool showVolumeInserterRuler = false; +#endif + + // If true, use the new expanded radius for rendering wireframes. + public static bool expandedWireframeRadius = false; + + // If true, adjust the world space for editing convenience after opening a creation. + // If false, don't (start with the identity world space). + public static bool adjustWorldSpaceOnOpen = true; + + // If true, trigger haptic feedback on hover. If false, don't (only in multiselect). + public static bool vibrateOnHover = false; + + // If true, selectively divert global undo/redo stack to local undo/redo stacks. + public static bool localUndoRedoEnabled = true; + + // If true, enable click to select functionality. + public static bool clickToSelectEnabled = false; + + // If true, undo redo works with click to select. Only true if local undo/redo is enabled and if click to select is enabled. + public static bool clickToSelectWithUndoRedoEnabled = clickToSelectEnabled && localUndoRedoEnabled; + + // If true, shows tooltips when the user touches the touchpad/thumbstick whilst on the Poly menu. + public static bool showModelsMenuTooltips = false; + + // If true, the subdivide tool will turn into the experimental loop subdivide form. + // Incompatible with planeSubdivideEnabled. + public static bool loopSubdivideEnabled = false; + + // If true, the subdivide tool will turn into the experimental plane subdivide form. + // Incompatible with loopSubdivideEnabled. + public static bool planeSubdivideEnabled = false; + + // If true, allow noncoplanar faces to remain during mesh fixing. + public static bool allowNoncoplanarFaces = false; + + // If true, show tooltips and ropes for multi-selecting. + public static bool showMultiselectTooltip = false; + + // If true, show rope guides for snapping. + public static bool showSnappingGuides = true; + + // If true, use the new continuous snap detection; + public static bool useContinuousSnapDetection = true; + + // If true, enable world space grid planes. + public static bool enableWorldSpaceGridPlanes = false; + + // If true, show previews of Poly models on the menu instead of thumbnails. + public static bool showPolyMenuModelPreviews = false; + + /// + /// This function takes a comma delimited string containing feature names prepended with a '+' or a '-' and turns + /// those features on or off respectively. + /// + public static void ToggleFeatureString(String featureString) + { + Dictionary fields = new Dictionary(); + foreach (FieldInfo fieldInfo in typeof(Features).GetFields(BindingFlags.Static | BindingFlags.Public)) + { + // Only get fields that bool and not read-only. + if (fieldInfo.FieldType == typeof(bool) && fieldInfo.MemberType == MemberTypes.Field && + !fieldInfo.IsInitOnly) + { + fields[fieldInfo.Name.ToLower()] = fieldInfo; + } + } + + string[] features = featureString.Split(','); + foreach (string feature in features) + { + Char prefix = feature[0]; + string featureName = feature.Substring(1, feature.Length - 1); + switch (prefix) + { + case '+': + fields[featureName.ToLower()].SetValue(null, true); + break; + case '-': + fields[featureName.ToLower()].SetValue(null, false); + break; + default: + break; + } + } } - } } - } } diff --git a/Assets/Scripts/model/main/ObjectFinder.cs b/Assets/Scripts/model/main/ObjectFinder.cs index 22706cd2..9402cb8b 100644 --- a/Assets/Scripts/model/main/ObjectFinder.cs +++ b/Assets/Scripts/model/main/ObjectFinder.cs @@ -18,99 +18,111 @@ using UnityEngine; using UnityEngine.SceneManagement; -namespace com.google.apps.peltzer.client.model.main { - /// - /// Responsible for finding unique objects in the scene. - /// Unique objects are those whose names begin with "ID_" (case sensitive). - /// These objects must be STATICALLY in the scene (added through the Unity editor). Dynamically created objects - /// will not be found by this class. - /// - public class ObjectFinder { +namespace com.google.apps.peltzer.client.model.main +{ /// - /// Prefix that unique objects should have. + /// Responsible for finding unique objects in the scene. + /// Unique objects are those whose names begin with "ID_" (case sensitive). + /// These objects must be STATICALLY in the scene (added through the Unity editor). Dynamically created objects + /// will not be found by this class. /// - private const string ID_PREFIX = "ID_"; + public class ObjectFinder + { + /// + /// Prefix that unique objects should have. + /// + private const string ID_PREFIX = "ID_"; - /// - /// Dictionary from object ID to GameObject. Lazily initialized on the first lookup. - /// - private static Dictionary cache; + /// + /// Dictionary from object ID to GameObject. Lazily initialized on the first lookup. + /// + private static Dictionary cache; - /// - /// Lock that protects the cache. - /// - private static object cacheLock = new object(); + /// + /// Lock that protects the cache. + /// + private static object cacheLock = new object(); - /// - /// Creates the cache of objects by going through all the objects in the scene to find - /// objects tagged with the ID_PREFIX. This is relatively expensive and should be done - /// only once at startup. - /// - /// - private static Dictionary CreateCache() { - Scene activeScene = SceneManager.GetActiveScene(); - AssertOrThrow.NotNull(activeScene, "Active scene is unexpectedly null. The universe is broken."); + /// + /// Creates the cache of objects by going through all the objects in the scene to find + /// objects tagged with the ID_PREFIX. This is relatively expensive and should be done + /// only once at startup. + /// + /// + private static Dictionary CreateCache() + { + Scene activeScene = SceneManager.GetActiveScene(); + AssertOrThrow.NotNull(activeScene, "Active scene is unexpectedly null. The universe is broken."); - Dictionary result = new Dictionary(); - foreach (GameObject obj in Resources.FindObjectsOfTypeAll()) { - // We only care about objects in this scene. - // We have to check for each object's current scene because FindObjectsOfTypeAll() returns Unity's internal - // objects as well, which are not part of this scene. - if (obj.scene != activeScene) { - continue; - } + Dictionary result = new Dictionary(); + foreach (GameObject obj in Resources.FindObjectsOfTypeAll()) + { + // We only care about objects in this scene. + // We have to check for each object's current scene because FindObjectsOfTypeAll() returns Unity's internal + // objects as well, which are not part of this scene. + if (obj.scene != activeScene) + { + continue; + } - // We only care about normal objects that appear in the editor's hierarchy (hideFlags == HideFlags.None). - // FindObjectsOfTypeAll() returns all objects, including objects that normally don't appear in the editor - // such as prefabs (not to be confused with prefab *instances*, which are shown in the hierarchy). - // We have to be careful to ignore those. - if (obj.hideFlags != HideFlags.None) { - continue; - } + // We only care about normal objects that appear in the editor's hierarchy (hideFlags == HideFlags.None). + // FindObjectsOfTypeAll() returns all objects, including objects that normally don't appear in the editor + // such as prefabs (not to be confused with prefab *instances*, which are shown in the hierarchy). + // We have to be careful to ignore those. + if (obj.hideFlags != HideFlags.None) + { + continue; + } - if (obj.name.StartsWith(ID_PREFIX)) { - AssertOrThrow.True(!result.ContainsKey(obj.name), - "ID collision: duplicate object ID: " + obj.name + " (case insensitive)"); - result[obj.name] = obj; + if (obj.name.StartsWith(ID_PREFIX)) + { + AssertOrThrow.True(!result.ContainsKey(obj.name), + "ID collision: duplicate object ID: " + obj.name + " (case insensitive)"); + result[obj.name] = obj; + } + } + return result; } - } - return result; - } - /// - /// Looks up a unique object by its ID. This locates the object regardless of whether it's - /// active or not. Throws an exception if the object can't be found. - /// - /// The ID of the object to locate. - /// The object - public static GameObject ObjectById(string id) { - AssertOrThrow.True(id.StartsWith(ID_PREFIX), - "Can't look up an ID that doesn't start with the ID prefix: " + id); - lock (cacheLock) { - if (cache == null) { - cache = CreateCache(); - } - GameObject result; - if (!cache.TryGetValue(id, out result)) { - throw new Exception("Object not found: " + id + ". Make sure it's statically in the scene."); + /// + /// Looks up a unique object by its ID. This locates the object regardless of whether it's + /// active or not. Throws an exception if the object can't be found. + /// + /// The ID of the object to locate. + /// The object + public static GameObject ObjectById(string id) + { + AssertOrThrow.True(id.StartsWith(ID_PREFIX), + "Can't look up an ID that doesn't start with the ID prefix: " + id); + lock (cacheLock) + { + if (cache == null) + { + cache = CreateCache(); + } + GameObject result; + if (!cache.TryGetValue(id, out result)) + { + throw new Exception("Object not found: " + id + ". Make sure it's statically in the scene."); + } + return result; + } } - return result; - } - } - /// - /// Similar to LookUpById but returns the component of the given type of the resulting GameObject. - /// Aborts and throws an exception if the object does not exist or if it does not have the requested - /// component. - /// - /// The type of component to query - /// The ID of the object to search. - /// The requested component. - public static T ComponentById(string id) { - GameObject obj = ObjectById(id); - T comp = obj.GetComponent(); - AssertOrThrow.NotNull(comp, "Object " + id + " does not have a component of type " + typeof(T).Name); - return comp; + /// + /// Similar to LookUpById but returns the component of the given type of the resulting GameObject. + /// Aborts and throws an exception if the object does not exist or if it does not have the requested + /// component. + /// + /// The type of component to query + /// The ID of the object to search. + /// The requested component. + public static T ComponentById(string id) + { + GameObject obj = ObjectById(id); + T comp = obj.GetComponent(); + AssertOrThrow.NotNull(comp, "Object " + id + " does not have a component of type " + typeof(T).Name); + return comp; + } } - } } diff --git a/Assets/Scripts/model/main/PeltzerMain.cs b/Assets/Scripts/model/main/PeltzerMain.cs index a7b6f938..40144bc3 100644 --- a/Assets/Scripts/model/main/PeltzerMain.cs +++ b/Assets/Scripts/model/main/PeltzerMain.cs @@ -37,1712 +37,1922 @@ using com.google.apps.peltzer.client.entitlement; using com.google.apps.peltzer.client.api_clients.assets_service_client; -namespace com.google.apps.peltzer.client.model.main { - public enum MenuAction { - SAVE, LOAD, SHOWCASE, TAKE_PHOTO, SHARE, CLEAR, BLOCKMODE, NOTHING, - SHOW_SAVE_CONFIRM, CANCEL_SAVE, NEW_WITH_SAVE, SIGN_IN, SIGN_OUT, ADD_REFERENCE, - TOGGLE_SOUND, SAVE_COPY, PUBLISH, PUBLISHED_TAKE_OFF_HEADSET_DISMISS, - TUTORIAL_START, TUTORIAL_DISMISS, TUTORIAL_PROMPT, TUTORIAL_CONFIRM_DISMISS, - TUTORIAL_SAVE_AND_CONFIRM, TUTORIAL_DONT_SAVE_AND_CONFIRM, PUBLISH_AFTER_SAVE_DISMISS, - PUBLISH_SIGN_IN_DISMISS, PUBLISH_AFTER_SAVE_CONFIRM, TUTORIAL_EXIT_YES, TUTORIAL_EXIT_NO, - SAVE_LOCALLY, SAVE_LOCAL_SIGN_IN_INSTEAD, TOGGLE_LEFT_HANDED, TOGGLE_TOOLTIPS, PLAY_VIDEO, - SAVE_SELECTED, TOGGLE_FEATURE, TOGGLE_EXPAND_WIREFRAME_FEATURE, - } - - public enum Handedness { NONE, LEFT, RIGHT } - - /// - /// BackgroundWork for serializing a model into bytes (for saving). - /// This does not handle the actual saving, it just serializes in preparation for saving. - /// - public class SerializeWork : BackgroundWork { - private readonly Model model; - private ICollection meshes; - private SaveData saveData; - private byte[] thumbnailBytes; - private Action callback; - private PolySerializer serializer; - private bool saveSelected; - +namespace com.google.apps.peltzer.client.model.main +{ + public enum MenuAction + { + SAVE, LOAD, SHOWCASE, TAKE_PHOTO, SHARE, CLEAR, BLOCKMODE, NOTHING, + SHOW_SAVE_CONFIRM, CANCEL_SAVE, NEW_WITH_SAVE, SIGN_IN, SIGN_OUT, ADD_REFERENCE, + TOGGLE_SOUND, SAVE_COPY, PUBLISH, PUBLISHED_TAKE_OFF_HEADSET_DISMISS, + TUTORIAL_START, TUTORIAL_DISMISS, TUTORIAL_PROMPT, TUTORIAL_CONFIRM_DISMISS, + TUTORIAL_SAVE_AND_CONFIRM, TUTORIAL_DONT_SAVE_AND_CONFIRM, PUBLISH_AFTER_SAVE_DISMISS, + PUBLISH_SIGN_IN_DISMISS, PUBLISH_AFTER_SAVE_CONFIRM, TUTORIAL_EXIT_YES, TUTORIAL_EXIT_NO, + SAVE_LOCALLY, SAVE_LOCAL_SIGN_IN_INSTEAD, TOGGLE_LEFT_HANDED, TOGGLE_TOOLTIPS, PLAY_VIDEO, + SAVE_SELECTED, TOGGLE_FEATURE, TOGGLE_EXPAND_WIREFRAME_FEATURE, + } + + public enum Handedness { NONE, LEFT, RIGHT } /// - /// Serializes the model into bytes in the background. + /// BackgroundWork for serializing a model into bytes (for saving). + /// This does not handle the actual saving, it just serializes in preparation for saving. /// - /// The model to serialize. - /// The meshes to serialize. - public SerializeWork(Model model, ICollection meshes, - byte[] thumbnailBytes, Action callback, PolySerializer serializer, bool saveSelected) { - this.model = model; - this.meshes = meshes; - this.thumbnailBytes = thumbnailBytes; - this.callback = callback; - this.serializer = serializer; - this.saveSelected = saveSelected; - } - - public void BackgroundWork() { - // Need to make sure all meshes are back in remesher for coalesced gltf export. - PeltzerMain.Instance.GetSelector().DeselectAll(); - saveData = ExportUtils.SerializeModel(model, meshes, - /* saveGltf */ true, /* saveFbx */ true, /* saveTriangulatedObj */ true, - /* includeDisplayRotation */ true, serializer, saveSelected); - saveData.thumbnailBytes = thumbnailBytes; - } - - public void PostWork() { - // Callback to functions waiting for serialization to finish. - callback(saveData); - } - } - - internal class SaveToDiskWork : BackgroundWork { - private SaveData saveData; - private string directory; - private bool isOfflineModelsFolder; - private bool isOverwrite; - private bool success; - - public SaveToDiskWork(SaveData saveData, string directory, bool isOfflineModelsFolder, bool isOverwrite) { - this.saveData = saveData; - this.directory = directory; - this.isOfflineModelsFolder = isOfflineModelsFolder; - this.isOverwrite = isOverwrite; - } + public class SerializeWork : BackgroundWork + { + private readonly Model model; + private ICollection meshes; + private SaveData saveData; + private byte[] thumbnailBytes; + private Action callback; + private PolySerializer serializer; + private bool saveSelected; + + + /// + /// Serializes the model into bytes in the background. + /// + /// The model to serialize. + /// The meshes to serialize. + public SerializeWork(Model model, ICollection meshes, + byte[] thumbnailBytes, Action callback, PolySerializer serializer, bool saveSelected) + { + this.model = model; + this.meshes = meshes; + this.thumbnailBytes = thumbnailBytes; + this.callback = callback; + this.serializer = serializer; + this.saveSelected = saveSelected; + } - public void BackgroundWork() { - success = ExportUtils.SaveLocally(saveData, directory); - } + public void BackgroundWork() + { + // Need to make sure all meshes are back in remesher for coalesced gltf export. + PeltzerMain.Instance.GetSelector().DeselectAll(); + saveData = ExportUtils.SerializeModel(model, meshes, + /* saveGltf */ true, /* saveFbx */ true, /* saveTriangulatedObj */ true, + /* includeDisplayRotation */ true, serializer, saveSelected); + saveData.thumbnailBytes = thumbnailBytes; + } - public void PostWork() { - if (isOfflineModelsFolder) { - if (success) { - DirectoryInfo directoryInfo = new DirectoryInfo(directory); - PeltzerMain.Instance.HandleSaveComplete(true, "Saved locally"); - if (isOverwrite) { - PeltzerMain.Instance.UpdateLocalModelOntoPolyMenu(directoryInfo); - } else { - PeltzerMain.Instance.LoadLocallySavedModelOntoPolyMenu(directoryInfo); - } - } else { - PeltzerMain.Instance.HandleSaveComplete(false, "Save failed"); - } - } + public void PostWork() + { + // Callback to functions waiting for serialization to finish. + callback(saveData); + } } - } + internal class SaveToDiskWork : BackgroundWork + { + private SaveData saveData; + private string directory; + private bool isOfflineModelsFolder; + private bool isOverwrite; + private bool success; + + public SaveToDiskWork(SaveData saveData, string directory, bool isOfflineModelsFolder, bool isOverwrite) + { + this.saveData = saveData; + this.directory = directory; + this.isOfflineModelsFolder = isOfflineModelsFolder; + this.isOverwrite = isOverwrite; + } - /// - /// Add a mesh to the spatial index in the background. - /// - internal class AddToIndex : BackgroundWork { - private SpatialIndex spatialIndex; - private MMesh mmesh; - - internal AddToIndex(SpatialIndex spatialIndex, MMesh mmesh) { - this.spatialIndex = spatialIndex; - this.mmesh = mmesh; - } + public void BackgroundWork() + { + success = ExportUtils.SaveLocally(saveData, directory); + } - public void BackgroundWork() { - spatialIndex.AddMesh(mmesh); + public void PostWork() + { + if (isOfflineModelsFolder) + { + if (success) + { + DirectoryInfo directoryInfo = new DirectoryInfo(directory); + PeltzerMain.Instance.HandleSaveComplete(true, "Saved locally"); + if (isOverwrite) + { + PeltzerMain.Instance.UpdateLocalModelOntoPolyMenu(directoryInfo); + } + else + { + PeltzerMain.Instance.LoadLocallySavedModelOntoPolyMenu(directoryInfo); + } + } + else + { + PeltzerMain.Instance.HandleSaveComplete(false, "Save failed"); + } + } + } } - public void PostWork() { - } - } - - /// - /// Update a mesh in the spatial index in the background. - /// - internal class UpdateInIndex : BackgroundWork { - private SpatialIndex spatialIndex; - private MMesh mmesh; - - internal UpdateInIndex(SpatialIndex spatialIndex, MMesh mmesh) { - this.spatialIndex = spatialIndex; - this.mmesh = mmesh; - } - public void BackgroundWork() { - spatialIndex.RemoveMesh(mmesh); - spatialIndex.AddMesh(mmesh); - } + /// + /// Add a mesh to the spatial index in the background. + /// + internal class AddToIndex : BackgroundWork + { + private SpatialIndex spatialIndex; + private MMesh mmesh; + + internal AddToIndex(SpatialIndex spatialIndex, MMesh mmesh) + { + this.spatialIndex = spatialIndex; + this.mmesh = mmesh; + } - public void PostWork() { - } - } - - /// - /// Remove a mesh from the spatial index in the background. - /// - internal class DeleteFromIndex : BackgroundWork { - private SpatialIndex spatialIndex; - private MMesh mmesh; - - internal DeleteFromIndex(SpatialIndex spatialIndex, MMesh mmesh) { - this.spatialIndex = spatialIndex; - this.mmesh = mmesh; - } + public void BackgroundWork() + { + spatialIndex.AddMesh(mmesh); + } - public void BackgroundWork() { - spatialIndex.RemoveMesh(mmesh); + public void PostWork() + { + } } - public void PostWork() { - } - } - - /// - /// Main controller for the Peltzer app. This is a singleton. - /// * Holds and renders the Model. - /// * Controls all background work and background thread. - /// - /// This is a singleton and guaranteed to be available at any time (the GameObject with this behavior - /// is statically part of MainScene). To obtain an instance, use the static property PeltzerMain.Instance. - /// - public class PeltzerMain : MonoBehaviour { - /// - /// Key for the player preferences to determine if a user has started Blocks before. - /// - private static string FIRST_TIME_KEY = "blocks_first_time"; - /// - /// Key for the player preferences to determine if a user is left-handed. - /// - public static string LEFT_HANDED_KEY = "blocks_left_handed"; /// - /// Key for the player preferences to determine if a user has disabled tooltips. + /// Update a mesh in the spatial index in the background. /// - public static string DISABLE_TOOLTIPS_KEY = "blocks_disable_tooltips"; - /// - /// Key for the player preferences to determine if a user has disabled sounds. - /// - public static string DISABLE_SOUNDS_KEY = "blocks_disable_sounds"; - /// - /// Key for the player preferences to determine if a user has revoked analytics permissions. - /// - public static string DISABLE_ANALYTICS_KEY = "blocks_disable_analytics"; - /// - /// Key for the player preferences to determine which environment theme to present. - /// - public static string ENVIRONMENT_THEME_KEY = "blocks_environment_theme"; + internal class UpdateInIndex : BackgroundWork + { + private SpatialIndex spatialIndex; + private MMesh mmesh; + + internal UpdateInIndex(SpatialIndex spatialIndex, MMesh mmesh) + { + this.spatialIndex = spatialIndex; + this.mmesh = mmesh; + } + + public void BackgroundWork() + { + spatialIndex.RemoveMesh(mmesh); + spatialIndex.AddMesh(mmesh); + } + + public void PostWork() + { + } + } /// - /// Message displayed to the user when saving the model. + /// Remove a mesh from the spatial index in the background. /// - private const string SAVE_MESSAGE = "Saving..."; + internal class DeleteFromIndex : BackgroundWork + { + private SpatialIndex spatialIndex; + private MMesh mmesh; + + internal DeleteFromIndex(SpatialIndex spatialIndex, MMesh mmesh) + { + this.spatialIndex = spatialIndex; + this.mmesh = mmesh; + } - // The default workspace, a room of 10x10x10 metres. - public static readonly Bounds DEFAULT_BOUNDS = new Bounds(Vector3.zero, new Vector3(10f, 10f, 10f)); + public void BackgroundWork() + { + spatialIndex.RemoveMesh(mmesh); + } - // Menu actions that are selectable while a tutorial is occurring. - public static readonly List TUTORIAL_MENU_ACTIONS = - new List {MenuAction.TUTORIAL_EXIT_NO, MenuAction.TUTORIAL_EXIT_YES, MenuAction.TUTORIAL_DISMISS, - MenuAction.TUTORIAL_PROMPT}; + public void PostWork() + { + } + } /// - /// How far the controller trigger must be pressed to be considered to be "down". - /// This is a number from 0 to 1. If it's closer to 0 it means the user has to press the trigger - /// only slightly to activate it; if it's closer to 1, it means the user must push it all the way - /// in order to activate it. + /// Main controller for the Peltzer app. This is a singleton. + /// * Holds and renders the Model. + /// * Controls all background work and background thread. /// - /// Note: we're setting a high value because we want RELEASING the trigger to be fast, because when - /// the user releases the trigger, their hand is exactly where they want, and will often slip from the - /// correct position during the course of releasing the trigger. + /// This is a singleton and guaranteed to be available at any time (the GameObject with this behavior + /// is statically part of MainScene). To obtain an instance, use the static property PeltzerMain.Instance. /// - public static float TRIGGER_THRESHOLD = 0.95f; - - /// - /// Initial size of serializer buffers. - /// This must be reasonably big in order to avoid reallocation when saving models. - /// - private const int SERIALIZER_BUFFER_INITIAL_SIZE = 128 * 1024 * 1024; // 128 MB + public class PeltzerMain : MonoBehaviour + { + /// + /// Key for the player preferences to determine if a user has started Blocks before. + /// + private static string FIRST_TIME_KEY = "blocks_first_time"; + /// + /// Key for the player preferences to determine if a user is left-handed. + /// + public static string LEFT_HANDED_KEY = "blocks_left_handed"; + /// + /// Key for the player preferences to determine if a user has disabled tooltips. + /// + public static string DISABLE_TOOLTIPS_KEY = "blocks_disable_tooltips"; + /// + /// Key for the player preferences to determine if a user has disabled sounds. + /// + public static string DISABLE_SOUNDS_KEY = "blocks_disable_sounds"; + /// + /// Key for the player preferences to determine if a user has revoked analytics permissions. + /// + public static string DISABLE_ANALYTICS_KEY = "blocks_disable_analytics"; + /// + /// Key for the player preferences to determine which environment theme to present. + /// + public static string ENVIRONMENT_THEME_KEY = "blocks_environment_theme"; + + /// + /// Message displayed to the user when saving the model. + /// + private const string SAVE_MESSAGE = "Saving..."; + + // The default workspace, a room of 10x10x10 metres. + public static readonly Bounds DEFAULT_BOUNDS = new Bounds(Vector3.zero, new Vector3(10f, 10f, 10f)); + + // Menu actions that are selectable while a tutorial is occurring. + public static readonly List TUTORIAL_MENU_ACTIONS = + new List {MenuAction.TUTORIAL_EXIT_NO, MenuAction.TUTORIAL_EXIT_YES, MenuAction.TUTORIAL_DISMISS, + MenuAction.TUTORIAL_PROMPT}; - /// - /// The (singleton) instance. Lazily cached when the Instance property is read for the first time. - /// - private static PeltzerMain instance; + /// + /// How far the controller trigger must be pressed to be considered to be "down". + /// This is a number from 0 to 1. If it's closer to 0 it means the user has to press the trigger + /// only slightly to activate it; if it's closer to 1, it means the user must push it all the way + /// in order to activate it. + /// + /// Note: we're setting a high value because we want RELEASING the trigger to be fast, because when + /// the user releases the trigger, their hand is exactly where they want, and will often slip from the + /// correct position during the course of releasing the trigger. + /// + public static float TRIGGER_THRESHOLD = 0.95f; + + /// + /// Initial size of serializer buffers. + /// This must be reasonably big in order to avoid reallocation when saving models. + /// + private const int SERIALIZER_BUFFER_INITIAL_SIZE = 128 * 1024 * 1024; // 128 MB + + /// + /// The (singleton) instance. Lazily cached when the Instance property is read for the first time. + /// + private static PeltzerMain instance; + + /// + /// Returns the singleton instance of PeltzerMain. + /// WARNING: This object performs late initialization of certain subsystems through the TrySetup() method, which + /// runs some time *after* the object is initialized. So even though PeltzerMain.Instance will always return + /// a valid reference to the singleton PeltzerMain object, it is not guaranteed to be initialized. Be aware + /// of that when using PeltzerMain.Instance. + /// + public static PeltzerMain Instance + { + get + { + if (instance == null) + { + instance = GameObject.FindObjectOfType(); + AssertOrThrow.NotNull(instance, "No PeltzerMain object found in scene!"); + } + return instance; + } + } - /// - /// Returns the singleton instance of PeltzerMain. - /// WARNING: This object performs late initialization of certain subsystems through the TrySetup() method, which - /// runs some time *after* the object is initialized. So even though PeltzerMain.Instance will always return - /// a valid reference to the singleton PeltzerMain object, it is not guaranteed to be initialized. Be aware - /// of that when using PeltzerMain.Instance. - /// - public static PeltzerMain Instance { - get { - if (instance == null) { - instance = GameObject.FindObjectOfType(); - AssertOrThrow.NotNull(instance, "No PeltzerMain object found in scene!"); - } - return instance; - } - } + /// + /// Distance threshold in metric units for helping determine a handedness switch / resolution. + /// In Unity units where 1.0 = 1 metre. + /// + public static readonly float HANDEDNESS_DISTANCE_THRESHOLD = 0.44f; + + // Set from within Unity. + public PeltzerController peltzerController; + public PaletteController paletteController; + public GameObject hmd; + [SerializeField] + private GameObject controllerGeometryLeftRiftPrefab; + [SerializeField] + private GameObject controllerGeometryRightRiftPrefab; + [SerializeField] + private GameObject controllerGeometryVivePrefab; + + private bool running = true; + private SpatialIndex spatialIndex; + public AudioLibrary audioLibrary { get; private set; } + private MaterialLibrary materialLibrary; + private Thread generalBackgroundThread; + private Thread polyMenuBackgroundThread; + private Thread filePickerBackgroundThread; + private ConcurrentQueue generalBackgroundQueue = new ConcurrentQueue(); + private ConcurrentQueue polyMenuBackgroundQueue = new ConcurrentQueue(); + private ConcurrentQueue filePickerBackgroundQueue = new ConcurrentQueue(); + private ConcurrentQueue forMainThread = new ConcurrentQueue(); + public AutoThumbnailCamera autoThumbnailCamera; + public Camera eyeCamera; + public Vector3 eyeCameraPosition; + + /// + /// Whether the peltzer controller is in the right hand, which is the default state. + /// + public bool peltzerControllerInRightHand = true; + + // Controller. + public ControllerMain controllerMain { get; private set; } + + // Tools. + private Reshaper reshaper; + private Freeform freeform; + private VolumeInserter volumeInserter; + private Extruder extruder; + private Selector selector; + private Subdivider subdivider; + private Deleter deleter; + private Mover mover; + private Painter painter; + private GifRecorder gifRecorder; + private Zoomer zoomer; + + // Creations Handler. + private ZandriaCreationsManager zandriaCreationsManager; + + // Environment Handler. + public EnvironmentThemeManager environmentThemeManager; + + // Model. + public Model model { private set; get; } + private Exporter exporter; + public WorldSpace worldSpace { get; private set; } + private GridHighlightComponent gridHighlighter; + + // The ID of the current model for local saves. + public string LocalId; + // The ID of the current model for cloud saves. + public string AssetId; + // The Asset ID of the model that was most-recently saved to Zandria. + public string LastSavedAssetId; + + // Saving + public AutoSave autoSave { get; private set; } + public bool ModelChangedSinceLastSave; + // Whether the last auto-save request was denied. + public bool LastAutoSaveDenied; + // A path to the user's Poly data folder. + public string userPath { get; private set; } + // A path to the user's Poly models data folder. + public string modelsPath { get; private set; } + // A path to a special offline cache of models the user saved whilst not authenticated. + public string offlineModelsPath { get; private set; } + + // Track this user's app-level settings. + public bool HasEverStartedPoly { get; private set; } + public bool HasEverChangedColor { get; set; } + public bool HasEverShownFeaturedTooltip { get; set; } + public bool HasDisabledTooltips { get; set; } + + // Track this user's session-level settings. + // Has this user opened the save url before? + public bool HasOpenedSaveUrlThisSession { get; set; } + // Whether we've shown "click the menu button to view your models" before. + public bool HasShownMenuTooltipThisSession; + + /// + /// Indicates whether the one-time setup is complete. + /// We try it from Start() and retry it from Update() until we succeed. + /// + private bool setupDone; + private DesktopMain desktopMain; + + public PolyMenuMain polyMenuMain; + + private FloatingMessage floatingMessage; + + private PreviewController previewController; + + /// + /// Restriction manager, which indicates which modes/features are allowed or disallowed. + /// Used to implement special restricted experiences such as tutorials, mini-games, etc. + /// + public RestrictionManager restrictionManager { get; private set; } + + /// + /// Tutorial manager, which directs the execution of tutorials. + /// + public TutorialManager tutorialManager { get; private set; } + + /// + /// Attention caller, responsible for calling the user's attention to parts of the UI. + /// + public AttentionCaller attentionCaller { get; private set; } + + public TooltipManager applicationButtonToolTips { get; private set; } + + /// + /// Progress indicator. Responsible for showing the indicator that appears on the left controller + /// to inform the user that some long operation is in progress. + /// + public ProgressIndicator progressIndicator { get; private set; } + + /// + /// Action to execute after a successful save. Currently only used to ensure that the model actually saves when + /// NEW_WITH_SAVE is executed from the menu. + /// + public Action saveCompleteAction; + + /// + /// Controller swap gesture detector. + /// + private ControllerSwapDetector controllerSwapDetector; + + /// + /// Creates and animates a preview of the model that a user just saved. + /// + public SavePreview savePreview; + + /// + /// Creates and animates a hint for the menu button. + /// + public MenuHint menuHint; + + /// + /// Manages reference images. + /// + public ReferenceImageManager referenceImageManager { get; private set; } + + /// + /// Takes care of choreographing the startup sequence. + /// + public IntroChoreographer introChoreographer { get; private set; } + + /// + /// Restriction manager, which indicates which modes/features are allowed or disallowed. + /// Used to implement special restricted experiences such as tutorials, mini-games, etc. + /// + public HighlightUtils highlightUtils { get; private set; } + + /// + /// Poly Worldspace bounding box reference. + /// + public PolyWorldBounds polyWorldBounds; + + // Variables to determine when to trigger snap tooltips. + // TODO: Move all tooltip logic to a separate manager to avoid polluting PeltzerMain. + public int volumesInserted; + public bool snappedInVolumeInserter; + public int movesCompleted; + public bool snappedInMover; + public int faceReshapesCompleted; + public bool snappedWhenReshapingFaces; + public int extrudesCompleted; + public bool snappedInExtruder; + public int subdividesCompleted; + public bool snappedInSubdivider; + + // An ugly hack for bug -- we let the assets service client directly update the modelId in PeltzerMain + // once save has completed (such that we can overwrite), but this can be problematic if a user has chosen to + // start a new model since first hitting save. + public bool newModelSinceLastSaved; + + // Serializer we use when saving models. This is for "manual" save (invoked by the user), as opposed to auto save. + private PolySerializer serializerForManualSave = new PolySerializer(); + + // Serializer we use for auto-save (must be separate from serializerForManualSave because autosave happens on + // the background thread). + private PolySerializer serializerForAutoSave = new PolySerializer(); + + /// + /// Web request manager, which centralizes the logic of issuing and waiting for web requests. + /// + public WebRequestManager webRequestManager { get; private set; } + + public PeltzerMain() + { + // Working space is a 6m cube centered at the origin. + worldSpace = new WorldSpace(DEFAULT_BOUNDS); + } - /// - /// Distance threshold in metric units for helping determine a handedness switch / resolution. - /// In Unity units where 1.0 = 1 metre. - /// - public static readonly float HANDEDNESS_DISTANCE_THRESHOLD = 0.44f; - - // Set from within Unity. - public PeltzerController peltzerController; - public PaletteController paletteController; - public GameObject hmd; - [SerializeField] - private GameObject controllerGeometryLeftRiftPrefab; - [SerializeField] - private GameObject controllerGeometryRightRiftPrefab; - [SerializeField] - private GameObject controllerGeometryVivePrefab; - - private bool running = true; - private SpatialIndex spatialIndex; - public AudioLibrary audioLibrary { get; private set; } - private MaterialLibrary materialLibrary; - private Thread generalBackgroundThread; - private Thread polyMenuBackgroundThread; - private Thread filePickerBackgroundThread; - private ConcurrentQueue generalBackgroundQueue = new ConcurrentQueue(); - private ConcurrentQueue polyMenuBackgroundQueue = new ConcurrentQueue(); - private ConcurrentQueue filePickerBackgroundQueue = new ConcurrentQueue(); - private ConcurrentQueue forMainThread = new ConcurrentQueue(); - public AutoThumbnailCamera autoThumbnailCamera; - public Camera eyeCamera; - public Vector3 eyeCameraPosition; + void Start() + { + // Check the user's app-level settings in the registry, if any. + if (PlayerPrefs.HasKey(FIRST_TIME_KEY)) + { + HasEverStartedPoly = true; + } + else + { + HasEverStartedPoly = false; + PlayerPrefs.SetString(FIRST_TIME_KEY, "true"); + PlayerPrefs.Save(); + } - /// - /// Whether the peltzer controller is in the right hand, which is the default state. - /// - public bool peltzerControllerInRightHand = true; - - // Controller. - public ControllerMain controllerMain { get; private set; } - - // Tools. - private Reshaper reshaper; - private Freeform freeform; - private VolumeInserter volumeInserter; - private Extruder extruder; - private Selector selector; - private Subdivider subdivider; - private Deleter deleter; - private Mover mover; - private Painter painter; - private GifRecorder gifRecorder; - private Zoomer zoomer; - - // Creations Handler. - private ZandriaCreationsManager zandriaCreationsManager; - - // Environment Handler. - public EnvironmentThemeManager environmentThemeManager; - - // Model. - public Model model { private set; get; } - private Exporter exporter; - public WorldSpace worldSpace { get; private set; } - private GridHighlightComponent gridHighlighter; - - // The ID of the current model for local saves. - public string LocalId; - // The ID of the current model for cloud saves. - public string AssetId; - // The Asset ID of the model that was most-recently saved to Zandria. - public string LastSavedAssetId; - - // Saving - public AutoSave autoSave { get; private set; } - public bool ModelChangedSinceLastSave; - // Whether the last auto-save request was denied. - public bool LastAutoSaveDenied; - // A path to the user's Poly data folder. - public string userPath { get; private set; } - // A path to the user's Poly models data folder. - public string modelsPath { get; private set; } - // A path to a special offline cache of models the user saved whilst not authenticated. - public string offlineModelsPath { get; private set; } - - // Track this user's app-level settings. - public bool HasEverStartedPoly { get; private set; } - public bool HasEverChangedColor { get; set; } - public bool HasEverShownFeaturedTooltip { get; set; } - public bool HasDisabledTooltips { get; set; } - - // Track this user's session-level settings. - // Has this user opened the save url before? - public bool HasOpenedSaveUrlThisSession { get; set; } - // Whether we've shown "click the menu button to view your models" before. - public bool HasShownMenuTooltipThisSession; + // Initializes static buffers we're using for optimizing setting of list values. + ReMesher.InitBufferCaches(); - /// - /// Indicates whether the one-time setup is complete. - /// We try it from Start() and retry it from Update() until we succeed. - /// - private bool setupDone; - private DesktopMain desktopMain; + // Set up the authentication. + gameObject.AddComponent(); - public PolyMenuMain polyMenuMain; + // Create and set up the web request manager. + webRequestManager = gameObject.AddComponent(); + webRequestManager.Setup(new WebRequestManager.WebRequestManagerConfig(AssetsServiceClient.POLY_KEY)); - private FloatingMessage floatingMessage; + // Add Oculus SDK stuff. + if (Config.Instance.sdkMode == SdkMode.Oculus) + { + OVRManager manager = gameObject.AddComponent(); + manager.trackingOriginType = OVRManager.TrackingOrigin.FloorLevel; + OculusAuth oculusAuth = gameObject.AddComponent(); + } - private PreviewController previewController; + // Add Vive hardware stuff. + if (Config.Instance.VrHardware == VrHardware.Rift) + { + // Create the left controller geometry for the palette controller. + GameObject controllerGeometryLeft; + if (Config.Instance.sdkMode == SdkMode.Oculus) + { + controllerGeometryLeft = Instantiate(controllerGeometryLeftRiftPrefab, paletteController.oculusRiftHolder.transform, false); + } + else + { + controllerGeometryLeft = Instantiate(controllerGeometryLeftRiftPrefab, paletteController.steamRiftHolder.transform, false); + } + paletteController.controllerGeometry = controllerGeometryLeft.GetComponent(); + + // Create the right controller geometry for the peltzer controller. + GameObject controllerGeometryRight; + if (Config.Instance.sdkMode == SdkMode.Oculus) + { + controllerGeometryRight = Instantiate(controllerGeometryRightRiftPrefab, peltzerController.oculusRiftHolder.transform, false); + } + else + { + controllerGeometryRight = Instantiate(controllerGeometryRightRiftPrefab, peltzerController.steamRiftHolder.transform, false); + } + peltzerController.controllerGeometry = controllerGeometryRight.GetComponent(); + + // Only allow hand toggling from the menu if the user is using a Rift. + ObjectFinder.ObjectById("ID_toggle_left_handed").SetActive(true); + ObjectFinder.ObjectById("ID_small_menu_div").SetActive(true); + ObjectFinder.ObjectById("ID_large_menu_div").SetActive(false); + } + else + { + // Create the left controller geometry for the palette controller. + var controllerGeometryLeft = Instantiate(controllerGeometryVivePrefab, paletteController.transform, false); + paletteController.controllerGeometry = controllerGeometryLeft.GetComponent(); + + // Create the right controller geometry for the peltzer controller. + var controllerGeometryRight = Instantiate(controllerGeometryVivePrefab, peltzerController.transform, false); + peltzerController.controllerGeometry = controllerGeometryRight.GetComponent(); + + // Don't allow hand toggling if the user is using a Vive. + ObjectFinder.ObjectById("ID_toggle_left_handed").SetActive(false); + ObjectFinder.ObjectById("ID_small_menu_div").SetActive(false); + ObjectFinder.ObjectById("ID_large_menu_div").SetActive(true); + } - /// - /// Restriction manager, which indicates which modes/features are allowed or disallowed. - /// Used to implement special restricted experiences such as tutorials, mini-games, etc. - /// - public RestrictionManager restrictionManager { get; private set; } + HashSet tips = new HashSet(); + tips.Add(new ControllerTooltip("ViewSaved", "View saved models", .044f)); + tips.Add(new ControllerTooltip("ViewFeatured", "View featured models", .052f)); + + applicationButtonToolTips = new TooltipManager(tips, + paletteController.controllerGeometry.applicationButtonTooltipRoot, + paletteController.controllerGeometry.applicationButtonTooltipLeft, + paletteController.controllerGeometry.applicationButtonTooltipRight); + + // Get the MaterialLibrary + materialLibrary = FindObjectOfType(); + + // Init Materials + MaterialRegistry.init(materialLibrary); + + // Pass the highlight material to the MeshHelper. + MeshHelper.highlightSilhouetteMaterial = MaterialRegistry.getHighlightSilhouetteMaterial(); + + // Pre-allocate the serializer buffers to avoid having to do that when saving the model. + serializerForAutoSave = new PolySerializer(); + serializerForAutoSave.SetupForWriting(/* minInitialCapacity */ SERIALIZER_BUFFER_INITIAL_SIZE); + serializerForManualSave = new PolySerializer(); + serializerForManualSave.SetupForWriting(/* minInitialCapacity */ SERIALIZER_BUFFER_INITIAL_SIZE); + + // Find the eye camera. + eyeCamera = ObjectFinder.ComponentById("ID_Camera (eye)") as Camera; + + // Create initial empty model. + model = new Model(worldSpace.bounds); + spatialIndex = new SpatialIndex(model, worldSpace.bounds); + SetupSpatialIndex(); + generalBackgroundThread = new Thread(ProcessGeneralBackgroundWork); + generalBackgroundThread.IsBackground = true; + generalBackgroundThread.Priority = System.Threading.ThreadPriority.Lowest; + generalBackgroundThread.Start(); + polyMenuBackgroundThread = new Thread(ProcessPolyMenuBackgroundWork); + polyMenuBackgroundThread.IsBackground = true; + polyMenuBackgroundThread.Priority = System.Threading.ThreadPriority.Lowest; + polyMenuBackgroundThread.Start(); + filePickerBackgroundThread = new Thread(ProcessFilePickerBackgroundWork); + filePickerBackgroundThread.IsBackground = true; + filePickerBackgroundThread.Priority = System.Threading.ThreadPriority.Lowest; + filePickerBackgroundThread.Start(); + + // Set up auto-saving. + userPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal); + + // GetFolderPath() can fail, returning an empty string. + if (userPath == "") + { + // If that happens, try a bunch of other folders. + userPath = System.Environment.GetFolderPath( + System.Environment.SpecialFolder.MyDocuments); + if (userPath == "") + { + userPath = System.Environment.GetFolderPath( + System.Environment.SpecialFolder.DesktopDirectory); + } + } - /// - /// Tutorial manager, which directs the execution of tutorials. - /// - public TutorialManager tutorialManager { get; private set; } + userPath = Path.Combine(userPath, "Blocks"); + if (!Path.IsPathRooted(userPath)) + { + Debug.Log("Failed to find Documents folder."); + } - /// - /// Attention caller, responsible for calling the user's attention to parts of the UI. - /// - public AttentionCaller attentionCaller { get; private set; } + modelsPath = Path.Combine(userPath, "Models"); + offlineModelsPath = Path.Combine(userPath, "OfflineModels"); + // TODO(bug): Actually do something incremental with these commands instead of persisting the + // whole state after every command. + autoSave = new AutoSave(model, modelsPath); + model.OnCommandApplied += ((Command command) => + { + TryAutoSave(); + ModelChangedSinceLastSave = true; + SetSaveButtonActiveIfModelNotEmpty(); + }); + model.OnUndo += ((Command command) => + { + TryAutoSave(); + ModelChangedSinceLastSave = true; + SetSaveButtonActiveIfModelNotEmpty(); + }); + model.OnRedo += ((Command command) => + { + TryAutoSave(); + ModelChangedSinceLastSave = true; + SetSaveButtonActiveIfModelNotEmpty(); + }); + + // Get the AudioLibrary and play the startup sound. + audioLibrary = FindObjectOfType(); + audioLibrary.Setup(); + + // Get the menu main. + polyMenuMain = FindObjectOfType(); + + // The previewController handles opening the image dialog and loading a reference. + previewController = FindObjectOfType(); + + // Get the desktop UI Main + desktopMain = FindObjectOfType(); + + // Get the ZandriaCreationsManager. + zandriaCreationsManager = FindObjectOfType(); + + // Get the EnvironmentThemeManager. + environmentThemeManager = ObjectFinder.ObjectById("ID_Environment").GetComponent(); + + // Get the worldspace bounding box. + polyWorldBounds = ObjectFinder.ObjectById("ID_PolyWorldBounds").GetComponent(); + + // Try to perform the setup. If we fail, that's ok, we'll try again in Update() until we succeed. + TrySetup(); + } - public TooltipManager applicationButtonToolTips { get; private set; } + /// + /// Tries to perform the one-time setup, if we're ready. + /// + /// True if setup was done, false if not done. + private bool TrySetup() + { + if (!PeltzerController.AcquireIfNecessary(ref peltzerController)) + { + Debug.LogWarning("couldn't find peltzer controller!"); + return false; + } + if (!PaletteController.AcquireIfNecessary(ref paletteController)) + { + Debug.LogWarning("couldn't find palette controller!"); + return false; + } - /// - /// Progress indicator. Responsible for showing the indicator that appears on the left controller - /// to inform the user that some long operation is in progress. - /// - public ProgressIndicator progressIndicator { get; private set; } + Debug.Log("Starting up, v" + Config.Instance.version); + + restrictionManager = new RestrictionManager(); + + // PeltzerController needs to grab references to some tools, so we need to add them first. + // We call Setup() on them later, though (since that requires PeltzerController to be fully set up). + volumeInserter = gameObject.AddComponent(); + freeform = gameObject.AddComponent(); + peltzerController.Setup(volumeInserter, freeform); + paletteController.Setup(); + controllerMain = new ControllerMain(peltzerController, paletteController); + tutorialManager = gameObject.AddComponent(); + attentionCaller = gameObject.AddComponent(); + attentionCaller.Setup(peltzerController, paletteController); + progressIndicator = gameObject.AddComponent(); + + savePreview = gameObject.AddComponent(); + savePreview.Setup(); + + menuHint = gameObject.AddComponent(); + menuHint.Setup(); + + // Tools. + zoomer = gameObject.AddComponent(); + zoomer.Setup(controllerMain, peltzerController, paletteController, worldSpace, audioLibrary); + highlightUtils = gameObject.AddComponent(); + highlightUtils.Setup(worldSpace, model, materialLibrary); + selector = gameObject.AddComponent(); + selector.Setup(model, controllerMain, peltzerController, paletteController, worldSpace, spatialIndex, + highlightUtils, materialLibrary); + freeform.Setup(model, controllerMain, peltzerController, audioLibrary, worldSpace); + volumeInserter.Setup(model, controllerMain, peltzerController, audioLibrary, worldSpace, + spatialIndex, model.meshRepresentationCache, selector); + reshaper = gameObject.AddComponent(); + reshaper.Setup(model, controllerMain, peltzerController, paletteController, selector, + audioLibrary, worldSpace, spatialIndex, model.meshRepresentationCache); + mover = gameObject.AddComponent(); + mover.Setup(model, controllerMain, peltzerController, paletteController, selector, volumeInserter, + audioLibrary, worldSpace, spatialIndex, model.meshRepresentationCache); + extruder = gameObject.AddComponent(); + extruder.Setup(model, controllerMain, peltzerController, paletteController, selector, + audioLibrary, worldSpace); + subdivider = gameObject.AddComponent(); + subdivider.Setup(model, controllerMain, peltzerController, paletteController, selector, + audioLibrary, worldSpace); + deleter = gameObject.AddComponent(); + deleter.Setup(model, controllerMain, peltzerController, selector, audioLibrary); + painter = gameObject.AddComponent(); + painter.Setup(model, controllerMain, peltzerController, selector, audioLibrary); + gifRecorder = gameObject.AddComponent(); + UXEffectManager.Setup(model.meshRepresentationCache, materialLibrary, worldSpace); + gridHighlighter = gameObject.AddComponent(); + gridHighlighter.Setup(materialLibrary, worldSpace, peltzerController); + + // Register cross controller handlers. + paletteController.RegisterCrossControllerHandlers(peltzerController); + + desktopMain.Setup(); + + // Model. + exporter = gameObject.AddComponent(); + // Setup FBX exporter. + FbxExporter.Setup(); + + // Starts the call to authenticate. + zandriaCreationsManager.Setup(); + + // Menu. + polyMenuMain.Setup(zandriaCreationsManager, paletteController); + + introChoreographer = gameObject.AddComponent(); + introChoreographer.Setup(audioLibrary, peltzerController, paletteController); + + floatingMessage = gameObject.AddComponent(); + floatingMessage.Setup(); + + // Controller switch gesture detector. + if (Features.enableControllerSwapping) + { + controllerSwapDetector = gameObject.AddComponent(); + controllerSwapDetector.Setup(); + } - /// - /// Action to execute after a successful save. Currently only used to ensure that the model actually saves when - /// NEW_WITH_SAVE is executed from the menu. - /// - public Action saveCompleteAction; + referenceImageManager = gameObject.AddComponent(); - /// - /// Controller swap gesture detector. - /// - private ControllerSwapDetector controllerSwapDetector; + // If the user logged in previously, then load their logged-in state, but don't prompt them to login otherwise. + SignIn(/* promptUserIfNoToken */ false); - /// - /// Creates and animates a preview of the model that a user just saved. - /// - public SavePreview savePreview; + // If the user has disabled tooltips according to their player preferences, toggle tooltips to 'off'. + if (PlayerPrefs.HasKey(DISABLE_TOOLTIPS_KEY) && PlayerPrefs.GetString(DISABLE_TOOLTIPS_KEY) == "true") + { + ToggleTooltipDisplay(); + } - /// - /// Creates and animates a hint for the menu button. - /// - public MenuHint menuHint; + // If the user has disabled sound according to their player preferences, toggle sounds to 'off'. + if (PlayerPrefs.HasKey(DISABLE_SOUNDS_KEY) && PlayerPrefs.GetString(DISABLE_SOUNDS_KEY) == "true") + { + audioLibrary.ToggleSounds(); + } - /// - /// Manages reference images. - /// - public ReferenceImageManager referenceImageManager { get; private set; } + // Set the environment theme based on last session. + environmentThemeManager.Setup(); + if (PlayerPrefs.HasKey(ENVIRONMENT_THEME_KEY)) + { + switch (PlayerPrefs.GetInt(ENVIRONMENT_THEME_KEY)) + { + case (int)EnvironmentThemeManager.EnvironmentTheme.DAY: + environmentThemeManager.SetEnvironment(EnvironmentThemeManager.EnvironmentTheme.DAY); + break; + case (int)EnvironmentThemeManager.EnvironmentTheme.PURPLE: + environmentThemeManager.SetEnvironment(EnvironmentThemeManager.EnvironmentTheme.PURPLE); + break; + case (int)EnvironmentThemeManager.EnvironmentTheme.BLACK: + environmentThemeManager.SetEnvironment(EnvironmentThemeManager.EnvironmentTheme.BLACK); + break; + case (int)EnvironmentThemeManager.EnvironmentTheme.WHITE: + environmentThemeManager.SetEnvironment(EnvironmentThemeManager.EnvironmentTheme.WHITE); + break; + default: + environmentThemeManager.SetEnvironment(EnvironmentThemeManager.EnvironmentTheme.DAY); + break; + } + } + setupDone = true; + return true; + } - /// - /// Takes care of choreographing the startup sequence. - /// - public IntroChoreographer introChoreographer { get; private set; } + void Update() + { + if (!setupDone && !TrySetup()) + { + // Couldn't do set up yet, so wait. + return; + } - /// - /// Restriction manager, which indicates which modes/features are allowed or disallowed. - /// Used to implement special restricted experiences such as tutorials, mini-games, etc. - /// - public HighlightUtils highlightUtils { get; private set; } + if (LastAutoSaveDenied) + { + TryAutoSave(); + } - /// - /// Poly Worldspace bounding box reference. - /// - public PolyWorldBounds polyWorldBounds; - - // Variables to determine when to trigger snap tooltips. - // TODO: Move all tooltip logic to a separate manager to avoid polluting PeltzerMain. - public int volumesInserted; - public bool snappedInVolumeInserter; - public int movesCompleted; - public bool snappedInMover; - public int faceReshapesCompleted; - public bool snappedWhenReshapingFaces; - public int extrudesCompleted; - public bool snappedInExtruder; - public int subdividesCompleted; - public bool snappedInSubdivider; - - // An ugly hack for bug -- we let the assets service client directly update the modelId in PeltzerMain - // once save has completed (such that we can overwrite), but this can be problematic if a user has chosen to - // start a new model since first hitting save. - public bool newModelSinceLastSaved; - - // Serializer we use when saving models. This is for "manual" save (invoked by the user), as opposed to auto save. - private PolySerializer serializerForManualSave = new PolySerializer(); - - // Serializer we use for auto-save (must be separate from serializerForManualSave because autosave happens on - // the background thread). - private PolySerializer serializerForAutoSave = new PolySerializer(); + // Detect if the user is left- or right- handed. We do this every frame to deal with them putting down + // and picking up controllers. + ResolveControllerHandedness(); + + // While we have done less than 5ms of work from the work queue, start doing new work. Note that the entirety of + // the de-queued new work will be performed this frame. + float startTime = Time.realtimeSinceStartup; + BackgroundWork work; + while ((Time.realtimeSinceStartup - startTime) < 0.005f && forMainThread.Dequeue(out work)) + { + work.PostWork(); + } - /// - /// Web request manager, which centralizes the logic of issuing and waiting for web requests. - /// - public WebRequestManager webRequestManager { get; private set; } + UXEffectManager.GetEffectManager().Update(); - public PeltzerMain() { - // Working space is a 6m cube centered at the origin. - worldSpace = new WorldSpace(DEFAULT_BOUNDS); - } + // Record the position of the camera because it is needed for saves, and background threads + // are unable to access gameObject transforms. + eyeCameraPosition = eyeCamera.transform.position; + } - void Start() { - // Check the user's app-level settings in the registry, if any. - if (PlayerPrefs.HasKey(FIRST_TIME_KEY)) { - HasEverStartedPoly = true; - } else { - HasEverStartedPoly = false; - PlayerPrefs.SetString(FIRST_TIME_KEY, "true"); - PlayerPrefs.Save(); - } - - // Initializes static buffers we're using for optimizing setting of list values. - ReMesher.InitBufferCaches(); - - // Set up the authentication. - gameObject.AddComponent(); - - // Create and set up the web request manager. - webRequestManager = gameObject.AddComponent(); - webRequestManager.Setup(new WebRequestManager.WebRequestManagerConfig(AssetsServiceClient.POLY_KEY)); - - // Add Oculus SDK stuff. - if (Config.Instance.sdkMode == SdkMode.Oculus) { - OVRManager manager = gameObject.AddComponent(); - manager.trackingOriginType = OVRManager.TrackingOrigin.FloorLevel; - OculusAuth oculusAuth = gameObject.AddComponent(); - } - - // Add Vive hardware stuff. - if (Config.Instance.VrHardware == VrHardware.Rift) { - // Create the left controller geometry for the palette controller. - GameObject controllerGeometryLeft; - if (Config.Instance.sdkMode == SdkMode.Oculus) { - controllerGeometryLeft = Instantiate(controllerGeometryLeftRiftPrefab, paletteController.oculusRiftHolder.transform, false); - } else { - controllerGeometryLeft = Instantiate(controllerGeometryLeftRiftPrefab, paletteController.steamRiftHolder.transform, false); - } - paletteController.controllerGeometry = controllerGeometryLeft.GetComponent(); - - // Create the right controller geometry for the peltzer controller. - GameObject controllerGeometryRight; - if (Config.Instance.sdkMode == SdkMode.Oculus) { - controllerGeometryRight = Instantiate(controllerGeometryRightRiftPrefab, peltzerController.oculusRiftHolder.transform, false); - } else { - controllerGeometryRight = Instantiate(controllerGeometryRightRiftPrefab, peltzerController.steamRiftHolder.transform, false); - } - peltzerController.controllerGeometry = controllerGeometryRight.GetComponent(); - - // Only allow hand toggling from the menu if the user is using a Rift. - ObjectFinder.ObjectById("ID_toggle_left_handed").SetActive(true); - ObjectFinder.ObjectById("ID_small_menu_div").SetActive(true); - ObjectFinder.ObjectById("ID_large_menu_div").SetActive(false); - } else { - // Create the left controller geometry for the palette controller. - var controllerGeometryLeft = Instantiate(controllerGeometryVivePrefab, paletteController.transform, false); - paletteController.controllerGeometry = controllerGeometryLeft.GetComponent(); - - // Create the right controller geometry for the peltzer controller. - var controllerGeometryRight = Instantiate(controllerGeometryVivePrefab, peltzerController.transform, false); - peltzerController.controllerGeometry = controllerGeometryRight.GetComponent(); - - // Don't allow hand toggling if the user is using a Vive. - ObjectFinder.ObjectById("ID_toggle_left_handed").SetActive(false); - ObjectFinder.ObjectById("ID_small_menu_div").SetActive(false); - ObjectFinder.ObjectById("ID_large_menu_div").SetActive(true); - } - - HashSet tips = new HashSet(); - tips.Add(new ControllerTooltip("ViewSaved", "View saved models", .044f)); - tips.Add(new ControllerTooltip("ViewFeatured", "View featured models", .052f)); - - applicationButtonToolTips = new TooltipManager(tips, - paletteController.controllerGeometry.applicationButtonTooltipRoot, - paletteController.controllerGeometry.applicationButtonTooltipLeft, - paletteController.controllerGeometry.applicationButtonTooltipRight); - - // Get the MaterialLibrary - materialLibrary = FindObjectOfType(); - - // Init Materials - MaterialRegistry.init(materialLibrary); - - // Pass the highlight material to the MeshHelper. - MeshHelper.highlightSilhouetteMaterial = MaterialRegistry.getHighlightSilhouetteMaterial(); - - // Pre-allocate the serializer buffers to avoid having to do that when saving the model. - serializerForAutoSave = new PolySerializer(); - serializerForAutoSave.SetupForWriting(/* minInitialCapacity */ SERIALIZER_BUFFER_INITIAL_SIZE); - serializerForManualSave = new PolySerializer(); - serializerForManualSave.SetupForWriting(/* minInitialCapacity */ SERIALIZER_BUFFER_INITIAL_SIZE); - - // Find the eye camera. - eyeCamera = ObjectFinder.ComponentById("ID_Camera (eye)") as Camera; - - // Create initial empty model. - model = new Model(worldSpace.bounds); - spatialIndex = new SpatialIndex(model, worldSpace.bounds); - SetupSpatialIndex(); - generalBackgroundThread = new Thread(ProcessGeneralBackgroundWork); - generalBackgroundThread.IsBackground = true; - generalBackgroundThread.Priority = System.Threading.ThreadPriority.Lowest; - generalBackgroundThread.Start(); - polyMenuBackgroundThread = new Thread(ProcessPolyMenuBackgroundWork); - polyMenuBackgroundThread.IsBackground = true; - polyMenuBackgroundThread.Priority = System.Threading.ThreadPriority.Lowest; - polyMenuBackgroundThread.Start(); - filePickerBackgroundThread = new Thread(ProcessFilePickerBackgroundWork); - filePickerBackgroundThread.IsBackground = true; - filePickerBackgroundThread.Priority = System.Threading.ThreadPriority.Lowest; - filePickerBackgroundThread.Start(); - - // Set up auto-saving. - userPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal); - - // GetFolderPath() can fail, returning an empty string. - if (userPath == "") { - // If that happens, try a bunch of other folders. - userPath = System.Environment.GetFolderPath( - System.Environment.SpecialFolder.MyDocuments); - if (userPath == "") { - userPath = System.Environment.GetFolderPath( - System.Environment.SpecialFolder.DesktopDirectory); - } - } - - userPath = Path.Combine(userPath, "Blocks"); - if (!Path.IsPathRooted(userPath)) { - Debug.Log("Failed to find Documents folder."); - } - - modelsPath = Path.Combine(userPath, "Models"); - offlineModelsPath = Path.Combine(userPath, "OfflineModels"); - // TODO(bug): Actually do something incremental with these commands instead of persisting the - // whole state after every command. - autoSave = new AutoSave(model, modelsPath); - model.OnCommandApplied += ((Command command) => { - TryAutoSave(); - ModelChangedSinceLastSave = true; - SetSaveButtonActiveIfModelNotEmpty(); - }); - model.OnUndo += ((Command command) => { - TryAutoSave(); - ModelChangedSinceLastSave = true; - SetSaveButtonActiveIfModelNotEmpty(); - }); - model.OnRedo += ((Command command) => { - TryAutoSave(); - ModelChangedSinceLastSave = true; - SetSaveButtonActiveIfModelNotEmpty(); - }); - - // Get the AudioLibrary and play the startup sound. - audioLibrary = FindObjectOfType(); - audioLibrary.Setup(); - - // Get the menu main. - polyMenuMain = FindObjectOfType(); - - // The previewController handles opening the image dialog and loading a reference. - previewController = FindObjectOfType(); - - // Get the desktop UI Main - desktopMain = FindObjectOfType(); - - // Get the ZandriaCreationsManager. - zandriaCreationsManager = FindObjectOfType(); - - // Get the EnvironmentThemeManager. - environmentThemeManager = ObjectFinder.ObjectById("ID_Environment").GetComponent(); - - // Get the worldspace bounding box. - polyWorldBounds = ObjectFinder.ObjectById("ID_PolyWorldBounds").GetComponent(); - - // Try to perform the setup. If we fail, that's ok, we'll try again in Update() until we succeed. - TrySetup(); - } - /// - /// Tries to perform the one-time setup, if we're ready. - /// - /// True if setup was done, false if not done. - private bool TrySetup() { - if (!PeltzerController.AcquireIfNecessary(ref peltzerController)) { - Debug.LogWarning("couldn't find peltzer controller!"); - return false; - } - if (!PaletteController.AcquireIfNecessary(ref paletteController)) { - Debug.LogWarning("couldn't find palette controller!"); - return false; - } - - Debug.Log("Starting up, v" + Config.Instance.version); - - restrictionManager = new RestrictionManager(); - - // PeltzerController needs to grab references to some tools, so we need to add them first. - // We call Setup() on them later, though (since that requires PeltzerController to be fully set up). - volumeInserter = gameObject.AddComponent(); - freeform = gameObject.AddComponent(); - peltzerController.Setup(volumeInserter, freeform); - paletteController.Setup(); - controllerMain = new ControllerMain(peltzerController, paletteController); - tutorialManager = gameObject.AddComponent(); - attentionCaller = gameObject.AddComponent(); - attentionCaller.Setup(peltzerController, paletteController); - progressIndicator = gameObject.AddComponent(); - - savePreview = gameObject.AddComponent(); - savePreview.Setup(); - - menuHint = gameObject.AddComponent(); - menuHint.Setup(); - - // Tools. - zoomer = gameObject.AddComponent(); - zoomer.Setup(controllerMain, peltzerController, paletteController, worldSpace, audioLibrary); - highlightUtils = gameObject.AddComponent(); - highlightUtils.Setup(worldSpace, model, materialLibrary); - selector = gameObject.AddComponent(); - selector.Setup(model, controllerMain, peltzerController, paletteController, worldSpace, spatialIndex, - highlightUtils, materialLibrary); - freeform.Setup(model, controllerMain, peltzerController, audioLibrary, worldSpace); - volumeInserter.Setup(model, controllerMain, peltzerController, audioLibrary, worldSpace, - spatialIndex, model.meshRepresentationCache, selector); - reshaper = gameObject.AddComponent(); - reshaper.Setup(model, controllerMain, peltzerController, paletteController, selector, - audioLibrary, worldSpace, spatialIndex, model.meshRepresentationCache); - mover = gameObject.AddComponent(); - mover.Setup(model, controllerMain, peltzerController, paletteController, selector, volumeInserter, - audioLibrary, worldSpace, spatialIndex, model.meshRepresentationCache); - extruder = gameObject.AddComponent(); - extruder.Setup(model, controllerMain, peltzerController, paletteController, selector, - audioLibrary, worldSpace); - subdivider = gameObject.AddComponent(); - subdivider.Setup(model, controllerMain, peltzerController, paletteController, selector, - audioLibrary, worldSpace); - deleter = gameObject.AddComponent(); - deleter.Setup(model, controllerMain, peltzerController, selector, audioLibrary); - painter = gameObject.AddComponent(); - painter.Setup(model, controllerMain, peltzerController, selector, audioLibrary); - gifRecorder = gameObject.AddComponent(); - UXEffectManager.Setup(model.meshRepresentationCache, materialLibrary, worldSpace); - gridHighlighter = gameObject.AddComponent(); - gridHighlighter.Setup(materialLibrary, worldSpace, peltzerController); - - // Register cross controller handlers. - paletteController.RegisterCrossControllerHandlers(peltzerController); - - desktopMain.Setup(); - - // Model. - exporter = gameObject.AddComponent(); - // Setup FBX exporter. - FbxExporter.Setup(); - - // Starts the call to authenticate. - zandriaCreationsManager.Setup(); - - // Menu. - polyMenuMain.Setup(zandriaCreationsManager, paletteController); - - introChoreographer = gameObject.AddComponent(); - introChoreographer.Setup(audioLibrary, peltzerController, paletteController); - - floatingMessage = gameObject.AddComponent(); - floatingMessage.Setup(); - - // Controller switch gesture detector. - if (Features.enableControllerSwapping) { - controllerSwapDetector = gameObject.AddComponent(); - controllerSwapDetector.Setup(); - } - - referenceImageManager = gameObject.AddComponent(); - - // If the user logged in previously, then load their logged-in state, but don't prompt them to login otherwise. - SignIn(/* promptUserIfNoToken */ false); - - // If the user has disabled tooltips according to their player preferences, toggle tooltips to 'off'. - if (PlayerPrefs.HasKey(DISABLE_TOOLTIPS_KEY) && PlayerPrefs.GetString(DISABLE_TOOLTIPS_KEY) == "true") { - ToggleTooltipDisplay(); - } - - // If the user has disabled sound according to their player preferences, toggle sounds to 'off'. - if (PlayerPrefs.HasKey(DISABLE_SOUNDS_KEY) && PlayerPrefs.GetString(DISABLE_SOUNDS_KEY) == "true") { - audioLibrary.ToggleSounds(); - } - - // Set the environment theme based on last session. - environmentThemeManager.Setup(); - if (PlayerPrefs.HasKey(ENVIRONMENT_THEME_KEY)) { - switch(PlayerPrefs.GetInt(ENVIRONMENT_THEME_KEY)) { - case (int)EnvironmentThemeManager.EnvironmentTheme.DAY: - environmentThemeManager.SetEnvironment(EnvironmentThemeManager.EnvironmentTheme.DAY); - break; - case (int)EnvironmentThemeManager.EnvironmentTheme.PURPLE: - environmentThemeManager.SetEnvironment(EnvironmentThemeManager.EnvironmentTheme.PURPLE); - break; - case (int)EnvironmentThemeManager.EnvironmentTheme.BLACK: - environmentThemeManager.SetEnvironment(EnvironmentThemeManager.EnvironmentTheme.BLACK); - break; - case (int)EnvironmentThemeManager.EnvironmentTheme.WHITE: - environmentThemeManager.SetEnvironment(EnvironmentThemeManager.EnvironmentTheme.WHITE); - break; - default: - environmentThemeManager.SetEnvironment(EnvironmentThemeManager.EnvironmentTheme.DAY); - break; - } - } - setupDone = true; - return true; - } + // All rendering needs to be done at the very end of the frame after all the state changes to + // the models have been made. + void LateUpdate() + { + // Render the model. + model.Render(); + // Render UX Effects + UXEffectManager.GetEffectManager().Render(); + } - void Update() { - if (!setupDone && !TrySetup()) { - // Couldn't do set up yet, so wait. - return; - } - - if (LastAutoSaveDenied) { - TryAutoSave(); - } - - // Detect if the user is left- or right- handed. We do this every frame to deal with them putting down - // and picking up controllers. - ResolveControllerHandedness(); - - // While we have done less than 5ms of work from the work queue, start doing new work. Note that the entirety of - // the de-queued new work will be performed this frame. - float startTime = Time.realtimeSinceStartup; - BackgroundWork work; - while ((Time.realtimeSinceStartup - startTime) < 0.005f && forMainThread.Dequeue(out work)) { - work.PostWork(); - } - - UXEffectManager.GetEffectManager().Update(); - - // Record the position of the camera because it is needed for saves, and background threads - // are unable to access gameObject transforms. - eyeCameraPosition = eyeCamera.transform.position; - } + /// + /// Sets the save button active if the model is not empty. + /// + private void SetSaveButtonActiveIfModelNotEmpty() + { + if (model.GetNumberOfMeshes() == 0) + { + attentionCaller.GreyOut(AttentionCaller.Element.SAVE_BUTTON_ICON, 0f); + } + else if (restrictionManager.menuActionsAllowed) + { + attentionCaller.Recolor(AttentionCaller.Element.SAVE_BUTTON_ICON); + } + } + /// + /// Sets the save button active if the selection is not empty. + /// + public void SetSaveSelectedButtonActiveIfSelectionNotEmpty() + { + if (!(selector.selectedMeshes.Count > 0)) + { + attentionCaller.GreyOut(AttentionCaller.Element.SAVE_SELECTED_BUTTON, 0f); + } + else if (restrictionManager.menuActionsAllowed) + { + attentionCaller.Recolor(AttentionCaller.Element.SAVE_SELECTED_BUTTON); + } + } - // All rendering needs to be done at the very end of the frame after all the state changes to - // the models have been made. - void LateUpdate() { - // Render the model. - model.Render(); - // Render UX Effects - UXEffectManager.GetEffectManager().Render(); - } + /// + /// Try and perform an auto-save. If the auto-saver is busy, mark that this request was denied, such that we can + /// try again on Update. This is preferable to implementing a Queue, as we only want to auto-save the most recent + /// state at any given point, rather than try and persist the whole command stack as individual auto-saves which + /// will overwrite eachother anyway. + /// + private void TryAutoSave() + { + if (!autoSave.IsCurrentlySaving) + { + LastAutoSaveDenied = false; + autoSave.IsCurrentlySaving = true; + DoPolyMenuBackgroundWork(new AutoSaveWork(model, model.meshRepresentationCache, autoSave, serializerForAutoSave)); + } + else + { + LastAutoSaveDenied = true; + } + } - /// - /// Sets the save button active if the model is not empty. - /// - private void SetSaveButtonActiveIfModelNotEmpty() { - if (model.GetNumberOfMeshes() == 0) { - attentionCaller.GreyOut(AttentionCaller.Element.SAVE_BUTTON_ICON, 0f); - } else if (restrictionManager.menuActionsAllowed) { - attentionCaller.Recolor(AttentionCaller.Element.SAVE_BUTTON_ICON); - } - } + /// + /// Called from the palette when an item in the file menu is "clicked". + /// + /// + public void InvokeMenuAction(MenuAction action, String featureString = null) + { + switch (action) + { + case MenuAction.CLEAR: + SetAllPromptsInactive(); + CreateNewModel(); + break; + case MenuAction.LOAD: + break; + case MenuAction.SHOW_SAVE_CONFIRM: + // Only show the save confirmation dialog if modified since last save. + // Do not offer the option to save if being called within the tutorial. + if (ModelChangedSinceLastSave && !tutorialManager.TutorialOccurring() && model.GetNumberOfMeshes() > 0) + { + // The save confirmation dialog will call InvokeMenuAction according to the user's + // decision (CLEAR, CANCEL_SAVE or NEW_WITH_SAVE). + SetAllPromptsInactive(); + paletteController.newModelPrompt.SetActive(true); + } + else + { + // Not modified since last save, so we can clear without confirmation. + InvokeMenuAction(MenuAction.CLEAR); + } + break; + case MenuAction.CANCEL_SAVE: + SetAllPromptsInactive(); + break; + case MenuAction.SAVE: + SetAllPromptsInactive(); + if (OAuth2Identity.Instance.LoggedIn) + { + SaveCurrentModel(publish: false, saveSelected: false); + } + else + { + paletteController.saveLocallyPrompt.SetActive(true); + } + break; + case MenuAction.SAVE_COPY: + SetAllPromptsInactive(); + SaveCurrentModelAsCopy(); + break; + case MenuAction.SAVE_SELECTED: + SaveCurrentSelectedModel(); + break; + case MenuAction.PUBLISH: + SetAllPromptsInactive(); + if (!OAuth2Identity.Instance.LoggedIn) + { + SignIn(/* promptUserIfNoToken */ true); + paletteController.publishSignInPrompt.SetActive(true); + } + else + { + SaveCurrentModel(publish: true, saveSelected: false); + paletteController.SetPublishDialogActive(); + } + break; + case MenuAction.PUBLISHED_TAKE_OFF_HEADSET_DISMISS: + SetAllPromptsInactive(); + break; + case MenuAction.NEW_WITH_SAVE: + SetAllPromptsInactive(); + saveCompleteAction = () => + { + InvokeMenuAction(MenuAction.CLEAR); + }; + // After the model is serialized and we're free to clear it: + SaveCurrentModel(publish: false, saveSelected: false); + break; + case MenuAction.SHARE: + break; + case MenuAction.SHOWCASE: + break; + case MenuAction.TAKE_PHOTO: + break; + case MenuAction.BLOCKMODE: + peltzerController.ToggleBlockMode(/* initiatedByUser */ true); + break; + case MenuAction.SIGN_IN: + // Prompt the user to take off their headset and sign in. + polyMenuMain.PromptUserToSignIn(); + SignIn(/* promptUserIfNoToken */ true); + break; + case MenuAction.SIGN_OUT: + SignOut(); + break; + case MenuAction.ADD_REFERENCE: + // Open a dialog to select an image. + previewController.SelectPreviewImage(); + break; + case MenuAction.TOGGLE_SOUND: + audioLibrary.ToggleSounds(); + break; + case MenuAction.TOGGLE_FEATURE: + polyWorldBounds.HandleFeatureToggle(); + break; + case MenuAction.TUTORIAL_PROMPT: + if (paletteController.tutorialBeginPrompt.activeInHierarchy || + paletteController.tutorialSavePrompt.activeInHierarchy || + paletteController.tutorialExitPrompt.activeInHierarchy) + { + SetAllPromptsInactive(); + } + else + { + SetAllPromptsInactive(); + paletteController.tutorialButton.GetComponent().material.color = PeltzerController.MENU_BUTTON_GREEN; + if (tutorialManager.TutorialOccurring()) + { + paletteController.tutorialExitPrompt.SetActive(true); + } + else + { + paletteController.tutorialBeginPrompt.SetActive(true); + } + } + break; + case MenuAction.TUTORIAL_DISMISS: + SetAllPromptsInactive(); + if (!HasEverShownFeaturedTooltip) + { + applicationButtonToolTips.TurnOn("ViewFeatured"); + polyMenuMain.SwitchToFeaturedSection(); + HasEverShownFeaturedTooltip = true; + } + break; + case MenuAction.TUTORIAL_START: + SetAllPromptsInactive(); + attentionCaller.StopGlowing(AttentionCaller.Element.TAKE_A_TUTORIAL_BUTTON); + + if (ModelChangedSinceLastSave && model.GetNumberOfMeshes() > 0) + { + paletteController.tutorialButton.GetComponent().material.color = PeltzerController.MENU_BUTTON_GREEN; + paletteController.tutorialSavePrompt.SetActive(true); + } + else + { + StartTutorial(); + } + break; + case MenuAction.TUTORIAL_CONFIRM_DISMISS: + SetAllPromptsInactive(); + break; + case MenuAction.TUTORIAL_SAVE_AND_CONFIRM: + SetAllPromptsInactive(); + saveCompleteAction = () => + { + StartTutorial(); + }; + // After the model is serialized and we're free to clear it: + SaveCurrentModel(publish: false, saveSelected: false); + break; + case MenuAction.TUTORIAL_DONT_SAVE_AND_CONFIRM: + SetAllPromptsInactive(); + StartTutorial(); + break; + case MenuAction.TUTORIAL_EXIT_YES: + SetAllPromptsInactive(); + tutorialManager.ExitTutorial(/* isForceExit */ true); + break; + case MenuAction.TUTORIAL_EXIT_NO: + SetAllPromptsInactive(); + break; + case MenuAction.PUBLISH_AFTER_SAVE_DISMISS: + SetAllPromptsInactive(); + break; + case MenuAction.PUBLISH_SIGN_IN_DISMISS: + SetAllPromptsInactive(); + break; + case MenuAction.PUBLISH_AFTER_SAVE_CONFIRM: + AssetsServiceClient.OpenPublishUrl(LastSavedAssetId); + SetAllPromptsInactive(); + paletteController.SetPublishDialogActive(); + break; + case MenuAction.SAVE_LOCALLY: + SetAllPromptsInactive(); + SaveCurrentModel(publish: false, saveSelected: false); + break; + case MenuAction.SAVE_LOCAL_SIGN_IN_INSTEAD: + SetAllPromptsInactive(); + SignInThenSaveModel(/* promptIfNoUserToken */ true); + paletteController.SetPublishDialogActive(); + break; + case MenuAction.TOGGLE_LEFT_HANDED: + controllerSwapDetector.TrySwappingControllers(); + break; + case MenuAction.TOGGLE_TOOLTIPS: + ToggleTooltipDisplay(); + break; + case MenuAction.TOGGLE_EXPAND_WIREFRAME_FEATURE: + selector.ResetInactive(); + break; + } + } - /// - /// Sets the save button active if the selection is not empty. - /// - public void SetSaveSelectedButtonActiveIfSelectionNotEmpty() { - if (!(selector.selectedMeshes.Count > 0)) { - attentionCaller.GreyOut(AttentionCaller.Element.SAVE_SELECTED_BUTTON, 0f); - } else if (restrictionManager.menuActionsAllowed) { - attentionCaller.Recolor(AttentionCaller.Element.SAVE_SELECTED_BUTTON); - } - } + /// + /// Gets the video viewer. + /// + public GameObject GetVideoViewer() + { + return ObjectFinder.ObjectById("VideoViewer"); + } - /// - /// Try and perform an auto-save. If the auto-saver is busy, mark that this request was denied, such that we can - /// try again on Update. This is preferable to implementing a Queue, as we only want to auto-save the most recent - /// state at any given point, rather than try and persist the whole command stack as individual auto-saves which - /// will overwrite eachother anyway. - /// - private void TryAutoSave() { - if (!autoSave.IsCurrentlySaving) { - LastAutoSaveDenied = false; - autoSave.IsCurrentlySaving = true; - DoPolyMenuBackgroundWork(new AutoSaveWork(model, model.meshRepresentationCache, autoSave, serializerForAutoSave)); - } else { - LastAutoSaveDenied = true; - } - } + /// + /// Toggles whether all tooltips should be disabled in the app. + /// + private void ToggleTooltipDisplay() + { + // Make the switch. + HasDisabledTooltips = !HasDisabledTooltips; + peltzerController.HideTooltips(); + paletteController.HideTooltips(); + + // Update player preferences. + PlayerPrefs.SetString(DISABLE_TOOLTIPS_KEY, HasDisabledTooltips ? "true" : "false"); + + // Update menu text. + ObjectFinder.ObjectById("ID_tooltips_are_enabled").SetActive(!HasDisabledTooltips); + ObjectFinder.ObjectById("ID_tooltips_are_disabled").SetActive(HasDisabledTooltips); + } - /// - /// Called from the palette when an item in the file menu is "clicked". - /// - /// - public void InvokeMenuAction(MenuAction action, String featureString = null) { - switch (action) { - case MenuAction.CLEAR: - SetAllPromptsInactive(); - CreateNewModel(); - break; - case MenuAction.LOAD: - break; - case MenuAction.SHOW_SAVE_CONFIRM: - // Only show the save confirmation dialog if modified since last save. - // Do not offer the option to save if being called within the tutorial. - if (ModelChangedSinceLastSave && !tutorialManager.TutorialOccurring() && model.GetNumberOfMeshes() > 0) { - // The save confirmation dialog will call InvokeMenuAction according to the user's - // decision (CLEAR, CANCEL_SAVE or NEW_WITH_SAVE). - SetAllPromptsInactive(); - paletteController.newModelPrompt.SetActive(true); - } else { - // Not modified since last save, so we can clear without confirmation. - InvokeMenuAction(MenuAction.CLEAR); - } - break; - case MenuAction.CANCEL_SAVE: - SetAllPromptsInactive(); - break; - case MenuAction.SAVE: - SetAllPromptsInactive(); - if (OAuth2Identity.Instance.LoggedIn) { - SaveCurrentModel(publish:false, saveSelected:false); - } else { - paletteController.saveLocallyPrompt.SetActive(true); - } - break; - case MenuAction.SAVE_COPY: - SetAllPromptsInactive(); - SaveCurrentModelAsCopy(); - break; - case MenuAction.SAVE_SELECTED: - SaveCurrentSelectedModel(); - break; - case MenuAction.PUBLISH: - SetAllPromptsInactive(); - if (!OAuth2Identity.Instance.LoggedIn) { - SignIn(/* promptUserIfNoToken */ true); - paletteController.publishSignInPrompt.SetActive(true); - } else { - SaveCurrentModel(publish:true, saveSelected:false); - paletteController.SetPublishDialogActive(); - } - break; - case MenuAction.PUBLISHED_TAKE_OFF_HEADSET_DISMISS: - SetAllPromptsInactive(); - break; - case MenuAction.NEW_WITH_SAVE: - SetAllPromptsInactive(); - saveCompleteAction = () => { - InvokeMenuAction(MenuAction.CLEAR); - }; - // After the model is serialized and we're free to clear it: - SaveCurrentModel(publish:false, saveSelected:false); - break; - case MenuAction.SHARE: - break; - case MenuAction.SHOWCASE: - break; - case MenuAction.TAKE_PHOTO: - break; - case MenuAction.BLOCKMODE: - peltzerController.ToggleBlockMode(/* initiatedByUser */ true); - break; - case MenuAction.SIGN_IN: - // Prompt the user to take off their headset and sign in. - polyMenuMain.PromptUserToSignIn(); - SignIn(/* promptUserIfNoToken */ true); - break; - case MenuAction.SIGN_OUT: - SignOut(); - break; - case MenuAction.ADD_REFERENCE: - // Open a dialog to select an image. - previewController.SelectPreviewImage(); - break; - case MenuAction.TOGGLE_SOUND: - audioLibrary.ToggleSounds(); - break; - case MenuAction.TOGGLE_FEATURE: - polyWorldBounds.HandleFeatureToggle(); - break; - case MenuAction.TUTORIAL_PROMPT: - if (paletteController.tutorialBeginPrompt.activeInHierarchy || - paletteController.tutorialSavePrompt.activeInHierarchy || - paletteController.tutorialExitPrompt.activeInHierarchy) { - SetAllPromptsInactive(); - } else { - SetAllPromptsInactive(); - paletteController.tutorialButton.GetComponent().material.color = PeltzerController.MENU_BUTTON_GREEN; - if (tutorialManager.TutorialOccurring()) { - paletteController.tutorialExitPrompt.SetActive(true); - } else { - paletteController.tutorialBeginPrompt.SetActive(true); + /// + /// Checks if the user has the 'is left handed' preference set and switches accordingly. This can't happen in + /// PeltzerMain Setup, and so is called once the intro sequence is complete. + /// + public void CheckLeftHandedPlayerPreference() + { + // If the user is left-handed according to their player preferences, switch their hands at setup. + if (Config.Instance.VrHardware == VrHardware.Rift && PlayerPrefs.HasKey(LEFT_HANDED_KEY) + && PlayerPrefs.GetString(LEFT_HANDED_KEY) == "true") + { + controllerSwapDetector.TrySwappingControllers(); } - } - break; - case MenuAction.TUTORIAL_DISMISS: - SetAllPromptsInactive(); - if (!HasEverShownFeaturedTooltip) { - applicationButtonToolTips.TurnOn("ViewFeatured"); - polyMenuMain.SwitchToFeaturedSection(); - HasEverShownFeaturedTooltip = true; - } - break; - case MenuAction.TUTORIAL_START: - SetAllPromptsInactive(); - attentionCaller.StopGlowing(AttentionCaller.Element.TAKE_A_TUTORIAL_BUTTON); - - if (ModelChangedSinceLastSave && model.GetNumberOfMeshes() > 0) { - paletteController.tutorialButton.GetComponent().material.color = PeltzerController.MENU_BUTTON_GREEN; - paletteController.tutorialSavePrompt.SetActive(true); - } else { - StartTutorial(); - } - break; - case MenuAction.TUTORIAL_CONFIRM_DISMISS: - SetAllPromptsInactive(); - break; - case MenuAction.TUTORIAL_SAVE_AND_CONFIRM: - SetAllPromptsInactive(); - saveCompleteAction = () => { - StartTutorial(); - }; - // After the model is serialized and we're free to clear it: - SaveCurrentModel(publish:false, saveSelected:false); - break; - case MenuAction.TUTORIAL_DONT_SAVE_AND_CONFIRM: - SetAllPromptsInactive(); - StartTutorial(); - break; - case MenuAction.TUTORIAL_EXIT_YES: - SetAllPromptsInactive(); - tutorialManager.ExitTutorial(/* isForceExit */ true); - break; - case MenuAction.TUTORIAL_EXIT_NO: - SetAllPromptsInactive(); - break; - case MenuAction.PUBLISH_AFTER_SAVE_DISMISS: - SetAllPromptsInactive(); - break; - case MenuAction.PUBLISH_SIGN_IN_DISMISS: - SetAllPromptsInactive(); - break; - case MenuAction.PUBLISH_AFTER_SAVE_CONFIRM: - AssetsServiceClient.OpenPublishUrl(LastSavedAssetId); - SetAllPromptsInactive(); - paletteController.SetPublishDialogActive(); - break; - case MenuAction.SAVE_LOCALLY: - SetAllPromptsInactive(); - SaveCurrentModel(publish:false, saveSelected:false); - break; - case MenuAction.SAVE_LOCAL_SIGN_IN_INSTEAD: - SetAllPromptsInactive(); - SignInThenSaveModel(/* promptIfNoUserToken */ true); - paletteController.SetPublishDialogActive(); - break; - case MenuAction.TOGGLE_LEFT_HANDED: - controllerSwapDetector.TrySwappingControllers(); - break; - case MenuAction.TOGGLE_TOOLTIPS: - ToggleTooltipDisplay(); - break; - case MenuAction.TOGGLE_EXPAND_WIREFRAME_FEATURE: - selector.ResetInactive(); - break; - } - } + } - /// - /// Gets the video viewer. - /// - public GameObject GetVideoViewer() { - return ObjectFinder.ObjectById("VideoViewer"); - } + public void SetPublishAfterSavePromptActive() + { + paletteController.publishAfterSavePrompt.SetActive(true); + } - /// - /// Toggles whether all tooltips should be disabled in the app. - /// - private void ToggleTooltipDisplay() { - // Make the switch. - HasDisabledTooltips = !HasDisabledTooltips; - peltzerController.HideTooltips(); - paletteController.HideTooltips(); - - // Update player preferences. - PlayerPrefs.SetString(DISABLE_TOOLTIPS_KEY, HasDisabledTooltips ? "true" : "false"); - - // Update menu text. - ObjectFinder.ObjectById("ID_tooltips_are_enabled").SetActive(!HasDisabledTooltips); - ObjectFinder.ObjectById("ID_tooltips_are_disabled").SetActive(HasDisabledTooltips); - } + private void SetAllPromptsInactive() + { + paletteController.newModelPrompt.SetActive(false); + paletteController.publishedTakeOffHeadsetPrompt.SetActive(false); + paletteController.tutorialBeginPrompt.SetActive(false); + paletteController.tutorialSavePrompt.SetActive(false); + paletteController.tutorialExitPrompt.SetActive(false); + paletteController.publishSignInPrompt.SetActive(false); + paletteController.publishAfterSavePrompt.SetActive(false); + paletteController.saveLocallyPrompt.SetActive(false); + paletteController.tutorialButton.GetComponent().material.color = PeltzerController.MENU_BUTTON_DARK; + applicationButtonToolTips.TurnOff(); + } - /// - /// Checks if the user has the 'is left handed' preference set and switches accordingly. This can't happen in - /// PeltzerMain Setup, and so is called once the intro sequence is complete. - /// - public void CheckLeftHandedPlayerPreference() { - // If the user is left-handed according to their player preferences, switch their hands at setup. - if (Config.Instance.VrHardware == VrHardware.Rift && PlayerPrefs.HasKey(LEFT_HANDED_KEY) - && PlayerPrefs.GetString(LEFT_HANDED_KEY) == "true") { - controllerSwapDetector.TrySwappingControllers(); - } - } + /// + /// Called when an EnvironmentMenuItem is clicked indicated a theme change. + /// + /// The environment theme. + public void SetEnvironmentTheme(EnvironmentThemeManager.EnvironmentTheme theme) + { + environmentThemeManager.SetEnvironment(theme); + PlayerPrefs.SetInt(ENVIRONMENT_THEME_KEY, (int)theme); + } - public void SetPublishAfterSavePromptActive() { - paletteController.publishAfterSavePrompt.SetActive(true); - } + private void StartTutorial() + { + GetMover().currentMoveType = tools.Mover.MoveType.MOVE; + paletteController.ChangeTouchpadOverlay(TouchpadOverlay.UNDO_REDO); + tutorialManager.StartTutorial(0); + } - private void SetAllPromptsInactive() { - paletteController.newModelPrompt.SetActive(false); - paletteController.publishedTakeOffHeadsetPrompt.SetActive(false); - paletteController.tutorialBeginPrompt.SetActive(false); - paletteController.tutorialSavePrompt.SetActive(false); - paletteController.tutorialExitPrompt.SetActive(false); - paletteController.publishSignInPrompt.SetActive(false); - paletteController.publishAfterSavePrompt.SetActive(false); - paletteController.saveLocallyPrompt.SetActive(false); - paletteController.tutorialButton.GetComponent().material.color = PeltzerController.MENU_BUTTON_DARK; - applicationButtonToolTips.TurnOff(); - } + /// + /// Load Zandria creations related to the logged-in user. + /// + public void LoadCreations() + { + // Start loading user creations and creations liked by the user. + zandriaCreationsManager.StartLoad(PolyMenuMain.CreationType.YOUR); + zandriaCreationsManager.StartLoad(PolyMenuMain.CreationType.LIKED); + } - /// - /// Called when an EnvironmentMenuItem is clicked indicated a theme change. - /// - /// The environment theme. - public void SetEnvironmentTheme(EnvironmentThemeManager.EnvironmentTheme theme) { - environmentThemeManager.SetEnvironment(theme); - PlayerPrefs.SetInt(ENVIRONMENT_THEME_KEY, (int)theme); - } + public void SignIn(bool promptUserIfNoToken) + { + // Sign the user in. + OAuth2Identity.Instance.Login(SignInSuccess, SignInFailure, promptUserIfNoToken); + } - private void StartTutorial() { - GetMover().currentMoveType = tools.Mover.MoveType.MOVE; - paletteController.ChangeTouchpadOverlay(TouchpadOverlay.UNDO_REDO); - tutorialManager.StartTutorial(0); - } + public void SignInThenSaveModel(bool promptUserIfNoToken) + { + // Sign the user in. + OAuth2Identity.Instance.Login(SignInSuccessWithModelSave, SignInFailure, promptUserIfNoToken); + } - /// - /// Load Zandria creations related to the logged-in user. - /// - public void LoadCreations() { - // Start loading user creations and creations liked by the user. - zandriaCreationsManager.StartLoad(PolyMenuMain.CreationType.YOUR); - zandriaCreationsManager.StartLoad(PolyMenuMain.CreationType.LIKED); - } + private void SignInSuccess() + { + // After authentication if the user actually authenticated. + // Load Zandria creations if the startup animation is not currently occurring. + if (introChoreographer.state == IntroChoreographer.State.DONE) + { + LoadCreations(); + } + else + { + // Otherwise, tell the introChoreographer to load the creations when startup is finished. + introChoreographer.loadCreationsWhenDone = true; + } - public void SignIn(bool promptUserIfNoToken) { - // Sign the user in. - OAuth2Identity.Instance.Login(SignInSuccess, SignInFailure, promptUserIfNoToken); - } + // Change the PolyMenu buttons. + polyMenuMain.SignIn(OAuth2Identity.Instance.Profile.icon, OAuth2Identity.Instance.Profile.name); + // They logged in, change the "Sign In" button to sign out. + GetDesktopMain().SignIn(OAuth2Identity.Instance.Profile.icon, OAuth2Identity.Instance.Profile.name); - public void SignInThenSaveModel(bool promptUserIfNoToken) { - // Sign the user in. - OAuth2Identity.Instance.Login(SignInSuccessWithModelSave, SignInFailure, promptUserIfNoToken); - } + paletteController.publishSignInPrompt.SetActive(false); + } - private void SignInSuccess() { - // After authentication if the user actually authenticated. - // Load Zandria creations if the startup animation is not currently occurring. - if (introChoreographer.state == IntroChoreographer.State.DONE) { - LoadCreations(); - } else { - // Otherwise, tell the introChoreographer to load the creations when startup is finished. - introChoreographer.loadCreationsWhenDone = true; - } - - // Change the PolyMenu buttons. - polyMenuMain.SignIn(OAuth2Identity.Instance.Profile.icon, OAuth2Identity.Instance.Profile.name); - // They logged in, change the "Sign In" button to sign out. - GetDesktopMain().SignIn(OAuth2Identity.Instance.Profile.icon, OAuth2Identity.Instance.Profile.name); - - paletteController.publishSignInPrompt.SetActive(false); - } + private void SignInSuccessWithModelSave() + { + SignInSuccess(); + SaveCurrentModel(publish: false, saveSelected: false); + } - private void SignInSuccessWithModelSave() { - SignInSuccess(); - SaveCurrentModel(publish:false, saveSelected:false); - } + private void SignInFailure() + { + // Change the PolyMenu buttons. + polyMenuMain.SignOut(); + // Update the desktop menu. + desktopMain.SignOut(); + } - private void SignInFailure() { - // Change the PolyMenu buttons. - polyMenuMain.SignOut(); - // Update the desktop menu. - desktopMain.SignOut(); - } + public void SignOut() + { + zandriaCreationsManager.ClearLoad(PolyMenuMain.CreationType.YOUR); + zandriaCreationsManager.ClearLoad(PolyMenuMain.CreationType.LIKED); + AssetsServiceClient.mostRecentLikedAssetId = null; + + // Try to load any local models. + zandriaCreationsManager.LoadOfflineModels(); + + // Sign the user out. + OAuth2Identity.Instance.Logout(); + // Change the PolyMenu buttons. + polyMenuMain.SignOut(); + // Update the desktop menu. + desktopMain.SignOut(); + } - public void SignOut() { - zandriaCreationsManager.ClearLoad(PolyMenuMain.CreationType.YOUR); - zandriaCreationsManager.ClearLoad(PolyMenuMain.CreationType.LIKED); - AssetsServiceClient.mostRecentLikedAssetId = null; + /// + /// Initiates a save operation for the current model. This sets the model to read-only mode, + /// serializes the bytes on the background thread, makes the model writable then saves the + /// bytes in a Coroutine. + /// + /// If true, also opens the url to publish the content. + /// If true, only saves the current selected content rather than + /// the whole model. + public void SaveCurrentModel(bool publish, bool saveSelected) + { + // Don't save empty scenes (the button will already be disabled). + if (model.GetNumberOfMeshes() == 0) + { + return; + } - // Try to load any local models. - zandriaCreationsManager.LoadOfflineModels(); + // Keep track of model changes so we know whether or not to pop up the 'are you sure' dialog on 'new'. + ModelChangedSinceLastSave = false; + newModelSinceLastSaved = false; - // Sign the user out. - OAuth2Identity.Instance.Logout(); - // Change the PolyMenu buttons. - polyMenuMain.SignOut(); - // Update the desktop menu. - desktopMain.SignOut(); - } + if (!model.writeable) + { + // Make sure we don't try to lock down the model twice, since unlocking would be an issue. + Debug.Log("Already saving."); + return; + } - /// - /// Initiates a save operation for the current model. This sets the model to read-only mode, - /// serializes the bytes on the background thread, makes the model writable then saves the - /// bytes in a Coroutine. - /// - /// If true, also opens the url to publish the content. - /// If true, only saves the current selected content rather than - /// the whole model. - public void SaveCurrentModel(bool publish, bool saveSelected) { - // Don't save empty scenes (the button will already be disabled). - if (model.GetNumberOfMeshes() == 0) { - return; - } - - // Keep track of model changes so we know whether or not to pop up the 'are you sure' dialog on 'new'. - ModelChangedSinceLastSave = false; - newModelSinceLastSaved = false; - - if (!model.writeable) { - // Make sure we don't try to lock down the model twice, since unlocking would be an issue. - Debug.Log("Already saving."); - return; - } - - // Mark the model as read-only while we serialize everything on the background thread. This is - // a pretty low-rent way to handle things. But it also minimizes complexity. - restrictionManager.controllerEventsAllowed = false; - model.writeable = false; - - progressIndicator.StartOperation(SAVE_MESSAGE); - ICollection meshes = saveSelected ? model.GetMatchingMeshes(selector.selectedMeshes) : model.GetAllMeshes(); - // Take a screenshot at the end of the next frame. - StartCoroutine(PeltzerMain.Instance.autoThumbnailCamera.TakeScreenShot((byte[] pngBytes) => { - // TODO bug - Temporarily doing this in the foreground, as GLTF export can't run in the background. - // This should be moved back to the background thread as soon as GLTF export is fixed to not use Unity objects. - SerializeWork serWork = new SerializeWork(model, meshes, pngBytes, (SaveData saveData) => { - // NOTE: this callback only means data is now serialized. It hasn't been saved yet! - - // The model can now be written, since it's already serialized. From this point on in the save process, - // we will no longer look at the model, we will only look at the serialized data. So we don't care what - // happens to the model from now on. - restrictionManager.controllerEventsAllowed = true; - model.writeable = true; - saveData.remixIds = model.GetAllRemixIds(meshes); - // Now let's save the serialized data. This will be done asynchronously. - SaveSerializedData(saveData, publish, saveSelected); - }, serializerForManualSave, saveSelected); - - serWork.BackgroundWork(); - serWork.PostWork(); - })); - } + // Mark the model as read-only while we serialize everything on the background thread. This is + // a pretty low-rent way to handle things. But it also minimizes complexity. + restrictionManager.controllerEventsAllowed = false; + model.writeable = false; + + progressIndicator.StartOperation(SAVE_MESSAGE); + ICollection meshes = saveSelected ? model.GetMatchingMeshes(selector.selectedMeshes) : model.GetAllMeshes(); + // Take a screenshot at the end of the next frame. + StartCoroutine(PeltzerMain.Instance.autoThumbnailCamera.TakeScreenShot((byte[] pngBytes) => + { + // TODO bug - Temporarily doing this in the foreground, as GLTF export can't run in the background. + // This should be moved back to the background thread as soon as GLTF export is fixed to not use Unity objects. + SerializeWork serWork = new SerializeWork(model, meshes, pngBytes, (SaveData saveData) => + { + // NOTE: this callback only means data is now serialized. It hasn't been saved yet! + + // The model can now be written, since it's already serialized. From this point on in the save process, + // we will no longer look at the model, we will only look at the serialized data. So we don't care what + // happens to the model from now on. + restrictionManager.controllerEventsAllowed = true; + model.writeable = true; + saveData.remixIds = model.GetAllRemixIds(meshes); + // Now let's save the serialized data. This will be done asynchronously. + SaveSerializedData(saveData, publish, saveSelected); + }, serializerForManualSave, saveSelected); + + serWork.BackgroundWork(); + serWork.PostWork(); + })); + } - /// - /// Saves the current model, and lets the user keep working on it in a new branch. That is to say, any edits - /// after this point will apply to a new modelId and hitting 'save' after this operation will create a second, - /// distinct save file. - /// This does not remove 'remix' info. - /// - public void SaveCurrentModelAsCopy() { - SaveCurrentModel(publish:false, saveSelected:false); - LocalId = null; - AssetId = null; - ModelChangedSinceLastSave = true; - } + /// + /// Saves the current model, and lets the user keep working on it in a new branch. That is to say, any edits + /// after this point will apply to a new modelId and hitting 'save' after this operation will create a second, + /// distinct save file. + /// This does not remove 'remix' info. + /// + public void SaveCurrentModelAsCopy() + { + SaveCurrentModel(publish: false, saveSelected: false); + LocalId = null; + AssetId = null; + ModelChangedSinceLastSave = true; + } - /// - /// Saves the current selected models only as a asset. Any edits after this point will apply to a previous - /// modelId and hitting 'save' after this operation will overwrite the previous model, not the selected - /// content. - /// This does not remove 'remix' info. - /// - public void SaveCurrentSelectedModel() { - if (selector.selectedMeshes.Count > 0) { - SaveCurrentModel(publish:false, saveSelected:true); - } - } + /// + /// Saves the current selected models only as a asset. Any edits after this point will apply to a previous + /// modelId and hitting 'save' after this operation will overwrite the previous model, not the selected + /// content. + /// This does not remove 'remix' info. + /// + public void SaveCurrentSelectedModel() + { + if (selector.selectedMeshes.Count > 0) + { + SaveCurrentModel(publish: false, saveSelected: true); + } + } - /// - /// Called (on UI thread) when the model data has been serialized and is ready to save. - /// - public void SaveSerializedData(SaveData saveData, bool publish, bool saveSelected) { - // Generate an ID if needed. A new id will be needed if the LocalId is null or we are currently - // only saving the selected content, otherwise we are just overwriting existing save data and - // can use the existing LocalId. - bool isOverwrite = (LocalId != null && !saveSelected); - string modelIdForSaving = isOverwrite ? LocalId : ObjFileExporter.RandomOpaqueId(); - - // Save locally to a regular directory. - string directory = Path.Combine(modelsPath, modelIdForSaving); - DoPolyMenuBackgroundWork( - new SaveToDiskWork(saveData, directory, /* isOfflineModelsFolder */ false, isOverwrite)); - if (OAuth2Identity.Instance.LoggedIn) { - // If the user is authenticated, save to the assets service. - // This is asynchronous, and will ultimately call HandleSaveComplete to report the result of the - // save operation when it ends. - exporter.UploadToVrAssetsService(saveData, publish, saveSelected); - } else { - // Take a screenshot at the end of the next frame, then save to a special 'offline' directory locally - // so the user doesn't lose their work just because they weren't authenticated/online. - // TODO(bug): Ensure thumbnail only contains selected content when saveSelected is true. - StartCoroutine(autoThumbnailCamera.TakeScreenShot((byte[] pngBytes) => { - saveData.thumbnailBytes = pngBytes; - directory = Path.Combine(offlineModelsPath, modelIdForSaving); - - DoPolyMenuBackgroundWork(new SaveToDiskWork(saveData, directory, /* isOfflineModelsFolder */ true, - isOverwrite)); - // If we are only saving the selected content, we don't want to overwrite the LocalId - // as the current id for the model we saved is only for the temporary selected content. - if (!saveSelected) { - LocalId = modelIdForSaving; - } - })); - } - } + /// + /// Called (on UI thread) when the model data has been serialized and is ready to save. + /// + public void SaveSerializedData(SaveData saveData, bool publish, bool saveSelected) + { + // Generate an ID if needed. A new id will be needed if the LocalId is null or we are currently + // only saving the selected content, otherwise we are just overwriting existing save data and + // can use the existing LocalId. + bool isOverwrite = (LocalId != null && !saveSelected); + string modelIdForSaving = isOverwrite ? LocalId : ObjFileExporter.RandomOpaqueId(); + + // Save locally to a regular directory. + string directory = Path.Combine(modelsPath, modelIdForSaving); + DoPolyMenuBackgroundWork( + new SaveToDiskWork(saveData, directory, /* isOfflineModelsFolder */ false, isOverwrite)); + if (OAuth2Identity.Instance.LoggedIn) + { + // If the user is authenticated, save to the assets service. + // This is asynchronous, and will ultimately call HandleSaveComplete to report the result of the + // save operation when it ends. + exporter.UploadToVrAssetsService(saveData, publish, saveSelected); + } + else + { + // Take a screenshot at the end of the next frame, then save to a special 'offline' directory locally + // so the user doesn't lose their work just because they weren't authenticated/online. + // TODO(bug): Ensure thumbnail only contains selected content when saveSelected is true. + StartCoroutine(autoThumbnailCamera.TakeScreenShot((byte[] pngBytes) => + { + saveData.thumbnailBytes = pngBytes; + directory = Path.Combine(offlineModelsPath, modelIdForSaving); + + DoPolyMenuBackgroundWork(new SaveToDiskWork(saveData, directory, /* isOfflineModelsFolder */ true, + isOverwrite)); + // If we are only saving the selected content, we don't want to overwrite the LocalId + // as the current id for the model we saved is only for the temporary selected content. + if (!saveSelected) + { + LocalId = modelIdForSaving; + } + })); + } + } - /// - /// Signals that a saving operation has completed. - /// - /// True if the save operation finished successfully, or false if it - /// caught fire, crashed and burned. - public void HandleSaveComplete(bool success, string message) { - if (saveCompleteAction != null) { - if (success) { - saveCompleteAction(); - saveCompleteAction = null; - } - else { - //Right now we don't need a saveFailedHandler, but one can be inserted here at such a point as we do. - saveCompleteAction = null; - } - } - progressIndicator.FinishOperation(success, message); - peltzerController.TriggerHapticFeedback(); - } + /// + /// Signals that a saving operation has completed. + /// + /// True if the save operation finished successfully, or false if it + /// caught fire, crashed and burned. + public void HandleSaveComplete(bool success, string message) + { + if (saveCompleteAction != null) + { + if (success) + { + saveCompleteAction(); + saveCompleteAction = null; + } + else + { + //Right now we don't need a saveFailedHandler, but one can be inserted here at such a point as we do. + saveCompleteAction = null; + } + } + progressIndicator.FinishOperation(success, message); + peltzerController.TriggerHapticFeedback(); + } - /// - /// Creates a new model, cache and spatial index, resets the state of the app, - /// and optionally removes any reference images present. - /// - public void CreateNewModel(bool clearReferenceImages = true, bool resetAttentionCaller = true, - bool resetRestrictions = true) { - LocalId = null; - AssetId = null; - newModelSinceLastSaved = true; - ResetState(); - if (clearReferenceImages) { - foreach (MoveableReferenceImage refImg in GameObject.FindObjectsOfType()) { - DestroyImmediate(refImg.gameObject); - } - } - - worldSpace.SetToDefault(); - zoomer.ClearState(); - model.Clear(worldSpace); - volumeInserter.ClearState(); - spatialIndex.Reset(DEFAULT_BOUNDS); - if (resetAttentionCaller) { - attentionCaller.ResetAll(); - } - // By default, all operations are allowed. - if (resetRestrictions) { - restrictionManager.AllowAll(); - } - - // When creating a new model, there will be no meshes, so the save button should be inactive. - SetSaveButtonActiveIfModelNotEmpty(); - SetSaveSelectedButtonActiveIfSelectionNotEmpty(); - - // Open the save url once per unique model when saved. - HasOpenedSaveUrlThisSession = false; - } + /// + /// Creates a new model, cache and spatial index, resets the state of the app, + /// and optionally removes any reference images present. + /// + public void CreateNewModel(bool clearReferenceImages = true, bool resetAttentionCaller = true, + bool resetRestrictions = true) + { + LocalId = null; + AssetId = null; + newModelSinceLastSaved = true; + ResetState(); + if (clearReferenceImages) + { + foreach (MoveableReferenceImage refImg in GameObject.FindObjectsOfType()) + { + DestroyImmediate(refImg.gameObject); + } + } - /// - /// Resets the state of every tool to the state it was in at startup, and switches to the default tool. - /// This gives a 'clean' experience when loading, hitting 'new', or starting a tutorial. - /// - public void ResetState() { - mover.currentMoveType = Mover.MoveType.MOVE; - worldSpace.SetToDefault(); - selector.DeselectAll(); - peltzerController.shapesMenu.SetShapeMenuItem((int)Primitives.Shape.CUBE, /* showMenu */ false); - peltzerController.currentMaterial = PeltzerController.DEFAULT_MATERIAL; - peltzerController.ChangeToolColor(); - peltzerController.SetDefaultMode(); - if (peltzerController.isBlockMode) { - peltzerController.ToggleBlockMode(/* initiatedByUser */ false); - } - } + worldSpace.SetToDefault(); + zoomer.ClearState(); + model.Clear(worldSpace); + volumeInserter.ClearState(); + spatialIndex.Reset(DEFAULT_BOUNDS); + if (resetAttentionCaller) + { + attentionCaller.ResetAll(); + } + // By default, all operations are allowed. + if (resetRestrictions) + { + restrictionManager.AllowAll(); + } - private void SetupSpatialIndex() { - - // Do spatial indexing on the background thread. Some important notes: - // 1) We copy the mesh on the main thread, since we don't want the background thread to be reading from - // it while we are changing it. Deep copies are fairly fast, since most parts of an MMesh are immutable. - // 2) When removing from the index, we do it in two steps: on the main thread, we simply mark the mesh as - // condemned (pending removal), so that it will immediately stop being returned in queries. For the actual - // deletion (which is more expensive), we do it in the background thread. - // We also have to worry about the case where something is deleted and quickly added back (which can happen - // for Moves, for example). That's why we have to be careful that all MUTATION to the index happens - // in the same thread (background), to guarantee ordering. We can't have the main thread adding to the - // index and the background thread removing stuff from the index, for example. - model.OnMeshAdded += (MMesh mesh) => DoBackgroundWork(new AddToIndex(spatialIndex, mesh.Clone())); - model.OnMeshChanged += (MMesh mesh, bool materialsChanged, bool geometryChanged, bool facesOrVertsChanged) => { - if (geometryChanged) { - // Mark the mesh as condemned on the main thread so that it is no longer reported by the spatial - // index. The actual deletion will happen in the background thread, because it's an expensive - // operation. - spatialIndex.CondemnMesh(mesh.id); - DoBackgroundWork(new UpdateInIndex(spatialIndex, mesh.Clone())); - } - }; - model.OnMeshDeleted += (MMesh mesh) => { - // Mark the mesh as condemned on the main thread so that it is no longer reported by the spatial - // index. The actual deletion will happen in the background thread, because it's an expensive - // operation. - spatialIndex.CondemnMesh(mesh.id); - DoBackgroundWork(new DeleteFromIndex(spatialIndex, mesh.Clone())); - }; - } + // When creating a new model, there will be no meshes, so the save button should be inactive. + SetSaveButtonActiveIfModelNotEmpty(); + SetSaveSelectedButtonActiveIfSelectionNotEmpty(); - /// - /// Takes in an identifier for a cloud-saved creation and then calls the creationsManager to load the creation. - /// - public void LoadSavedModelOntoPolyMenu(string assetId, bool wasPublished) { - zandriaCreationsManager.GetAssetFromAssetsService(assetId, delegate (ObjectStoreEntry objectStoreResult) { - zandriaCreationsManager.StartSingleCreationLoad(PolyMenuMain.CreationType.YOUR, objectStoreResult, - /* isLocal */ false, /* isSave */ true); - }); - - if (!HasShownMenuTooltipThisSession) { - applicationButtonToolTips.TurnOn("ViewSaved"); - polyMenuMain.SwitchToYourModelsSection(); - HasShownMenuTooltipThisSession = true; - } - } + // Open the save url once per unique model when saved. + HasOpenedSaveUrlThisSession = false; + } - /// - /// Takes in the directory for a locally-saved creation and then calls the creationsManager to load the creation. - /// - public void LoadLocallySavedModelOntoPolyMenu(DirectoryInfo directory) { - ObjectStoreEntry objectStoreEntry; - if (zandriaCreationsManager.GetObjectStoreEntryFromLocalDirectory(directory, out objectStoreEntry)) { - zandriaCreationsManager.StartSingleCreationLoad(PolyMenuMain.CreationType.YOUR, objectStoreEntry, - /* isLocal */ true, /* isSave */ true); - } - - if (!HasShownMenuTooltipThisSession) { - applicationButtonToolTips.TurnOn("ViewSaved"); - polyMenuMain.SwitchToYourModelsSection(); - HasShownMenuTooltipThisSession = true; - } - } + /// + /// Resets the state of every tool to the state it was in at startup, and switches to the default tool. + /// This gives a 'clean' experience when loading, hitting 'new', or starting a tutorial. + /// + public void ResetState() + { + mover.currentMoveType = Mover.MoveType.MOVE; + worldSpace.SetToDefault(); + selector.DeselectAll(); + peltzerController.shapesMenu.SetShapeMenuItem((int)Primitives.Shape.CUBE, /* showMenu */ false); + peltzerController.currentMaterial = PeltzerController.DEFAULT_MATERIAL; + peltzerController.ChangeToolColor(); + peltzerController.SetDefaultMode(); + if (peltzerController.isBlockMode) + { + peltzerController.ToggleBlockMode(/* initiatedByUser */ false); + } + } - /// - /// Takes in an updated model for a cloud-saved creation and then calls the creationsManager to update it. - /// - public void UpdateCloudModelOntoPolyMenu(string asset) { - zandriaCreationsManager.UpdateSingleCloudCreationOnYourModels(asset); - } + private void SetupSpatialIndex() + { + + // Do spatial indexing on the background thread. Some important notes: + // 1) We copy the mesh on the main thread, since we don't want the background thread to be reading from + // it while we are changing it. Deep copies are fairly fast, since most parts of an MMesh are immutable. + // 2) When removing from the index, we do it in two steps: on the main thread, we simply mark the mesh as + // condemned (pending removal), so that it will immediately stop being returned in queries. For the actual + // deletion (which is more expensive), we do it in the background thread. + // We also have to worry about the case where something is deleted and quickly added back (which can happen + // for Moves, for example). That's why we have to be careful that all MUTATION to the index happens + // in the same thread (background), to guarantee ordering. We can't have the main thread adding to the + // index and the background thread removing stuff from the index, for example. + model.OnMeshAdded += (MMesh mesh) => DoBackgroundWork(new AddToIndex(spatialIndex, mesh.Clone())); + model.OnMeshChanged += (MMesh mesh, bool materialsChanged, bool geometryChanged, bool facesOrVertsChanged) => + { + if (geometryChanged) + { + // Mark the mesh as condemned on the main thread so that it is no longer reported by the spatial + // index. The actual deletion will happen in the background thread, because it's an expensive + // operation. + spatialIndex.CondemnMesh(mesh.id); + DoBackgroundWork(new UpdateInIndex(spatialIndex, mesh.Clone())); + } + }; + model.OnMeshDeleted += (MMesh mesh) => + { + // Mark the mesh as condemned on the main thread so that it is no longer reported by the spatial + // index. The actual deletion will happen in the background thread, because it's an expensive + // operation. + spatialIndex.CondemnMesh(mesh.id); + DoBackgroundWork(new DeleteFromIndex(spatialIndex, mesh.Clone())); + }; + } - /// - /// Takes in an updated model for a locally-saved creation and then calls the creationsManager to update it. - /// - public void UpdateLocalModelOntoPolyMenu(DirectoryInfo directoryInfo) { - zandriaCreationsManager.UpdateSingleLocalCreationOnYourModels(directoryInfo); - } + /// + /// Takes in an identifier for a cloud-saved creation and then calls the creationsManager to load the creation. + /// + public void LoadSavedModelOntoPolyMenu(string assetId, bool wasPublished) + { + zandriaCreationsManager.GetAssetFromAssetsService(assetId, delegate (ObjectStoreEntry objectStoreResult) + { + zandriaCreationsManager.StartSingleCreationLoad(PolyMenuMain.CreationType.YOUR, objectStoreResult, + /* isLocal */ false, /* isSave */ true); + }); + + if (!HasShownMenuTooltipThisSession) + { + applicationButtonToolTips.TurnOn("ViewSaved"); + polyMenuMain.SwitchToYourModelsSection(); + HasShownMenuTooltipThisSession = true; + } + } - /// - /// Loads a given peltzer file into the model, optionally with replaying. - /// - /// Options controlling how to load the file. If null, - /// uses defaults. - public void LoadPeltzerFileIntoModel(PeltzerFile file, LoadOptions loadOptions = null) { - loadOptions = loadOptions ?? LoadOptions.DEFAULTS; + /// + /// Takes in the directory for a locally-saved creation and then calls the creationsManager to load the creation. + /// + public void LoadLocallySavedModelOntoPolyMenu(DirectoryInfo directory) + { + ObjectStoreEntry objectStoreEntry; + if (zandriaCreationsManager.GetObjectStoreEntryFromLocalDirectory(directory, out objectStoreEntry)) + { + zandriaCreationsManager.StartSingleCreationLoad(PolyMenuMain.CreationType.YOUR, objectStoreEntry, + /* isLocal */ true, /* isSave */ true); + } - foreach (MMesh originalMesh in file.meshes) { - MMesh mesh = loadOptions.cloneBeforeLoad ? originalMesh.Clone() : originalMesh; + if (!HasShownMenuTooltipThisSession) + { + applicationButtonToolTips.TurnOn("ViewSaved"); + polyMenuMain.SwitchToYourModelsSection(); + HasShownMenuTooltipThisSession = true; + } + } - // Override the remix ID, if requested. - if (loadOptions.overrideRemixId != null) { - mesh.remixIds = new HashSet(); - mesh.remixIds.Add(loadOptions.overrideRemixId); + /// + /// Takes in an updated model for a cloud-saved creation and then calls the creationsManager to update it. + /// + public void UpdateCloudModelOntoPolyMenu(string asset) + { + zandriaCreationsManager.UpdateSingleCloudCreationOnYourModels(asset); } - // Give the mesh a new ID if necessary (if it conflicts with a mesh that's already in the - // model). - if (model.HasMesh(mesh.id)) { - mesh.ChangeId(model.GenerateMeshId()); + /// + /// Takes in an updated model for a locally-saved creation and then calls the creationsManager to update it. + /// + public void UpdateLocalModelOntoPolyMenu(DirectoryInfo directoryInfo) + { + zandriaCreationsManager.UpdateSingleLocalCreationOnYourModels(directoryInfo); } - AssertOrThrow.True(model.AddMesh(mesh), "Attempted to load an invalid mesh"); - } - } - /// - /// Loads a PeltzerFile from the project's resources. - /// - /// The resource path. Note that due to a weird Unity thing, the file - /// should be saved with the .bytes extension in the Assets/Resources/ folder, but resourcePath should - /// NOT contain the .bytes extension. So if your file is in Assets/Resources/Foo/bar.bytes, - /// then resourcePath should be "Foo/bar". - public void LoadPeltzerFileFromResources(string resourcePath, bool resetAttentionCaller = true, - bool resetRestrictions = true, bool clearReferenceImages = true) { - TextAsset file = Resources.Load(resourcePath); - AssertOrThrow.NotNull(file, "Failed to load PeltzerFile from resource: " + resourcePath); - PeltzerFile peltzerFile; - if (!PeltzerFileHandler.PeltzerFileFromBytes(file.bytes, out peltzerFile)) { - throw new Exception("Failed to parse PeltzerFile from resource: " + resourcePath); - } - CreateNewModel(clearReferenceImages, resetAttentionCaller, resetRestrictions); - LoadPeltzerFileIntoModel(peltzerFile); - } + /// + /// Loads a given peltzer file into the model, optionally with replaying. + /// + /// Options controlling how to load the file. If null, + /// uses defaults. + public void LoadPeltzerFileIntoModel(PeltzerFile file, LoadOptions loadOptions = null) + { + loadOptions = loadOptions ?? LoadOptions.DEFAULTS; + + foreach (MMesh originalMesh in file.meshes) + { + MMesh mesh = loadOptions.cloneBeforeLoad ? originalMesh.Clone() : originalMesh; + + // Override the remix ID, if requested. + if (loadOptions.overrideRemixId != null) + { + mesh.remixIds = new HashSet(); + mesh.remixIds.Add(loadOptions.overrideRemixId); + } + + // Give the mesh a new ID if necessary (if it conflicts with a mesh that's already in the + // model). + if (model.HasMesh(mesh.id)) + { + mesh.ChangeId(model.GenerateMeshId()); + } + AssertOrThrow.True(model.AddMesh(mesh), "Attempted to load an invalid mesh"); + } + } - public void RecordGif() { - gifRecorder.RecordGif(); - } + /// + /// Loads a PeltzerFile from the project's resources. + /// + /// The resource path. Note that due to a weird Unity thing, the file + /// should be saved with the .bytes extension in the Assets/Resources/ folder, but resourcePath should + /// NOT contain the .bytes extension. So if your file is in Assets/Resources/Foo/bar.bytes, + /// then resourcePath should be "Foo/bar". + public void LoadPeltzerFileFromResources(string resourcePath, bool resetAttentionCaller = true, + bool resetRestrictions = true, bool clearReferenceImages = true) + { + TextAsset file = Resources.Load(resourcePath); + AssertOrThrow.NotNull(file, "Failed to load PeltzerFile from resource: " + resourcePath); + PeltzerFile peltzerFile; + if (!PeltzerFileHandler.PeltzerFileFromBytes(file.bytes, out peltzerFile)) + { + throw new Exception("Failed to parse PeltzerFile from resource: " + resourcePath); + } + CreateNewModel(clearReferenceImages, resetAttentionCaller, resetRestrictions); + LoadPeltzerFileIntoModel(peltzerFile); + } - /// - /// Call before exit. Shuts down any background threads. Finalizes any saved data. - /// - public void Shutdown() { - running = false; - } + public void RecordGif() + { + gifRecorder.RecordGif(); + } - /// - /// Whether an operation is in progress. If so, we'll deny Undo/Redo operations. - /// - /// - public bool OperationInProgress() { - switch (peltzerController.mode) { - case ControllerMode.delete: - return deleter.isDeleting; - case ControllerMode.extrude: - return extruder.IsExtrudingFace(); - case ControllerMode.insertStroke: - return freeform.IsStroking(); - case ControllerMode.insertVolume: - return volumeInserter.IsFilling(); - case ControllerMode.move: - return mover.IsMoving(); - case ControllerMode.paintFace: - case ControllerMode.paintMesh: - return painter.IsPainting(); - case ControllerMode.reshape: - return reshaper.IsReshaping(); - case ControllerMode.subdivideFace: - return false; - case ControllerMode.subtract: - return volumeInserter.IsFilling(); - } - - return false; - } + /// + /// Call before exit. Shuts down any background threads. Finalizes any saved data. + /// + public void Shutdown() + { + running = false; + } - /// - /// Get the Peltzer model. - /// - /// The model. - public Model GetModel() { - return model; - } + /// + /// Whether an operation is in progress. If so, we'll deny Undo/Redo operations. + /// + /// + public bool OperationInProgress() + { + switch (peltzerController.mode) + { + case ControllerMode.delete: + return deleter.isDeleting; + case ControllerMode.extrude: + return extruder.IsExtrudingFace(); + case ControllerMode.insertStroke: + return freeform.IsStroking(); + case ControllerMode.insertVolume: + return volumeInserter.IsFilling(); + case ControllerMode.move: + return mover.IsMoving(); + case ControllerMode.paintFace: + case ControllerMode.paintMesh: + return painter.IsPainting(); + case ControllerMode.reshape: + return reshaper.IsReshaping(); + case ControllerMode.subdivideFace: + return false; + case ControllerMode.subtract: + return volumeInserter.IsFilling(); + } - public Extruder GetExtruder() { - return extruder; - } + return false; + } - public Exporter GetExporter() { - return exporter; - } + /// + /// Get the Peltzer model. + /// + /// The model. + public Model GetModel() + { + return model; + } - public Mover GetMover() { - return mover; - } + public Extruder GetExtruder() + { + return extruder; + } - public Deleter GetDeleter() { - return deleter; - } + public Exporter GetExporter() + { + return exporter; + } - public Reshaper GetReshaper() { - return reshaper; - } + public Mover GetMover() + { + return mover; + } - public Selector GetSelector() { - return selector; - } + public Deleter GetDeleter() + { + return deleter; + } - public Freeform GetFreeform() { - return freeform; - } + public Reshaper GetReshaper() + { + return reshaper; + } - public VolumeInserter GetVolumeInserter() { - return volumeInserter; - } + public Selector GetSelector() + { + return selector; + } - public Subdivider GetSubdivider() { - return subdivider; - } + public Freeform GetFreeform() + { + return freeform; + } - public DesktopMain GetDesktopMain() { - return desktopMain; - } + public VolumeInserter GetVolumeInserter() + { + return volumeInserter; + } - public PolyMenuMain GetPolyMenuMain() { - return polyMenuMain; - } + public Subdivider GetSubdivider() + { + return subdivider; + } - public PreviewController GetPreviewController() { - return previewController; - } + public DesktopMain GetDesktopMain() + { + return desktopMain; + } - public FloatingMessage GetFloatingMessage() { - return floatingMessage; - } + public PolyMenuMain GetPolyMenuMain() + { + return polyMenuMain; + } - public Painter GetPainter() { - return painter; - } + public PreviewController GetPreviewController() + { + return previewController; + } - public SpatialIndex GetSpatialIndex() { - return spatialIndex; - } + public FloatingMessage GetFloatingMessage() + { + return floatingMessage; + } + + public Painter GetPainter() + { + return painter; + } - public Zoomer Zoomer { get { return zoomer; } } + public SpatialIndex GetSpatialIndex() + { + return spatialIndex; + } - /// - /// Enqueue work that should be done on the general background thread. - /// This thread is expected to be used only for operations affecting the model. - /// - /// The work - public void DoBackgroundWork(BackgroundWork work) { - generalBackgroundQueue.Enqueue(work); - } + public Zoomer Zoomer { get { return zoomer; } } - /// - /// Enqueue work that should be done on the Poly Menu background thread. - /// This thread is expected to be used for anything around saving or loading objects. - /// - /// The work - public void DoPolyMenuBackgroundWork(BackgroundWork work) { - polyMenuBackgroundQueue.Enqueue(work); - } + /// + /// Enqueue work that should be done on the general background thread. + /// This thread is expected to be used only for operations affecting the model. + /// + /// The work + public void DoBackgroundWork(BackgroundWork work) + { + generalBackgroundQueue.Enqueue(work); + } - /// - /// Enqueue work that should be done on the File Picker background thread. - /// This thread is expected to be used for the operations where a user picks a file. - /// - /// The work - public void DoFilePickerBackgroundWork(BackgroundWork work) { - filePickerBackgroundQueue.Enqueue(work); - } + /// + /// Enqueue work that should be done on the Poly Menu background thread. + /// This thread is expected to be used for anything around saving or loading objects. + /// + /// The work + public void DoPolyMenuBackgroundWork(BackgroundWork work) + { + polyMenuBackgroundQueue.Enqueue(work); + } - /// - /// Main function for general background thread. - /// - private void ProcessGeneralBackgroundWork() { - while (running) { - BackgroundWork work; - if (generalBackgroundQueue.WaitAndDequeue(/* wait time ms */ 1000, out work)) { - try { - work.BackgroundWork(); - forMainThread.Enqueue(work); - } catch (Exception e) { - // Should probably be a fatal error. For now, just log something. - Debug.LogError("Exception handling background work: " + e); - } - } - } - } + /// + /// Enqueue work that should be done on the File Picker background thread. + /// This thread is expected to be used for the operations where a user picks a file. + /// + /// The work + public void DoFilePickerBackgroundWork(BackgroundWork work) + { + filePickerBackgroundQueue.Enqueue(work); + } - /// - /// Main function for Poly Menu background thread. - /// - private void ProcessPolyMenuBackgroundWork() { - while (running) { - BackgroundWork work; - if (polyMenuBackgroundQueue.WaitAndDequeue(/* wait time ms */ 1000, out work)) { - try { - work.BackgroundWork(); - forMainThread.Enqueue(work); - } catch (Exception e) { - // Should probably be a fatal error. For now, just log something. - Debug.LogError("Exception handling background work: " + e); - } - } - } - } + /// + /// Main function for general background thread. + /// + private void ProcessGeneralBackgroundWork() + { + while (running) + { + BackgroundWork work; + if (generalBackgroundQueue.WaitAndDequeue(/* wait time ms */ 1000, out work)) + { + try + { + work.BackgroundWork(); + forMainThread.Enqueue(work); + } + catch (Exception e) + { + // Should probably be a fatal error. For now, just log something. + Debug.LogError("Exception handling background work: " + e); + } + } + } + } - /// - /// Main function for File Picker background thread. - /// - private void ProcessFilePickerBackgroundWork() { - while (running) { - BackgroundWork work; - if (filePickerBackgroundQueue.WaitAndDequeue(/* wait time ms */ 1000, out work)) { - try { - work.BackgroundWork(); - forMainThread.Enqueue(work); - } catch (Exception e) { - // Should probably be a fatal error. For now, just log something. - Debug.LogError("Exception handling background work: " + e); - } - } - } - } + /// + /// Main function for Poly Menu background thread. + /// + private void ProcessPolyMenuBackgroundWork() + { + while (running) + { + BackgroundWork work; + if (polyMenuBackgroundQueue.WaitAndDequeue(/* wait time ms */ 1000, out work)) + { + try + { + work.BackgroundWork(); + forMainThread.Enqueue(work); + } + catch (Exception e) + { + // Should probably be a fatal error. For now, just log something. + Debug.LogError("Exception handling background work: " + e); + } + } + } + } - /// - /// Detects when the handedness changes of the controllers and accomodates necessary changes. - /// For the PaletteController, this means placing the menu on the opposite side. - /// - public void ResolveControllerHandedness() { - // We only need to check for the Vive, the Rift's handedness is known, and is modified only by controller bumps. - if (Config.Instance.VrHardware == VrHardware.Vive) { - Vector3 vectorBetweenControllers = peltzerController.transform.position - paletteController.transform.position; - if (vectorBetweenControllers.magnitude < HANDEDNESS_DISTANCE_THRESHOLD) return; - // If the sign is positive, the user is right-handed (holding the peltzer controller in their right hand). - float dotProduct = Vector3.Dot(vectorBetweenControllers, hmd.transform.right); - peltzerControllerInRightHand = dotProduct > 0; - } - - if (peltzerController.handedness == Handedness.LEFT && peltzerControllerInRightHand) { - peltzerController.handedness = Handedness.RIGHT; - peltzerController.ControllerHandednessChanged(); - paletteController.handedness = Handedness.LEFT; - paletteController.ControllerHandednessChanged(); - } else if (peltzerController.handedness == Handedness.RIGHT && !peltzerControllerInRightHand) { - peltzerController.handedness = Handedness.LEFT; - peltzerController.ControllerHandednessChanged(); - paletteController.handedness = Handedness.RIGHT; - paletteController.ControllerHandednessChanged(); - } - - // Now the switch is complete it is okay to re-enable tooltips. - peltzerController.ShowTooltips(); - } + /// + /// Main function for File Picker background thread. + /// + private void ProcessFilePickerBackgroundWork() + { + while (running) + { + BackgroundWork work; + if (filePickerBackgroundQueue.WaitAndDequeue(/* wait time ms */ 1000, out work)) + { + try + { + work.BackgroundWork(); + forMainThread.Enqueue(work); + } + catch (Exception e) + { + // Should probably be a fatal error. For now, just log something. + Debug.LogError("Exception handling background work: " + e); + } + } + } + } - /// - /// Options controlling how to load files. - /// - public class LoadOptions { - public static readonly LoadOptions DEFAULTS = new LoadOptions(); - - /// - /// If true, clone the meshes from the PeltzerFile instead of using them directly. - /// Use this if you want to keep the PeltzerFile for other purposes. - /// - public bool cloneBeforeLoad = false; - - /// - /// If not null, all remix IDs of all loaded meshes will be overridden with this value. - /// Use this if you want all meshes to have the same remix ID (for example, after loading - /// a model from Zandria that belongs to someone else, to give appropriate credit). - /// - public string overrideRemixId = null; + /// + /// Detects when the handedness changes of the controllers and accomodates necessary changes. + /// For the PaletteController, this means placing the menu on the opposite side. + /// + public void ResolveControllerHandedness() + { + // We only need to check for the Vive, the Rift's handedness is known, and is modified only by controller bumps. + if (Config.Instance.VrHardware == VrHardware.Vive) + { + Vector3 vectorBetweenControllers = peltzerController.transform.position - paletteController.transform.position; + if (vectorBetweenControllers.magnitude < HANDEDNESS_DISTANCE_THRESHOLD) return; + // If the sign is positive, the user is right-handed (holding the peltzer controller in their right hand). + float dotProduct = Vector3.Dot(vectorBetweenControllers, hmd.transform.right); + peltzerControllerInRightHand = dotProduct > 0; + } + + if (peltzerController.handedness == Handedness.LEFT && peltzerControllerInRightHand) + { + peltzerController.handedness = Handedness.RIGHT; + peltzerController.ControllerHandednessChanged(); + paletteController.handedness = Handedness.LEFT; + paletteController.ControllerHandednessChanged(); + } + else if (peltzerController.handedness == Handedness.RIGHT && !peltzerControllerInRightHand) + { + peltzerController.handedness = Handedness.LEFT; + peltzerController.ControllerHandednessChanged(); + paletteController.handedness = Handedness.RIGHT; + paletteController.ControllerHandednessChanged(); + } + + // Now the switch is complete it is okay to re-enable tooltips. + peltzerController.ShowTooltips(); + } + + /// + /// Options controlling how to load files. + /// + public class LoadOptions + { + public static readonly LoadOptions DEFAULTS = new LoadOptions(); + + /// + /// If true, clone the meshes from the PeltzerFile instead of using them directly. + /// Use this if you want to keep the PeltzerFile for other purposes. + /// + public bool cloneBeforeLoad = false; + + /// + /// If not null, all remix IDs of all loaded meshes will be overridden with this value. + /// Use this if you want all meshes to have the same remix ID (for example, after loading + /// a model from Zandria that belongs to someone else, to give appropriate credit). + /// + public string overrideRemixId = null; + } } - } } diff --git a/Assets/Scripts/model/main/RestrictionManager.cs b/Assets/Scripts/model/main/RestrictionManager.cs index 91120d48..e17c271e 100644 --- a/Assets/Scripts/model/main/RestrictionManager.cs +++ b/Assets/Scripts/model/main/RestrictionManager.cs @@ -18,305 +18,331 @@ using System.ComponentModel; using UnityEngine; -namespace com.google.apps.peltzer.client.model.main { - /// - /// Manages the restrictions to use the app. - /// Restrictions are set when the app is running in a special mode, such as the tutorial mode. - /// - public class RestrictionManager { - /// - /// Indicates whether or not volume insertion is allowed. - /// - public bool volumeInsertionAllowed { get; set; } - - /// - /// Indicates whether or not filling during volume insertion is allowed. - /// - public bool volumeFillingAllowed { get; set; } - - /// - /// Indicates whether or not the user is allowed use the shapes menu (for primitive selection) in - /// the volume inserter. - /// - public bool shapesMenuAllowed { get; set; } - - /// - /// Indicates whether or not the user is allowed to scale the primitive up/down in the volume inserter. - /// - public bool scaleOnVolumeInsertionAllowed { get; set; } - - /// - /// Indicates whether the user is allowed to use the palette. - /// - public bool paletteAllowed { get; set; } - - /// - /// Indicates whether the menu actions (grid mode, save, new, etc) are allowed. - /// - public bool menuActionsAllowed { get; set; } - - /// - /// Indicates whether the menu actions related to tutorial are allowed. - /// - public bool tutorialMenuActionsAllowed { get; set; } - - /// - /// Indicates whether switching between the Poly menu and tools menu is allowed. - /// - public bool menuSwitchAllowed { get; set; } - - /// - /// Indicates whether undo/redo are allowed. - /// - public bool undoRedoAllowed { get; set; } - - /// - /// Indicates whether or not manipulating the model-world transform is allowed (zooming/panning/rotating). - /// - public bool changeWorldTransformAllowed { get; set; } - - /// - /// Indicates whether tooltips are supposed to appear or not. - /// - private bool _tooltipsAllowed; - public bool tooltipsAllowed { - get { return _tooltipsAllowed; } - set { - _tooltipsAllowed = value; - if (!_tooltipsAllowed) { - PeltzerMain.Instance.paletteController.HideTooltips(); - PeltzerMain.Instance.peltzerController.HideTooltips(); +namespace com.google.apps.peltzer.client.model.main +{ + /// + /// Manages the restrictions to use the app. + /// Restrictions are set when the app is running in a special mode, such as the tutorial mode. + /// + public class RestrictionManager + { + /// + /// Indicates whether or not volume insertion is allowed. + /// + public bool volumeInsertionAllowed { get; set; } + + /// + /// Indicates whether or not filling during volume insertion is allowed. + /// + public bool volumeFillingAllowed { get; set; } + + /// + /// Indicates whether or not the user is allowed use the shapes menu (for primitive selection) in + /// the volume inserter. + /// + public bool shapesMenuAllowed { get; set; } + + /// + /// Indicates whether or not the user is allowed to scale the primitive up/down in the volume inserter. + /// + public bool scaleOnVolumeInsertionAllowed { get; set; } + + /// + /// Indicates whether the user is allowed to use the palette. + /// + public bool paletteAllowed { get; set; } + + /// + /// Indicates whether the menu actions (grid mode, save, new, etc) are allowed. + /// + public bool menuActionsAllowed { get; set; } + + /// + /// Indicates whether the menu actions related to tutorial are allowed. + /// + public bool tutorialMenuActionsAllowed { get; set; } + + /// + /// Indicates whether switching between the Poly menu and tools menu is allowed. + /// + public bool menuSwitchAllowed { get; set; } + + /// + /// Indicates whether undo/redo are allowed. + /// + public bool undoRedoAllowed { get; set; } + + /// + /// Indicates whether or not manipulating the model-world transform is allowed (zooming/panning/rotating). + /// + public bool changeWorldTransformAllowed { get; set; } + + /// + /// Indicates whether tooltips are supposed to appear or not. + /// + private bool _tooltipsAllowed; + public bool tooltipsAllowed + { + get { return _tooltipsAllowed; } + set + { + _tooltipsAllowed = value; + if (!_tooltipsAllowed) + { + PeltzerMain.Instance.paletteController.HideTooltips(); + PeltzerMain.Instance.peltzerController.HideTooltips(); + } + } } - } - } - - /// - /// Indicates whether reference images should be shown. - /// - public bool insertingReferenceImagesAllowed { get; set; } - - /// - /// Indicates whether changing colours via the colour palette is allowed. - /// - public bool changingColorsAllowed { get; set; } - - /// - /// Indicates whether throwing objects away to delete them is allowed. - /// - public bool throwAwayAllowed { get; set; } - - /// - /// Whether the touchpad can be modified to highligh and glow. When this is true we need to hide the standard - /// touchpad object and replace it with a quad segmented one so we can isolate the highlight. - /// - public bool touchpadHighlightingAllowed { get; set; } - - /// - /// Whether toolheads should change color when a new color is selected. - /// - public bool toolheadColorChangeAllowed { get; set; } - - /// - /// Whether the user is allowed to move meshes. - /// - public bool movingMeshesAllowed { get; set; } - - /// - /// Whether the world bounding box should be shown. - /// - public bool showingWorldBoundingBoxAllowed { get; set; } - - /// - /// Whether the user is allowed to snap. - /// - public bool snappingAllowed { get; set; } - - /// - /// Whether the user is allowed to copy. - /// - public bool copyingAllowed { get; set; } - - /// - /// Whether the user is given a bigger selection radius. - /// - public bool increasedMultiSelectRadiusAllowed { get; set; } - - /// - /// Whether the user is allowed to deselect. - /// - public bool deselectAllowed { get; set; } - - public bool touchpadUpAllowed { get; set; } - public bool touchpadDownAllowed { get; set; } - public bool touchpadRightAllowed { get; set; } - public bool touchpadLeftAllowed { get; set; } - /// - /// Whether controllerMain will send out controller events. - /// - public bool controllerEventsAllowed { get; set; } - - - /// - /// Indicates whether the user is allowed to use each controller mode or not. - /// Using an array instead of a dictionary for faster lookups. - /// - private bool[] controllerModeAllowed = new bool[Enum.GetValues(typeof(ControllerMode)).Length]; - /// - /// In the tutorial, we only wish one specific mesh ID to be selectable. - /// - public int? onlySelectableMeshIdForTutorial; - - /// - /// In the tutorial, we only allow the user to select one color. - /// - private int onlyAllowedMaterialId; - - /// - /// Creates a new RestrictionManager. By default no restrictions are in place. The restriction manager sets bool - /// restrictions that are enforced throughout all of the scripts and tools in the project. - /// - public RestrictionManager() { - // Everything is allowed by default. - AllowAll(); - } + /// + /// Indicates whether reference images should be shown. + /// + public bool insertingReferenceImagesAllowed { get; set; } + + /// + /// Indicates whether changing colours via the colour palette is allowed. + /// + public bool changingColorsAllowed { get; set; } + + /// + /// Indicates whether throwing objects away to delete them is allowed. + /// + public bool throwAwayAllowed { get; set; } + + /// + /// Whether the touchpad can be modified to highligh and glow. When this is true we need to hide the standard + /// touchpad object and replace it with a quad segmented one so we can isolate the highlight. + /// + public bool touchpadHighlightingAllowed { get; set; } + + /// + /// Whether toolheads should change color when a new color is selected. + /// + public bool toolheadColorChangeAllowed { get; set; } + + /// + /// Whether the user is allowed to move meshes. + /// + public bool movingMeshesAllowed { get; set; } + + /// + /// Whether the world bounding box should be shown. + /// + public bool showingWorldBoundingBoxAllowed { get; set; } + + /// + /// Whether the user is allowed to snap. + /// + public bool snappingAllowed { get; set; } + + /// + /// Whether the user is allowed to copy. + /// + public bool copyingAllowed { get; set; } + + /// + /// Whether the user is given a bigger selection radius. + /// + public bool increasedMultiSelectRadiusAllowed { get; set; } + + /// + /// Whether the user is allowed to deselect. + /// + public bool deselectAllowed { get; set; } + + public bool touchpadUpAllowed { get; set; } + public bool touchpadDownAllowed { get; set; } + public bool touchpadRightAllowed { get; set; } + public bool touchpadLeftAllowed { get; set; } + /// + /// Whether controllerMain will send out controller events. + /// + public bool controllerEventsAllowed { get; set; } + + + /// + /// Indicates whether the user is allowed to use each controller mode or not. + /// Using an array instead of a dictionary for faster lookups. + /// + private bool[] controllerModeAllowed = new bool[Enum.GetValues(typeof(ControllerMode)).Length]; + + /// + /// In the tutorial, we only wish one specific mesh ID to be selectable. + /// + public int? onlySelectableMeshIdForTutorial; + + /// + /// In the tutorial, we only allow the user to select one color. + /// + private int onlyAllowedMaterialId; + + /// + /// Creates a new RestrictionManager. By default no restrictions are in place. The restriction manager sets bool + /// restrictions that are enforced throughout all of the scripts and tools in the project. + /// + public RestrictionManager() + { + // Everything is allowed by default. + AllowAll(); + } - /// - /// Returns whether or not the given controller mode is currently allowed. - /// - /// The controller mode - /// True if allowed (user can use it), false if not allowed (use can't use it). - public bool IsControllerModeAllowed(ControllerMode mode) { - return controllerModeAllowed[(int)mode]; - } + /// + /// Returns whether or not the given controller mode is currently allowed. + /// + /// The controller mode + /// True if allowed (user can use it), false if not allowed (use can't use it). + public bool IsControllerModeAllowed(ControllerMode mode) + { + return controllerModeAllowed[(int)mode]; + } - /// - /// Returns whether a materialId is allowed to be selected. - /// - /// - /// - public bool IsColorAllowed(int materialId) { - // -2 means no colors allowed at all. - if (onlyAllowedMaterialId == -2) { - return false; - } - - // Return true if the material is the allowed material or if -1 which means all materials are allowed. - return (materialId == onlyAllowedMaterialId || onlyAllowedMaterialId == -1); - } + /// + /// Returns whether a materialId is allowed to be selected. + /// + /// + /// + public bool IsColorAllowed(int materialId) + { + // -2 means no colors allowed at all. + if (onlyAllowedMaterialId == -2) + { + return false; + } + + // Return true if the material is the allowed material or if -1 which means all materials are allowed. + return (materialId == onlyAllowedMaterialId || onlyAllowedMaterialId == -1); + } - public void SetOnlyAllowedColor(int materialId) { - if (onlyAllowedMaterialId == materialId) return; - onlyAllowedMaterialId = materialId; - } + public void SetOnlyAllowedColor(int materialId) + { + if (onlyAllowedMaterialId == materialId) return; + onlyAllowedMaterialId = materialId; + } - /// - /// Sets the allowed controller modes. All other modes will be disallowed. - /// - /// The allowed modes. Can be null to mean no modes are allowed. - public void SetAllowedControllerModes(IEnumerable modes) { - for (int i = 0; i < controllerModeAllowed.Length; i++) { - controllerModeAllowed[i] = false; - } - if (modes == null) { - return; - } - foreach (ControllerMode mode in modes) { - controllerModeAllowed[(int)mode] = true; - } - } + /// + /// Sets the allowed controller modes. All other modes will be disallowed. + /// + /// The allowed modes. Can be null to mean no modes are allowed. + public void SetAllowedControllerModes(IEnumerable modes) + { + for (int i = 0; i < controllerModeAllowed.Length; i++) + { + controllerModeAllowed[i] = false; + } + if (modes == null) + { + return; + } + foreach (ControllerMode mode in modes) + { + controllerModeAllowed[(int)mode] = true; + } + } - /// - /// (Syntactic sugar) Allows only one controller mode. - /// - /// The only mode to allow. - public void SetOnlyAllowedControllerMode(ControllerMode mode) { - for (int i = 0; i < controllerModeAllowed.Length; i++) { - controllerModeAllowed[i] = ((int)mode == i); - } - } + /// + /// (Syntactic sugar) Allows only one controller mode. + /// + /// The only mode to allow. + public void SetOnlyAllowedControllerMode(ControllerMode mode) + { + for (int i = 0; i < controllerModeAllowed.Length; i++) + { + controllerModeAllowed[i] = ((int)mode == i); + } + } - public void SetTouchpadHighlightingAllowed(bool isAllowed) { - // The segmented touchpad may not be available for the Rift, which uses a thumbstick. - if (PeltzerMain.Instance.peltzerController.controllerGeometry.segmentedTouchpad == null) return; + public void SetTouchpadHighlightingAllowed(bool isAllowed) + { + // The segmented touchpad may not be available for the Rift, which uses a thumbstick. + if (PeltzerMain.Instance.peltzerController.controllerGeometry.segmentedTouchpad == null) return; - PeltzerMain.Instance.peltzerController.controllerGeometry.touchpad.SetActive(!isAllowed); - PeltzerMain.Instance.paletteController.controllerGeometry.touchpad.SetActive(!isAllowed); - PeltzerMain.Instance.peltzerController.controllerGeometry.segmentedTouchpad.SetActive(isAllowed); - PeltzerMain.Instance.paletteController.controllerGeometry.segmentedTouchpad.SetActive(isAllowed); + PeltzerMain.Instance.peltzerController.controllerGeometry.touchpad.SetActive(!isAllowed); + PeltzerMain.Instance.paletteController.controllerGeometry.touchpad.SetActive(!isAllowed); + PeltzerMain.Instance.peltzerController.controllerGeometry.segmentedTouchpad.SetActive(isAllowed); + PeltzerMain.Instance.paletteController.controllerGeometry.segmentedTouchpad.SetActive(isAllowed); - touchpadHighlightingAllowed = isAllowed; - } + touchpadHighlightingAllowed = isAllowed; + } - public void SetTouchpadAllowed (TouchpadLocation touchPad) { - touchpadUpAllowed = touchPad == TouchpadLocation.TOP; - touchpadDownAllowed = touchPad == TouchpadLocation.BOTTOM; - touchpadRightAllowed = touchPad == TouchpadLocation.RIGHT; - touchpadLeftAllowed = touchPad == TouchpadLocation.LEFT; - } + public void SetTouchpadAllowed(TouchpadLocation touchPad) + { + touchpadUpAllowed = touchPad == TouchpadLocation.TOP; + touchpadDownAllowed = touchPad == TouchpadLocation.BOTTOM; + touchpadRightAllowed = touchPad == TouchpadLocation.RIGHT; + touchpadLeftAllowed = touchPad == TouchpadLocation.LEFT; + } - /// - /// Unrestricts everything, returning to the default unrestricted mode. - /// - public void AllowAll() { - Reset(/* allowAll */ true); - } + /// + /// Unrestricts everything, returning to the default unrestricted mode. + /// + public void AllowAll() + { + Reset(/* allowAll */ true); + } - /// - /// Restricts everything. - /// - public void ForbidAll() { - Reset(/* allowAll */ false); - } + /// + /// Restricts everything. + /// + public void ForbidAll() + { + Reset(/* allowAll */ false); + } - /// - /// Resets all restrictions to either the allowed state or the restricted state. - /// - private void Reset(bool allow) { - for (int i = 0; i < controllerModeAllowed.Length; i++) { - controllerModeAllowed[i] = allow; - } - volumeInsertionAllowed = allow; - volumeFillingAllowed = allow; - shapesMenuAllowed = allow; - scaleOnVolumeInsertionAllowed = allow; - paletteAllowed = allow; - menuActionsAllowed = allow; - tooltipsAllowed = allow; - undoRedoAllowed = allow; - changeWorldTransformAllowed = allow; - insertingReferenceImagesAllowed = allow; - changingColorsAllowed = allow; - touchpadUpAllowed = allow; - touchpadDownAllowed = allow; - touchpadRightAllowed = allow; - touchpadLeftAllowed = allow; - controllerEventsAllowed = allow; - toolheadColorChangeAllowed = allow; - showingWorldBoundingBoxAllowed = allow; - snappingAllowed = allow; - movingMeshesAllowed = allow; - copyingAllowed = allow; - tutorialMenuActionsAllowed = allow; - deselectAllowed = allow; - // Allowing a larger selection radius is the non-standard state. - increasedMultiSelectRadiusAllowed = !allow; - - // -1 means all are allowed, -2 mean none. - if (allow) { - SetOnlyAllowedColor(-1); - } else { - SetOnlyAllowedColor(-2); - } - - // Highlighting is the non-standard state. - SetTouchpadHighlightingAllowed(!allow); - - throwAwayAllowed = allow; - menuSwitchAllowed = allow; - if (allow) { - onlySelectableMeshIdForTutorial = null; - } + /// + /// Resets all restrictions to either the allowed state or the restricted state. + /// + private void Reset(bool allow) + { + for (int i = 0; i < controllerModeAllowed.Length; i++) + { + controllerModeAllowed[i] = allow; + } + volumeInsertionAllowed = allow; + volumeFillingAllowed = allow; + shapesMenuAllowed = allow; + scaleOnVolumeInsertionAllowed = allow; + paletteAllowed = allow; + menuActionsAllowed = allow; + tooltipsAllowed = allow; + undoRedoAllowed = allow; + changeWorldTransformAllowed = allow; + insertingReferenceImagesAllowed = allow; + changingColorsAllowed = allow; + touchpadUpAllowed = allow; + touchpadDownAllowed = allow; + touchpadRightAllowed = allow; + touchpadLeftAllowed = allow; + controllerEventsAllowed = allow; + toolheadColorChangeAllowed = allow; + showingWorldBoundingBoxAllowed = allow; + snappingAllowed = allow; + movingMeshesAllowed = allow; + copyingAllowed = allow; + tutorialMenuActionsAllowed = allow; + deselectAllowed = allow; + // Allowing a larger selection radius is the non-standard state. + increasedMultiSelectRadiusAllowed = !allow; + + // -1 means all are allowed, -2 mean none. + if (allow) + { + SetOnlyAllowedColor(-1); + } + else + { + SetOnlyAllowedColor(-2); + } + + // Highlighting is the non-standard state. + SetTouchpadHighlightingAllowed(!allow); + + throwAwayAllowed = allow; + menuSwitchAllowed = allow; + if (allow) + { + onlySelectableMeshIdForTutorial = null; + } + } } - } } diff --git a/Assets/Scripts/model/main/WorldSpace.cs b/Assets/Scripts/model/main/WorldSpace.cs index 925fdbf1..a327b56e 100644 --- a/Assets/Scripts/model/main/WorldSpace.cs +++ b/Assets/Scripts/model/main/WorldSpace.cs @@ -14,87 +14,101 @@ using UnityEngine; -namespace com.google.apps.peltzer.client.model.main { - /// - /// Manages the transformation between "world space" and "model space". - /// - /// Model space is the space in which the meshes are represented. The world space is the Unity coordinate system. - /// By manipulating the transformation between model and world space, the user can pan/rotate/zoom the model - /// to make editing easier. The transform, however, is NOT part of the model, it's just a viewing convenience - /// (think of it like the scrollbar position and zoom level of a 2D document -- they just influence how you - /// view the document, they are not part of the content itself). - /// - /// - public class WorldSpace { - private const float MAX_SCALE = 4.0f; - private const float MIN_SCALE = 0.25f; - private const float DEFAULT_SCALE = 0.8f; - private float _scale; - private Vector3 _offset; - private Quaternion _rotation = Quaternion.identity; - private Matrix4x4 _modelToWorld; - private Matrix4x4 _worldToModel; - private bool isLimited; - public Bounds bounds { get; private set; } - public float scale { get { return _scale; } set { _scale = LimitScale(value); RecalcTransform(); } } - public Quaternion rotation { get { return _rotation; } set { _rotation = value; RecalcTransform(); } } - public Vector3 offset { get { return _offset; } set { _offset = value; RecalcTransform(); } } - public Matrix4x4 modelToWorld { get { return _modelToWorld; } private set { _modelToWorld = value; } } - public Matrix4x4 worldToModel { get { return _worldToModel; } private set { _worldToModel = value; } } +namespace com.google.apps.peltzer.client.model.main +{ + /// + /// Manages the transformation between "world space" and "model space". + /// + /// Model space is the space in which the meshes are represented. The world space is the Unity coordinate system. + /// By manipulating the transformation between model and world space, the user can pan/rotate/zoom the model + /// to make editing easier. The transform, however, is NOT part of the model, it's just a viewing convenience + /// (think of it like the scrollbar position and zoom level of a 2D document -- they just influence how you + /// view the document, they are not part of the content itself). + /// + /// + public class WorldSpace + { + private const float MAX_SCALE = 4.0f; + private const float MIN_SCALE = 0.25f; + private const float DEFAULT_SCALE = 0.8f; + private float _scale; + private Vector3 _offset; + private Quaternion _rotation = Quaternion.identity; + private Matrix4x4 _modelToWorld; + private Matrix4x4 _worldToModel; + private bool isLimited; + public Bounds bounds { get; private set; } + public float scale { get { return _scale; } set { _scale = LimitScale(value); RecalcTransform(); } } + public Quaternion rotation { get { return _rotation; } set { _rotation = value; RecalcTransform(); } } + public Vector3 offset { get { return _offset; } set { _offset = value; RecalcTransform(); } } + public Matrix4x4 modelToWorld { get { return _modelToWorld; } private set { _modelToWorld = value; } } + public Matrix4x4 worldToModel { get { return _worldToModel; } private set { _worldToModel = value; } } - public WorldSpace(Bounds bounds, bool isLimited = true) { - this.bounds = bounds; - this.isLimited = isLimited; - SetToDefault(); - } + public WorldSpace(Bounds bounds, bool isLimited = true) + { + this.bounds = bounds; + this.isLimited = isLimited; + SetToDefault(); + } - public void SetToDefault() { - scale = DEFAULT_SCALE; - offset = Vector3.zero; - _rotation = Quaternion.identity; - } + public void SetToDefault() + { + scale = DEFAULT_SCALE; + offset = Vector3.zero; + _rotation = Quaternion.identity; + } - public Vector3 WorldToModel(Vector3 pos) { - return _worldToModel.MultiplyPoint(pos); - } + public Vector3 WorldToModel(Vector3 pos) + { + return _worldToModel.MultiplyPoint(pos); + } - public Vector3 ModelToWorld(Vector3 pos) { - return _modelToWorld.MultiplyPoint(pos); - } + public Vector3 ModelToWorld(Vector3 pos) + { + return _modelToWorld.MultiplyPoint(pos); + } - public Vector3 ModelVectorToWorld(Vector3 vec) { - return _modelToWorld.MultiplyVector(vec); - } + public Vector3 ModelVectorToWorld(Vector3 vec) + { + return _modelToWorld.MultiplyVector(vec); + } - public Vector3 WorldVectorToModel(Vector3 vec) { - return _worldToModel.MultiplyVector(vec); - } + public Vector3 WorldVectorToModel(Vector3 vec) + { + return _worldToModel.MultiplyVector(vec); + } - public Quaternion ModelOrientationToWorld(Quaternion orient) { - return rotation * orient; - } + public Quaternion ModelOrientationToWorld(Quaternion orient) + { + return rotation * orient; + } - public Quaternion WorldOrientationToModel(Quaternion orient) { - return Quaternion.Inverse(rotation) * orient; - } + public Quaternion WorldOrientationToModel(Quaternion orient) + { + return Quaternion.Inverse(rotation) * orient; + } - public void ResetScale() { - scale = 1.0f; - } + public void ResetScale() + { + scale = 1.0f; + } - public void ResetRotation() { - _rotation = Quaternion.identity; - } + public void ResetRotation() + { + _rotation = Quaternion.identity; + } - private void RecalcTransform() { - // TODO(31747542): Non identity rotations break things that use _offset and _scale directly instead - // of using the matrices. - _modelToWorld = Matrix4x4.TRS(_offset, _rotation, Vector3.one * _scale); - worldToModel = _modelToWorld.inverse; - } + private void RecalcTransform() + { + // TODO(31747542): Non identity rotations break things that use _offset and _scale directly instead + // of using the matrices. + _modelToWorld = Matrix4x4.TRS(_offset, _rotation, Vector3.one * _scale); + worldToModel = _modelToWorld.inverse; + } - private float LimitScale(float scale) { - return isLimited ? Mathf.Min(MAX_SCALE, Mathf.Max(scale, MIN_SCALE)) : scale; + private float LimitScale(float scale) + { + return isLimited ? Mathf.Min(MAX_SCALE, Mathf.Max(scale, MIN_SCALE)) : scale; + } } - } } diff --git a/Assets/Scripts/model/main/Zoomer.cs b/Assets/Scripts/model/main/Zoomer.cs index 6b8e8a9c..76213bbf 100644 --- a/Assets/Scripts/model/main/Zoomer.cs +++ b/Assets/Scripts/model/main/Zoomer.cs @@ -17,301 +17,346 @@ using com.google.apps.peltzer.client.model.controller; using com.google.apps.peltzer.client.app; -namespace com.google.apps.peltzer.client.model.main { - /// - /// Allow the user to control the world transform. - /// This is misnamed: this doesn't only do zoom but also pan/rotate. - /// - public class Zoomer : MonoBehaviour { - // The amount of time to forcefully show the world space bounding box in the beginning of the session. - private const float FIRST_TIME_RUN_DURATION = 10.0f; - - private PeltzerController peltzerController; - private PaletteController paletteController; - private WorldSpace worldSpace; - private GameObject visualBoundingBox; - private GameObject visualBoundingBoxCube; - private GameObject visualBoundingBoxLines; - private GameObject gridPlanes; - private AudioLibrary audioLibrary; - - // Whether the user is manipulating a grid plane to show the bounding box or not. - public bool isManipulatingGridPlane = false; - // Whether to hide tooltips for moving/zooming. - public bool userHasEverMoved = false; - public bool userHasEverZoomed = false; - - // Did we start moving or zooming last frame? If so, lastXXXPos are valid. - public bool Zooming { get; set; } - public bool moving = false; - public bool isMovingWithPaletteController; - public bool isMovingWithPeltzerController; - private Vector3 lastPalettePos; - private Vector3 lastPeltzerPos; - - // The start time within the life cycle of the application session as to when the instance, - // which controls the visibility of the world space, comes into play. - public float firstRunStartTime = 0f; - - // Details of the world state at the beginning of a zoom operation. - private float startDistance; - private float startScale; - private Quaternion worldRotationAtZoomStart = Quaternion.identity; - private Vector3 controllerDiffAtZoomStart; - private Vector3 centerAtZoomStartModel; - - // World space rendering. - private Material wallMaterial; - - /// - /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. - /// As such, this setup method must be called before the tool is used for it to have a valid state. - /// - public void Setup(ControllerMain controllerMain, PeltzerController peltzerController, - PaletteController paletteController, WorldSpace worldSpace, AudioLibrary audioLibrary) { - this.peltzerController = peltzerController; - this.paletteController = paletteController; - this.worldSpace = worldSpace; - this.audioLibrary = audioLibrary; - controllerMain.ControllerActionHandler += ControllerActionHandler; - - // Get the reference to the bounding box GameObject. - visualBoundingBox = ObjectFinder.ObjectById("ID_PolyWorldBounds"); - gridPlanes = visualBoundingBox.transform.Find("GridPlanes").gameObject; - visualBoundingBoxCube = visualBoundingBox.transform.Find("Cube").gameObject; - visualBoundingBoxLines = visualBoundingBox.transform.Find("Lines").gameObject; - wallMaterial = visualBoundingBox.transform.Find("Cube").GetComponent().material; - } - - private bool IsResetEvent(ControllerEventArgs args) { - // Only consider this a reset event if the secondary button hit was on the same controller - // which has a trigger down. - if ((isMovingWithPaletteController && args.ControllerType == ControllerType.PALETTE) - || (isMovingWithPeltzerController && args.ControllerType == ControllerType.PELTZER)) { - // If the controller is a Rift, reset uses the secondary button; otherwise uses the touchpad. - if (Config.Instance.VrHardware == VrHardware.Rift) { - return args.ButtonId == ButtonId.SecondaryButton - && args.Action == ButtonAction.DOWN; - } - return args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN; - } else { - return false; - } - } - - private void ControllerActionHandler(object sender, ControllerEventArgs args) { - if ((Zooming || moving) && IsResetEvent(args)) { - ClearState(); - audioLibrary.PlayClip(audioLibrary.zoomResetSound); - } - } - - public void ClearState() { - worldSpace.SetToDefault(); - InitZoomStartVars(); - } - +namespace com.google.apps.peltzer.client.model.main +{ /// - /// Keep track of the world-state at the start of a zoom operation, so we can calculate deltas. + /// Allow the user to control the world transform. + /// This is misnamed: this doesn't only do zoom but also pan/rotate. /// - private void InitZoomStartVars() { - worldRotationAtZoomStart = worldSpace.rotation; - - Vector3 paletteControllerPositionAtZoomStart = paletteController.transform.position; - Vector3 peltzerControllerPositionAtZoomStart = peltzerController.transform.position; - Vector3 paletteControllerModelPositionAtZoomStart = worldSpace.WorldToModel(paletteControllerPositionAtZoomStart); - Vector3 peltzerControllerModelPositionAtZoomStart = worldSpace.WorldToModel(peltzerControllerPositionAtZoomStart); - - centerAtZoomStartModel = (paletteControllerModelPositionAtZoomStart + peltzerControllerModelPositionAtZoomStart) * 0.5f; - controllerDiffAtZoomStart = Vector3.Normalize(paletteControllerPositionAtZoomStart - peltzerControllerPositionAtZoomStart); - startScale = worldSpace.scale; - startDistance = Vector3.Distance(paletteControllerPositionAtZoomStart, peltzerControllerPositionAtZoomStart); - } - - void Update() { - if (!PeltzerMain.Instance.restrictionManager.changeWorldTransformAllowed) { - return; - } - - if (firstRunStartTime == 0f) { - firstRunStartTime = Time.time; - } - - // Check for grips. - bool paletteGripDown = false; - bool peltzerGripDown = false; - paletteGripDown = PaletteController.AcquireIfNecessary(ref paletteController) - && paletteController.controller.IsPressed(ButtonId.Grip); - peltzerGripDown = PeltzerController.AcquireIfNecessary(ref peltzerController) - && peltzerController.controller.IsPressed(ButtonId.Grip); - - if (paletteGripDown && peltzerGripDown && !PeltzerMain.Instance.tutorialManager.TutorialOccurring()) { - // Where zooming is false, this means this is the first frame of the ZoomWorld operation, so all we'll do is - // collect the controller positions so we can detect relative changes in distance between controllers. - if (Zooming) { - ZoomWorld(); - } else { - StartZooming(); + public class Zoomer : MonoBehaviour + { + // The amount of time to forcefully show the world space bounding box in the beginning of the session. + private const float FIRST_TIME_RUN_DURATION = 10.0f; + + private PeltzerController peltzerController; + private PaletteController paletteController; + private WorldSpace worldSpace; + private GameObject visualBoundingBox; + private GameObject visualBoundingBoxCube; + private GameObject visualBoundingBoxLines; + private GameObject gridPlanes; + private AudioLibrary audioLibrary; + + // Whether the user is manipulating a grid plane to show the bounding box or not. + public bool isManipulatingGridPlane = false; + // Whether to hide tooltips for moving/zooming. + public bool userHasEverMoved = false; + public bool userHasEverZoomed = false; + + // Did we start moving or zooming last frame? If so, lastXXXPos are valid. + public bool Zooming { get; set; } + public bool moving = false; + public bool isMovingWithPaletteController; + public bool isMovingWithPeltzerController; + private Vector3 lastPalettePos; + private Vector3 lastPeltzerPos; + + // The start time within the life cycle of the application session as to when the instance, + // which controls the visibility of the world space, comes into play. + public float firstRunStartTime = 0f; + + // Details of the world state at the beginning of a zoom operation. + private float startDistance; + private float startScale; + private Quaternion worldRotationAtZoomStart = Quaternion.identity; + private Vector3 controllerDiffAtZoomStart; + private Vector3 centerAtZoomStartModel; + + // World space rendering. + private Material wallMaterial; + + /// + /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. + /// As such, this setup method must be called before the tool is used for it to have a valid state. + /// + public void Setup(ControllerMain controllerMain, PeltzerController peltzerController, + PaletteController paletteController, WorldSpace worldSpace, AudioLibrary audioLibrary) + { + this.peltzerController = peltzerController; + this.paletteController = paletteController; + this.worldSpace = worldSpace; + this.audioLibrary = audioLibrary; + controllerMain.ControllerActionHandler += ControllerActionHandler; + + // Get the reference to the bounding box GameObject. + visualBoundingBox = ObjectFinder.ObjectById("ID_PolyWorldBounds"); + gridPlanes = visualBoundingBox.transform.Find("GridPlanes").gameObject; + visualBoundingBoxCube = visualBoundingBox.transform.Find("Cube").gameObject; + visualBoundingBoxLines = visualBoundingBox.transform.Find("Lines").gameObject; + wallMaterial = visualBoundingBox.transform.Find("Cube").GetComponent().material; } - } else { - if (Zooming) { - EndZooming(); + + private bool IsResetEvent(ControllerEventArgs args) + { + // Only consider this a reset event if the secondary button hit was on the same controller + // which has a trigger down. + if ((isMovingWithPaletteController && args.ControllerType == ControllerType.PALETTE) + || (isMovingWithPeltzerController && args.ControllerType == ControllerType.PELTZER)) + { + // If the controller is a Rift, reset uses the secondary button; otherwise uses the touchpad. + if (Config.Instance.VrHardware == VrHardware.Rift) + { + return args.ButtonId == ButtonId.SecondaryButton + && args.Action == ButtonAction.DOWN; + } + return args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN; + } + else + { + return false; + } } - if (paletteGripDown || peltzerGripDown) { - // Where moving is false, this means this is the first frame of the MoveWorld operation, so all we'll do is - // collect the controller positions so we can detect movement. - if (moving) { - Vector3 offset = paletteGripDown ? - paletteController.transform.position - lastPalettePos : - peltzerController.transform.position - lastPeltzerPos; - worldSpace.offset += offset; - } - if (paletteGripDown) { - StartMovingWithPaletteController(); - StopMovingWithPeltzerController(); - } else { - StartMovingWithPeltzerController(); - StopMovingWithPaletteController(); - } - userHasEverMoved = true; - moving = true; - } else if (moving) { - // Check if we are currently moving before setting moving to false. - moving = false; - StopMovingWithPeltzerController(); - StopMovingWithPaletteController(); + + private void ControllerActionHandler(object sender, ControllerEventArgs args) + { + if ((Zooming || moving) && IsResetEvent(args)) + { + ClearState(); + audioLibrary.PlayClip(audioLibrary.zoomResetSound); + } } - } - if (Zooming || moving) { - if (paletteController != null) { - lastPalettePos = paletteController.transform.position; + + public void ClearState() + { + worldSpace.SetToDefault(); + InitZoomStartVars(); } - if (peltzerController != null) { - lastPeltzerPos = peltzerController.transform.position; + + /// + /// Keep track of the world-state at the start of a zoom operation, so we can calculate deltas. + /// + private void InitZoomStartVars() + { + worldRotationAtZoomStart = worldSpace.rotation; + + Vector3 paletteControllerPositionAtZoomStart = paletteController.transform.position; + Vector3 peltzerControllerPositionAtZoomStart = peltzerController.transform.position; + Vector3 paletteControllerModelPositionAtZoomStart = worldSpace.WorldToModel(paletteControllerPositionAtZoomStart); + Vector3 peltzerControllerModelPositionAtZoomStart = worldSpace.WorldToModel(peltzerControllerPositionAtZoomStart); + + centerAtZoomStartModel = (paletteControllerModelPositionAtZoomStart + peltzerControllerModelPositionAtZoomStart) * 0.5f; + controllerDiffAtZoomStart = Vector3.Normalize(paletteControllerPositionAtZoomStart - peltzerControllerPositionAtZoomStart); + startScale = worldSpace.scale; + startDistance = Vector3.Distance(paletteControllerPositionAtZoomStart, peltzerControllerPositionAtZoomStart); } - } - if (PeltzerMain.Instance.restrictionManager.showingWorldBoundingBoxAllowed) { - MaybeShowBoundingBox(); - } - } + void Update() + { + if (!PeltzerMain.Instance.restrictionManager.changeWorldTransformAllowed) + { + return; + } + + if (firstRunStartTime == 0f) + { + firstRunStartTime = Time.time; + } + + // Check for grips. + bool paletteGripDown = false; + bool peltzerGripDown = false; + paletteGripDown = PaletteController.AcquireIfNecessary(ref paletteController) + && paletteController.controller.IsPressed(ButtonId.Grip); + peltzerGripDown = PeltzerController.AcquireIfNecessary(ref peltzerController) + && peltzerController.controller.IsPressed(ButtonId.Grip); + + if (paletteGripDown && peltzerGripDown && !PeltzerMain.Instance.tutorialManager.TutorialOccurring()) + { + // Where zooming is false, this means this is the first frame of the ZoomWorld operation, so all we'll do is + // collect the controller positions so we can detect relative changes in distance between controllers. + if (Zooming) + { + ZoomWorld(); + } + else + { + StartZooming(); + } + } + else + { + if (Zooming) + { + EndZooming(); + } + if (paletteGripDown || peltzerGripDown) + { + // Where moving is false, this means this is the first frame of the MoveWorld operation, so all we'll do is + // collect the controller positions so we can detect movement. + if (moving) + { + Vector3 offset = paletteGripDown ? + paletteController.transform.position - lastPalettePos : + peltzerController.transform.position - lastPeltzerPos; + worldSpace.offset += offset; + } + if (paletteGripDown) + { + StartMovingWithPaletteController(); + StopMovingWithPeltzerController(); + } + else + { + StartMovingWithPeltzerController(); + StopMovingWithPaletteController(); + } + userHasEverMoved = true; + moving = true; + } + else if (moving) + { + // Check if we are currently moving before setting moving to false. + moving = false; + StopMovingWithPeltzerController(); + StopMovingWithPaletteController(); + } + } + if (Zooming || moving) + { + if (paletteController != null) + { + lastPalettePos = paletteController.transform.position; + } + if (peltzerController != null) + { + lastPeltzerPos = peltzerController.transform.position; + } + } + + if (PeltzerMain.Instance.restrictionManager.showingWorldBoundingBoxAllowed) + { + MaybeShowBoundingBox(); + } + } - private void StartMovingWithPaletteController() { - isMovingWithPaletteController = true; - paletteController.ChangeTouchpadOverlay(TouchpadOverlay.RESET_ZOOM); - } + private void StartMovingWithPaletteController() + { + isMovingWithPaletteController = true; + paletteController.ChangeTouchpadOverlay(TouchpadOverlay.RESET_ZOOM); + } - private void StopMovingWithPaletteController() { - isMovingWithPaletteController = false; - paletteController.ResetTouchpadOverlay(); - } + private void StopMovingWithPaletteController() + { + isMovingWithPaletteController = false; + paletteController.ResetTouchpadOverlay(); + } - private void StartMovingWithPeltzerController() { - isMovingWithPeltzerController = true; - peltzerController.ChangeTouchpadOverlay(TouchpadOverlay.RESET_ZOOM); - } + private void StartMovingWithPeltzerController() + { + isMovingWithPeltzerController = true; + peltzerController.ChangeTouchpadOverlay(TouchpadOverlay.RESET_ZOOM); + } - private void StopMovingWithPeltzerController() { - isMovingWithPeltzerController = false; - peltzerController.ResetTouchpadOverlay(); - } + private void StopMovingWithPeltzerController() + { + isMovingWithPeltzerController = false; + peltzerController.ResetTouchpadOverlay(); + } - private void StartZooming() { - Zooming = true; - StartMovingWithPaletteController(); - StartMovingWithPeltzerController(); - if (!userHasEverZoomed) { - userHasEverZoomed = true; - peltzerController.DisableGripTooltips(); - paletteController.DisableGripTooltips(); - } - peltzerController.ChangeTouchpadOverlay(TouchpadOverlay.RESET_ZOOM); - paletteController.ChangeTouchpadOverlay(TouchpadOverlay.RESET_ZOOM); - InitZoomStartVars(); - PeltzerMain.Instance.restrictionManager.undoRedoAllowed = false; - } + private void StartZooming() + { + Zooming = true; + StartMovingWithPaletteController(); + StartMovingWithPeltzerController(); + if (!userHasEverZoomed) + { + userHasEverZoomed = true; + peltzerController.DisableGripTooltips(); + paletteController.DisableGripTooltips(); + } + peltzerController.ChangeTouchpadOverlay(TouchpadOverlay.RESET_ZOOM); + paletteController.ChangeTouchpadOverlay(TouchpadOverlay.RESET_ZOOM); + InitZoomStartVars(); + PeltzerMain.Instance.restrictionManager.undoRedoAllowed = false; + } - private void EndZooming() { - Zooming = false; - peltzerController.ResetTouchpadOverlay(); - paletteController.ResetTouchpadOverlay(); - StopMovingWithPaletteController(); - StopMovingWithPeltzerController(); - // Re-enable zooming, if the tool menu is active. - PeltzerMain.Instance.restrictionManager.undoRedoAllowed = PeltzerMain.Instance.GetPolyMenuMain().ToolMenuIsActive(); - } + private void EndZooming() + { + Zooming = false; + peltzerController.ResetTouchpadOverlay(); + paletteController.ResetTouchpadOverlay(); + StopMovingWithPaletteController(); + StopMovingWithPeltzerController(); + // Re-enable zooming, if the tool menu is active. + PeltzerMain.Instance.restrictionManager.undoRedoAllowed = PeltzerMain.Instance.GetPolyMenuMain().ToolMenuIsActive(); + } - /// - /// Zooms the entire world, based on the relative position of the controllers. - /// - private void ZoomWorld() { - Vector3 curPalettePos = paletteController.transform.position; - Vector3 curPeltzerPos = peltzerController.transform.position; - Vector3 curDiffVector = Vector3.Normalize(curPalettePos - curPeltzerPos); - - // We only allow rotation around the Y axis. - Quaternion curRotation = Quaternion.FromToRotation(controllerDiffAtZoomStart, curDiffVector); - curRotation = Quaternion.Euler(new Vector3(0, curRotation.eulerAngles.y, 0)); - - // Change the scale by the ratio of the old distance to the new one. - float curDist = Vector3.Distance(curPalettePos, curPeltzerPos); - worldSpace.scale = startScale / (startDistance / curDist); - worldSpace.rotation = curRotation * worldRotationAtZoomStart; - - - Vector3 targetWorldCenter = (curPalettePos + curPeltzerPos) * 0.5f; - // We also need to change the offset so that the model-space position we were centered on at start remains - // in the same point in world space. - Vector3 toTargetWorldPos = targetWorldCenter - worldSpace.ModelToWorld(centerAtZoomStartModel); - worldSpace.offset = worldSpace.offset + toTargetWorldPos; - } + /// + /// Zooms the entire world, based on the relative position of the controllers. + /// + private void ZoomWorld() + { + Vector3 curPalettePos = paletteController.transform.position; + Vector3 curPeltzerPos = peltzerController.transform.position; + Vector3 curDiffVector = Vector3.Normalize(curPalettePos - curPeltzerPos); + + // We only allow rotation around the Y axis. + Quaternion curRotation = Quaternion.FromToRotation(controllerDiffAtZoomStart, curDiffVector); + curRotation = Quaternion.Euler(new Vector3(0, curRotation.eulerAngles.y, 0)); + + // Change the scale by the ratio of the old distance to the new one. + float curDist = Vector3.Distance(curPalettePos, curPeltzerPos); + worldSpace.scale = startScale / (startDistance / curDist); + worldSpace.rotation = curRotation * worldRotationAtZoomStart; + + + Vector3 targetWorldCenter = (curPalettePos + curPeltzerPos) * 0.5f; + // We also need to change the offset so that the model-space position we were centered on at start remains + // in the same point in world space. + Vector3 toTargetWorldPos = targetWorldCenter - worldSpace.ModelToWorld(centerAtZoomStartModel); + worldSpace.offset = worldSpace.offset + toTargetWorldPos; + } - /// - /// Updates the position, rotation and scale of the visual bounding box. - /// - private void UpdateVisualBoundingBox() { - // Get world position of selector position. - Vector4 selectorWorldPosition = PeltzerMain.Instance.worldSpace - .ModelToWorld(PeltzerMain.Instance.peltzerController.LastPositionModel); - selectorWorldPosition.w = 0; - wallMaterial.SetVector("_SelectorPosition", selectorWorldPosition); - visualBoundingBox.transform.position = worldSpace.offset; - visualBoundingBox.transform.localScale = worldSpace.scale * PeltzerMain.DEFAULT_BOUNDS.size; - visualBoundingBox.transform.localRotation = worldSpace.rotation; - } + /// + /// Updates the position, rotation and scale of the visual bounding box. + /// + private void UpdateVisualBoundingBox() + { + // Get world position of selector position. + Vector4 selectorWorldPosition = PeltzerMain.Instance.worldSpace + .ModelToWorld(PeltzerMain.Instance.peltzerController.LastPositionModel); + selectorWorldPosition.w = 0; + wallMaterial.SetVector("_SelectorPosition", selectorWorldPosition); + visualBoundingBox.transform.position = worldSpace.offset; + visualBoundingBox.transform.localScale = worldSpace.scale * PeltzerMain.DEFAULT_BOUNDS.size; + visualBoundingBox.transform.localRotation = worldSpace.rotation; + } - /// - /// Makes the bounding box visible, if required. - /// - private void MaybeShowBoundingBox() { - bool userInBounds = worldSpace.bounds.Contains(peltzerController.LastPositionModel); - // Measure the controller positions distance from the world space bounds using delta of bounds extents. - Bounds tempBounds = new Bounds(); - tempBounds.Encapsulate(worldSpace.bounds); - tempBounds.Expand(-1.0f); - userInBounds = tempBounds.Contains(peltzerController.LastPositionModel); - bool showVisualBoundingBox = IsFirstTimeShow() || Zooming - || moving - || !userInBounds - || peltzerController.heldMeshes - || isManipulatingGridPlane ? true : false; - visualBoundingBoxLines.SetActive(showVisualBoundingBox); - visualBoundingBoxCube.SetActive(showVisualBoundingBox); - if (showVisualBoundingBox) { - UpdateVisualBoundingBox(); - } - } + /// + /// Makes the bounding box visible, if required. + /// + private void MaybeShowBoundingBox() + { + bool userInBounds = worldSpace.bounds.Contains(peltzerController.LastPositionModel); + // Measure the controller positions distance from the world space bounds using delta of bounds extents. + Bounds tempBounds = new Bounds(); + tempBounds.Encapsulate(worldSpace.bounds); + tempBounds.Expand(-1.0f); + userInBounds = tempBounds.Contains(peltzerController.LastPositionModel); + bool showVisualBoundingBox = IsFirstTimeShow() || Zooming + || moving + || !userInBounds + || peltzerController.heldMeshes + || isManipulatingGridPlane ? true : false; + visualBoundingBoxLines.SetActive(showVisualBoundingBox); + visualBoundingBoxCube.SetActive(showVisualBoundingBox); + if (showVisualBoundingBox) + { + UpdateVisualBoundingBox(); + } + } - /// - /// First time show is an override to subsequent logic that allows us to "present" the bounding box - /// as a first time / onboarding segment. This function maintains the initial presentation of the - /// worldspace bounds and persists it for the duration specified by FIRST_TIME_RUN_DURATION. - /// - private bool IsFirstTimeShow() { - // If duration is met, set firstTimeRun = false - float timeDelta = Time.time - firstRunStartTime; - return timeDelta < FIRST_TIME_RUN_DURATION; + /// + /// First time show is an override to subsequent logic that allows us to "present" the bounding box + /// as a first time / onboarding segment. This function maintains the initial presentation of the + /// worldspace bounds and persists it for the duration specified by FIRST_TIME_RUN_DURATION. + /// + private bool IsFirstTimeShow() + { + // If duration is met, set firstTimeRun = false + float timeDelta = Time.time - firstRunStartTime; + return timeDelta < FIRST_TIME_RUN_DURATION; + } } - } } diff --git a/Assets/Scripts/model/render/ColorChanger.cs b/Assets/Scripts/model/render/ColorChanger.cs index 4074e5b4..90762e10 100644 --- a/Assets/Scripts/model/render/ColorChanger.cs +++ b/Assets/Scripts/model/render/ColorChanger.cs @@ -14,28 +14,36 @@ using UnityEngine; -namespace com.google.apps.peltzer.client.model.render { - /// - /// Placed on a component with a renderer, this will update the renderer to use a given material from the - /// registry when ChangeMaterial is called. - /// - public class ColorChanger : MonoBehaviour { - public Renderer rend; +namespace com.google.apps.peltzer.client.model.render +{ + /// + /// Placed on a component with a renderer, this will update the renderer to use a given material from the + /// registry when ChangeMaterial is called. + /// + public class ColorChanger : MonoBehaviour + { + public Renderer rend; - void Start() { - rend = GetComponent(); - } + void Start() + { + rend = GetComponent(); + } - public void ChangeMaterial(int materialId) { - if (rend != null) { - if (rend.material.HasProperty("_OverrideAmount")) { // TODO: Could add a default material to stuff instead. - float overrideAmount = rend.material.GetFloat("_OverrideAmount"); - rend.material = new Material(MaterialRegistry.GetMaterialWithAlbedoById(materialId)); - rend.material.SetFloat("_OverrideAmount", overrideAmount); - } else { - rend.material = new Material(MaterialRegistry.GetMaterialWithAlbedoById(materialId)); + public void ChangeMaterial(int materialId) + { + if (rend != null) + { + if (rend.material.HasProperty("_OverrideAmount")) + { // TODO: Could add a default material to stuff instead. + float overrideAmount = rend.material.GetFloat("_OverrideAmount"); + rend.material = new Material(MaterialRegistry.GetMaterialWithAlbedoById(materialId)); + rend.material.SetFloat("_OverrideAmount", overrideAmount); + } + else + { + rend.material = new Material(MaterialRegistry.GetMaterialWithAlbedoById(materialId)); + } + } } - } } - } } diff --git a/Assets/Scripts/model/render/FaceTriangulator.cs b/Assets/Scripts/model/render/FaceTriangulator.cs index 33b8fb60..669237ce 100644 --- a/Assets/Scripts/model/render/FaceTriangulator.cs +++ b/Assets/Scripts/model/render/FaceTriangulator.cs @@ -20,363 +20,416 @@ using System.Text; using UnityEngine; -namespace com.google.apps.peltzer.client.model.render { - public struct Triangle { - public int vertId0 { get; private set; } - public int vertId1 { get; private set; } - public int vertId2 { get; private set; } - - public Triangle(int v1, int v2, int v3) { - vertId0 = v1; - vertId1 = v2; - vertId2 = v3; - } - - public override string ToString() +namespace com.google.apps.peltzer.client.model.render +{ + public struct Triangle { - return string.Format("[Triangle: vertId0={0}, vertId1={1}, vertId2={2}]", vertId0, vertId1, vertId2); - } + public int vertId0 { get; private set; } + public int vertId1 { get; private set; } + public int vertId2 { get; private set; } + + public Triangle(int v1, int v2, int v3) + { + vertId0 = v1; + vertId1 = v2; + vertId2 = v3; + } - public override bool Equals(object obj) - { - return obj is Triangle ? Equals((Triangle)obj) : base.Equals(obj); - } + public override string ToString() + { + return string.Format("[Triangle: vertId0={0}, vertId1={1}, vertId2={2}]", vertId0, vertId1, vertId2); + } - public bool Equals(Triangle other) - { - // Same vertices in same winding order. - return (vertId0 == other.vertId0 && vertId1 == other.vertId1 && vertId2 == other.vertId2) || - (vertId0 == other.vertId1 && vertId1 == other.vertId2 && vertId2 == other.vertId0) || - (vertId0 == other.vertId2 && vertId1 == other.vertId0 && vertId2 == other.vertId1); + public override bool Equals(object obj) + { + return obj is Triangle ? Equals((Triangle)obj) : base.Equals(obj); + } + + public bool Equals(Triangle other) + { + // Same vertices in same winding order. + return (vertId0 == other.vertId0 && vertId1 == other.vertId1 && vertId2 == other.vertId2) || + (vertId0 == other.vertId1 && vertId1 == other.vertId2 && vertId2 == other.vertId0) || + (vertId0 == other.vertId2 && vertId1 == other.vertId0 && vertId2 == other.vertId1); + } + + public override int GetHashCode() + { + // 10 bits for each id, ordered by size, beyond which collisions will occur. + int[] verts = { vertId0, vertId1, vertId2 }; + Array.Sort(verts); + return verts[2] << 20 + verts[1] << 10 + verts[0]; + } } - public override int GetHashCode() + // Defines an extension to linked list to allow circular next operation and circular previous operation. + public static class CircularLinkedList { - // 10 bits for each id, ordered by size, beyond which collisions will occur. - int[] verts = {vertId0, vertId1, vertId2}; - Array.Sort(verts); - return verts[2] << 20 + verts[1] << 10 + verts[0]; - } - } - - // Defines an extension to linked list to allow circular next operation and circular previous operation. - public static class CircularLinkedList { - public static LinkedListNode CircularNext( - this LinkedListNode current) { - if (current == current.List.Last) { - return current.List.First; - } else { - return current.Next; - } - } - public static LinkedListNode CircularPrevious( - this LinkedListNode current) { - if (current == current.List.First) { - return current.List.Last; - } else { - return current.Previous; - } + public static LinkedListNode CircularNext( + this LinkedListNode current) + { + if (current == current.List.Last) + { + return current.List.First; + } + else + { + return current.Next; + } + } + public static LinkedListNode CircularPrevious( + this LinkedListNode current) + { + if (current == current.List.First) + { + return current.List.Last; + } + else + { + return current.Previous; + } + } } - } - public class FaceTriangulator { - - private struct HoleInfo { - public LinkedList vertList; - public LinkedListNode bestCandidate; - public Vector3 bestPos; - public float bestMagnitude; - } + public class FaceTriangulator + { - /// - /// Triangulate an entire mesh and return all resulting triangles. - /// - /// The mesh to triangulate. - /// List of triangles that represent the same geometry as the given mesh. - public static List TriangulateMesh(MMesh mesh) { - List geometry = new List(); - foreach (Face f in mesh.GetFaces()) { - geometry.AddRange(f.GetTriangulation(mesh)); - } - return geometry; - } + private struct HoleInfo + { + public LinkedList vertList; + public LinkedListNode bestCandidate; + public Vector3 bestPos; + public float bestMagnitude; + } - /// - /// Triangulates the given face of the given mesh. - /// - /// The mesh to which the face belongs. - /// The face to triangulate. - /// The list of triangles that represents the geometry of the face. - public static List TriangulateFace(MMesh mesh, Face face) { - List border = new List(face.vertexIds.Count); - for (int i = 0; i < face.vertexIds.Count; i++) { - border.Add(mesh.GetVertex(face.vertexIds[i])); - } - return Triangulate(border); - } - - /// - /// Triangulates the given face of the given mesh. - /// - /// The mesh to which the face belongs. - /// The face to triangulate. - /// The list of triangles that represents the geometry of the face. - public static List TriangulateFace(MMesh.GeometryOperation operation, Face face) { - - List border = new List(face.vertexIds.Count); - for (int i = 0; i < face.vertexIds.Count; i++) { - border.Add(operation.GetCurrentVertex(face.vertexIds[i])); - } - return Triangulate(border); - } + /// + /// Triangulate an entire mesh and return all resulting triangles. + /// + /// The mesh to triangulate. + /// List of triangles that represent the same geometry as the given mesh. + public static List TriangulateMesh(MMesh mesh) + { + List geometry = new List(); + foreach (Face f in mesh.GetFaces()) + { + geometry.AddRange(f.GetTriangulation(mesh)); + } + return geometry; + } - /// - /// Triangulate a polygon with holes. The current implementation is a somewhat slow O(n^2) - /// ear-clipping algorithm due to its straight-forward implementation. - /// - /// Outside of the polygon -- in clockwise order. - /// List of triangles that fully cover the area defined by the border on the - /// outside and the holes on the inside. - public static List Triangulate(List border) { - List triangles = new List(border.Count); - - // Assuming clockwise wind, take the plane normal of the first three vertices - // to determine intended face direction. - Vector3 faceNormal = MeshMath.CalculateNormal(border); - - // Store remaining vertices in a pseudo-circular linked list. - LinkedList remaining = new LinkedList(border); - - - // Initialize the three vertices to check for ears. - LinkedListNode prev = remaining.Last; - LinkedListNode current = remaining.First; - LinkedListNode next = current.Next; - - bool noMoreEars = true; - while (remaining.Count > 2) { - if (Math3d.IsConvex(current.Value.loc, prev.Value.loc, next.Value.loc, faceNormal)) { - bool cut = true; - // Check that none of the remaining vertices are inside. - LinkedListNode check = next.CircularNext(); - while (check != prev) { - // Because of hole patching, the same vertex might be in multiple places in the list. - // We can safely skip repetitions of the three points. - if (check.Value.loc == prev.Value.loc || - check.Value.loc == current.Value.loc || - check.Value.loc == next.Value.loc) { - check = check.CircularNext(); - continue; + /// + /// Triangulates the given face of the given mesh. + /// + /// The mesh to which the face belongs. + /// The face to triangulate. + /// The list of triangles that represents the geometry of the face. + public static List TriangulateFace(MMesh mesh, Face face) + { + List border = new List(face.vertexIds.Count); + for (int i = 0; i < face.vertexIds.Count; i++) + { + border.Add(mesh.GetVertex(face.vertexIds[i])); } - if (Math3d.TriangleContainsPoint(prev.Value.loc, current.Value.loc, next.Value.loc, check.Value.loc)) { - cut = false; - break; + return Triangulate(border); + } + + /// + /// Triangulates the given face of the given mesh. + /// + /// The mesh to which the face belongs. + /// The face to triangulate. + /// The list of triangles that represents the geometry of the face. + public static List TriangulateFace(MMesh.GeometryOperation operation, Face face) + { + + List border = new List(face.vertexIds.Count); + for (int i = 0; i < face.vertexIds.Count; i++) + { + border.Add(operation.GetCurrentVertex(face.vertexIds[i])); } - check = check.CircularNext(); - } - - if (cut) { - noMoreEars = false; - triangles.Add(new Triangle(prev.Value.id, current.Value.id, next.Value.id)); - remaining.Remove(current); - current = next; - next = next.CircularNext(); - - // Skip update logic which was handled above. - continue; - } + return Triangulate(border); } - // Update node pointers. - prev = current; - current = next; - next = next.CircularNext(); - - // If looped around and no ear was cut, fall back to simple. convex implementation when ear-clipping fails. - // NOTE: This should never happen provided we never have non-coplanar-faces, but we're just playing safe. - if (current == remaining.First) { - if (noMoreEars) { - // Fall back to simple. convex implementation when ear-clipping fails. - triangles = new List(border.Count); - for (int i = 2; i < border.Count; i++) { - triangles.Add(new Triangle( - border[0].id, - border[i - 1].id, - border[i].id)); + /// + /// Triangulate a polygon with holes. The current implementation is a somewhat slow O(n^2) + /// ear-clipping algorithm due to its straight-forward implementation. + /// + /// Outside of the polygon -- in clockwise order. + /// List of triangles that fully cover the area defined by the border on the + /// outside and the holes on the inside. + public static List Triangulate(List border) + { + List triangles = new List(border.Count); + + // Assuming clockwise wind, take the plane normal of the first three vertices + // to determine intended face direction. + Vector3 faceNormal = MeshMath.CalculateNormal(border); + + // Store remaining vertices in a pseudo-circular linked list. + LinkedList remaining = new LinkedList(border); + + + // Initialize the three vertices to check for ears. + LinkedListNode prev = remaining.Last; + LinkedListNode current = remaining.First; + LinkedListNode next = current.Next; + + bool noMoreEars = true; + while (remaining.Count > 2) + { + if (Math3d.IsConvex(current.Value.loc, prev.Value.loc, next.Value.loc, faceNormal)) + { + bool cut = true; + // Check that none of the remaining vertices are inside. + LinkedListNode check = next.CircularNext(); + while (check != prev) + { + // Because of hole patching, the same vertex might be in multiple places in the list. + // We can safely skip repetitions of the three points. + if (check.Value.loc == prev.Value.loc || + check.Value.loc == current.Value.loc || + check.Value.loc == next.Value.loc) + { + check = check.CircularNext(); + continue; + } + if (Math3d.TriangleContainsPoint(prev.Value.loc, current.Value.loc, next.Value.loc, check.Value.loc)) + { + cut = false; + break; + } + check = check.CircularNext(); + } + + if (cut) + { + noMoreEars = false; + triangles.Add(new Triangle(prev.Value.id, current.Value.id, next.Value.id)); + remaining.Remove(current); + current = next; + next = next.CircularNext(); + + // Skip update logic which was handled above. + continue; + } + } + + // Update node pointers. + prev = current; + current = next; + next = next.CircularNext(); + + // If looped around and no ear was cut, fall back to simple. convex implementation when ear-clipping fails. + // NOTE: This should never happen provided we never have non-coplanar-faces, but we're just playing safe. + if (current == remaining.First) + { + if (noMoreEars) + { + // Fall back to simple. convex implementation when ear-clipping fails. + triangles = new List(border.Count); + for (int i = 2; i < border.Count; i++) + { + triangles.Add(new Triangle( + border[0].id, + border[i - 1].id, + border[i].id)); + } + return triangles; + } + else + { + noMoreEars = true; + } + } } return triangles; - } else { - noMoreEars = true; - } } - } - return triangles; - } - public static bool RemoveHoleAtVertex( - LinkedList border, LinkedList hole, Vertex bestCandidate, - Vector3 faceNormal, Vector3? origin = null, Vector3? axis = null) { - // Initialize defaults for optional parameters. - LinkedListNode bestNode = hole.Find(bestCandidate); - Vector3 bestPos = bestCandidate.loc; - if (origin == null || axis == null) { - origin = bestPos; - // Take as axis the normal of the vertex to minimize chance of occlusion. - Vector3 prevPos = bestNode.Previous.Value.loc; - Vector3 nextPos = bestNode.Next.Value.loc; - axis = Vector3.Lerp(prevPos - bestPos, nextPos - bestPos, 0.5f); - // If the vertex on the hole is concave, it's concave wrt the polygon and the axis - // will be pointing "inward", away from the border. Flip it. - if (!Math3d.IsConvex( - bestPos, - prevPos, - nextPos, - faceNormal)) { - axis *= -1; - } - } - - // Find first visible point on arbitrary axis from chosen point in hole. - // This is probably by far the most computationally intense part of the algorithm. - LinkedListNode start = border.First; - LinkedListNode end = start.CircularNext(); - LinkedListNode bestOnBorder = null; - float bestMagnitude = float.MaxValue; - Vector3 intersectionPos = Vector3.zero; - do { - // Find intersection projection on axis - Vector3 a, b; - Vector3 startVec = start.Value.loc; - Vector3 edge = end.Value.loc - startVec; - bool intersecting = - Math3d.ClosestPointsOnTwoLines(out a, out b, startVec, edge, bestPos, axis.Value) && - a == b; - - // Record whether intersection occured in the "forward" direction on the ray cast from - // bestOnHole. - bool forward = Vector3.Dot(b - bestPos, axis.Value) > 0; - - if (intersecting && forward) { - // The two points are assumed to be the same but b is technically the one that should be - // used. - float coordVal = Vector3.Project(b - origin.Value, axis.Value).magnitude; - if (coordVal < bestMagnitude) { - intersectionPos = b; - bestMagnitude = coordVal; - // Choose as "best" the vertex that is futher on the axis - // (to assure no visibility blockage wrt hole since vertex in hole - // was chosen with maximal on-axis projection magnitude) - if (Vector3.Project(start.Value.loc - origin.Value, axis.Value).magnitude > - Vector3.Project(end.Value.loc - origin.Value, axis.Value).magnitude) { - bestOnBorder = start; - } else { - bestOnBorder = end; + public static bool RemoveHoleAtVertex( + LinkedList border, LinkedList hole, Vertex bestCandidate, + Vector3 faceNormal, Vector3? origin = null, Vector3? axis = null) + { + // Initialize defaults for optional parameters. + LinkedListNode bestNode = hole.Find(bestCandidate); + Vector3 bestPos = bestCandidate.loc; + if (origin == null || axis == null) + { + origin = bestPos; + // Take as axis the normal of the vertex to minimize chance of occlusion. + Vector3 prevPos = bestNode.Previous.Value.loc; + Vector3 nextPos = bestNode.Next.Value.loc; + axis = Vector3.Lerp(prevPos - bestPos, nextPos - bestPos, 0.5f); + // If the vertex on the hole is concave, it's concave wrt the polygon and the axis + // will be pointing "inward", away from the border. Flip it. + if (!Math3d.IsConvex( + bestPos, + prevPos, + nextPos, + faceNormal)) + { + axis *= -1; + } } - } - } - // Update to next pair. - start = end; - end = end.CircularNext(); - } while (start != border.First); - - if (bestOnBorder == null) { - return false; - } - - // Finally, confirm visibility from chosen best vertices by checking other reflex vertices on - // the border are not contained - LinkedListNode onBorder = bestOnBorder.CircularNext(); - LinkedListNode occludingBestOnBorder = null; - double bestAngle = Math.PI * 2; - while (onBorder != bestOnBorder) { - bool convex = Math3d.IsConvex( - onBorder.Value.loc, - onBorder.CircularPrevious().Value.loc, - onBorder.CircularNext().Value.loc, - faceNormal); - if (!convex && - Math3d.TriangleContainsPoint( - bestPos, intersectionPos, - bestOnBorder.Value.loc, - onBorder.Value.loc)) { - // Find angle between intersection line and occluding vertex line. - Vector3 intersectionVec = intersectionPos - bestPos; - Vector3 occludingVertexVec = onBorder.Value.loc - bestPos; - float angle = Vector3.Dot(intersectionVec, occludingVertexVec); - if (angle < bestAngle) { - occludingBestOnBorder = onBorder; - bestAngle = angle; - } - } + // Find first visible point on arbitrary axis from chosen point in hole. + // This is probably by far the most computationally intense part of the algorithm. + LinkedListNode start = border.First; + LinkedListNode end = start.CircularNext(); + LinkedListNode bestOnBorder = null; + float bestMagnitude = float.MaxValue; + Vector3 intersectionPos = Vector3.zero; + do + { + // Find intersection projection on axis + Vector3 a, b; + Vector3 startVec = start.Value.loc; + Vector3 edge = end.Value.loc - startVec; + bool intersecting = + Math3d.ClosestPointsOnTwoLines(out a, out b, startVec, edge, bestPos, axis.Value) && + a == b; + + // Record whether intersection occured in the "forward" direction on the ray cast from + // bestOnHole. + bool forward = Vector3.Dot(b - bestPos, axis.Value) > 0; + + if (intersecting && forward) + { + // The two points are assumed to be the same but b is technically the one that should be + // used. + float coordVal = Vector3.Project(b - origin.Value, axis.Value).magnitude; + if (coordVal < bestMagnitude) + { + intersectionPos = b; + bestMagnitude = coordVal; + // Choose as "best" the vertex that is futher on the axis + // (to assure no visibility blockage wrt hole since vertex in hole + // was chosen with maximal on-axis projection magnitude) + if (Vector3.Project(start.Value.loc - origin.Value, axis.Value).magnitude > + Vector3.Project(end.Value.loc - origin.Value, axis.Value).magnitude) + { + bestOnBorder = start; + } + else + { + bestOnBorder = end; + } + } + } + + // Update to next pair. + start = end; + end = end.CircularNext(); + } while (start != border.First); + + if (bestOnBorder == null) + { + return false; + } - onBorder = onBorder.CircularNext(); - } - - // Update bestOnBorder with the actual best if occluding vertex was found. - if (occludingBestOnBorder != null) { - bestOnBorder = occludingBestOnBorder; - } - - // Add hole to border by cutting the shape. - LinkedListNode terminalOnBorder = bestOnBorder.CircularNext(); - LinkedListNode onHole = bestNode; - do { - // Holes are wound counter-clockwise so adding them in order is appropriate to preserve - // overall polygon clockwise winding. - border.AddBefore(terminalOnBorder, onHole.Value); - onHole = onHole.CircularNext(); - } while (onHole != bestNode); - // Two additional vertices are added to complete loop. - border.AddBefore(terminalOnBorder, bestCandidate); - border.AddBefore(terminalOnBorder, bestOnBorder.Value); - - return true; - } + // Finally, confirm visibility from chosen best vertices by checking other reflex vertices on + // the border are not contained + LinkedListNode onBorder = bestOnBorder.CircularNext(); + LinkedListNode occludingBestOnBorder = null; + double bestAngle = Math.PI * 2; + while (onBorder != bestOnBorder) + { + bool convex = Math3d.IsConvex( + onBorder.Value.loc, + onBorder.CircularPrevious().Value.loc, + onBorder.CircularNext().Value.loc, + faceNormal); + if (!convex && + Math3d.TriangleContainsPoint( + bestPos, intersectionPos, + bestOnBorder.Value.loc, + onBorder.Value.loc)) + { + // Find angle between intersection line and occluding vertex line. + Vector3 intersectionVec = intersectionPos - bestPos; + Vector3 occludingVertexVec = onBorder.Value.loc - bestPos; + float angle = Vector3.Dot(intersectionVec, occludingVertexVec); + if (angle < bestAngle) + { + occludingBestOnBorder = onBorder; + bestAngle = angle; + } + } + + onBorder = onBorder.CircularNext(); + } + + // Update bestOnBorder with the actual best if occluding vertex was found. + if (occludingBestOnBorder != null) + { + bestOnBorder = occludingBestOnBorder; + } - public static bool RemoveHoles( - LinkedList border, List> holes, Vector3 faceNormal) { - if (holes == null || holes.Count == 0) { - return false; - } - - // Pick an arbitrary vertex as origin and edge as direction - Vector3 origin = border.First.Value.loc; - Vector3 axis = border.First.Next.Value.loc - origin; - - List holeInfos = new List(); - - // Record useful meta about each hole. - foreach (List hole in holes) { - // Create a list of hole vertices that will later be connected to main list. - HoleInfo holeInfo; - holeInfo.vertList = new LinkedList(hole); - LinkedListNode holeCurrent = holeInfo.vertList.First; - holeInfo.bestCandidate = null; - holeInfo.bestMagnitude = float.MinValue; - - // Find maximal offset on the previously chosen arbitrary axis. - while (holeCurrent != null) { - float coordVal = Vector3.Project(holeCurrent.Value.loc - origin, axis).magnitude; - if (coordVal > holeInfo.bestMagnitude) { - holeInfo.bestMagnitude = coordVal; - holeInfo.bestCandidate = holeCurrent; - } - holeCurrent = holeCurrent.Next; + // Add hole to border by cutting the shape. + LinkedListNode terminalOnBorder = bestOnBorder.CircularNext(); + LinkedListNode onHole = bestNode; + do + { + // Holes are wound counter-clockwise so adding them in order is appropriate to preserve + // overall polygon clockwise winding. + border.AddBefore(terminalOnBorder, onHole.Value); + onHole = onHole.CircularNext(); + } while (onHole != bestNode); + // Two additional vertices are added to complete loop. + border.AddBefore(terminalOnBorder, bestCandidate); + border.AddBefore(terminalOnBorder, bestOnBorder.Value); + + return true; } - holeInfo.bestPos = holeInfo.bestCandidate.Value.loc; - holeInfos.Add(holeInfo); - } + public static bool RemoveHoles( + LinkedList border, List> holes, Vector3 faceNormal) + { + if (holes == null || holes.Count == 0) + { + return false; + } + + // Pick an arbitrary vertex as origin and edge as direction + Vector3 origin = border.First.Value.loc; + Vector3 axis = border.First.Next.Value.loc - origin; + + List holeInfos = new List(); + + // Record useful meta about each hole. + foreach (List hole in holes) + { + // Create a list of hole vertices that will later be connected to main list. + HoleInfo holeInfo; + holeInfo.vertList = new LinkedList(hole); + LinkedListNode holeCurrent = holeInfo.vertList.First; + holeInfo.bestCandidate = null; + holeInfo.bestMagnitude = float.MinValue; + + // Find maximal offset on the previously chosen arbitrary axis. + while (holeCurrent != null) + { + float coordVal = Vector3.Project(holeCurrent.Value.loc - origin, axis).magnitude; + if (coordVal > holeInfo.bestMagnitude) + { + holeInfo.bestMagnitude = coordVal; + holeInfo.bestCandidate = holeCurrent; + } + holeCurrent = holeCurrent.Next; + } + + holeInfo.bestPos = holeInfo.bestCandidate.Value.loc; + holeInfos.Add(holeInfo); + } - // Order holes by largest magnitude in arbitrary axis. - holeInfos = holeInfos.OrderByDescending(h => h.bestMagnitude).ToList(); + // Order holes by largest magnitude in arbitrary axis. + holeInfos = holeInfos.OrderByDescending(h => h.bestMagnitude).ToList(); - foreach (HoleInfo hole in holeInfos) { - RemoveHoleAtVertex(border, hole.vertList, hole.bestCandidate.Value, faceNormal, origin, - axis); - } - return true; + foreach (HoleInfo hole in holeInfos) + { + RemoveHoleAtVertex(border, hole.vertList, hole.bestCandidate.Value, faceNormal, origin, + axis); + } + return true; + } } - } } diff --git a/Assets/Scripts/model/render/MaterialAndColor.cs b/Assets/Scripts/model/render/MaterialAndColor.cs index 4599db35..9dc204d5 100644 --- a/Assets/Scripts/model/render/MaterialAndColor.cs +++ b/Assets/Scripts/model/render/MaterialAndColor.cs @@ -14,47 +14,55 @@ using UnityEngine; -namespace com.google.apps.peltzer.client.model.render { - public class MaterialAndColor { - // This is necessary in order to change to a different version of the material - ie, when we need to use a preview - // shader instead of the regular shader. This allows us to look up the correct replacement material and properly - // handle exception materials such as Glass and Gem. Ideally at some point we retain references in classes to - // only matId and look up MaterialAndColor on demand in order to avoid sync bugs. Until we can refactor to that, - // material and color should instead be treated as canonical and matId used only when change-of-shader is required - // for effects. - public int matId; - public Material material; - // This material is to handle an edge case when a material needs two passes which need to be in different spots of - // the renderqueue (Glass, at the moment). We should abstract this away into something more elegant later. - public Material material2; - public Color32 color; +namespace com.google.apps.peltzer.client.model.render +{ + public class MaterialAndColor + { + // This is necessary in order to change to a different version of the material - ie, when we need to use a preview + // shader instead of the regular shader. This allows us to look up the correct replacement material and properly + // handle exception materials such as Glass and Gem. Ideally at some point we retain references in classes to + // only matId and look up MaterialAndColor on demand in order to avoid sync bugs. Until we can refactor to that, + // material and color should instead be treated as canonical and matId used only when change-of-shader is required + // for effects. + public int matId; + public Material material; + // This material is to handle an edge case when a material needs two passes which need to be in different spots of + // the renderqueue (Glass, at the moment). We should abstract this away into something more elegant later. + public Material material2; + public Color32 color; - public MaterialAndColor(Material material, int id) { - this.material = material; - this.color = new Color32(255, 255, 255, 255); - this.matId = id; - } + public MaterialAndColor(Material material, int id) + { + this.material = material; + this.color = new Color32(255, 255, 255, 255); + this.matId = id; + } - public MaterialAndColor(Material material, Color32 color, int id) { - this.material = material; - this.color = color; - this.matId = id; - } - - public MaterialAndColor(Material material, Material material2, Color32 color, int id) { - this.material = material; - this.material2 = material2; - this.color = color; - this.matId = id; - } + public MaterialAndColor(Material material, Color32 color, int id) + { + this.material = material; + this.color = color; + this.matId = id; + } + + public MaterialAndColor(Material material, Material material2, Color32 color, int id) + { + this.material = material; + this.material2 = material2; + this.color = color; + this.matId = id; + } - public MaterialAndColor Clone() { - if (material2 != null) { - return new MaterialAndColor(new Material(material), new Material(material2), color, matId); - } - else { - return new MaterialAndColor(new Material(material), color, matId); - } + public MaterialAndColor Clone() + { + if (material2 != null) + { + return new MaterialAndColor(new Material(material), new Material(material2), color, matId); + } + else + { + return new MaterialAndColor(new Material(material), color, matId); + } + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/render/MaterialRegistry.cs b/Assets/Scripts/model/render/MaterialRegistry.cs index b103d6e8..15bd130e 100644 --- a/Assets/Scripts/model/render/MaterialRegistry.cs +++ b/Assets/Scripts/model/render/MaterialRegistry.cs @@ -14,19 +14,22 @@ using UnityEngine; -namespace com.google.apps.peltzer.client.model.render { - /// - /// A place to get materials by id. - /// - public class MaterialRegistry { - public enum MaterialType { - PAPER, - GEM, - GLASS - } +namespace com.google.apps.peltzer.client.model.render +{ + /// + /// A place to get materials by id. + /// + public class MaterialRegistry + { + public enum MaterialType + { + PAPER, + GEM, + GLASS + } - // Taken from UI docs, starting at: go/oblinks - public static int[] rawColors = { + // Taken from UI docs, starting at: go/oblinks + public static int[] rawColors = { 0xBA68C8, 0x9C27B0, 0x673AB7, @@ -53,232 +56,260 @@ public enum MaterialType { 0x1A1A1A, }; - // Unity Materials with albedo set. - private static Material[] materialsWithAlbedo = null; + // Unity Materials with albedo set. + private static Material[] materialsWithAlbedo = null; - // Our custom MaterialAndColor, with albedo unset, as we use vertex colours. - private static MaterialAndColor[] materials = null; - private static MaterialAndColor[] previewMaterials = null; - private static MaterialAndColor[] highlightMaterials = null; - private static Color32[] color32s = null; + // Our custom MaterialAndColor, with albedo unset, as we use vertex colours. + private static MaterialAndColor[] materials = null; + private static MaterialAndColor[] previewMaterials = null; + private static MaterialAndColor[] highlightMaterials = null; + private static Color32[] color32s = null; - private static MaterialAndColor highlightSilhouetteMaterial; + private static MaterialAndColor highlightSilhouetteMaterial; - public static int GLASS_ID = rawColors.Length; - public static int GEM_ID = rawColors.Length + 1; - public static int PINK_WIREFRAME_ID = rawColors.Length + 2; - public static int GREEN_WIREFRAME_ID = rawColors.Length + 3; - public static int HIGHLIGHT_SILHOUETTE_ID = rawColors.Length + 4; - - public static readonly int RED_ID = 8; - public static readonly int DEEP_ORANGE_ID = 14; - public static readonly int YELLOW_ID = 12; - public static readonly int WHITE_ID = 21; - public static readonly int BLACK_ID = 23; + public static int GLASS_ID = rawColors.Length; + public static int GEM_ID = rawColors.Length + 1; + public static int PINK_WIREFRAME_ID = rawColors.Length + 2; + public static int GREEN_WIREFRAME_ID = rawColors.Length + 3; + public static int HIGHLIGHT_SILHOUETTE_ID = rawColors.Length + 4; - private static readonly string BASE_SHADER = "Mogwai/DirectionalPaperUniform"; - private static readonly string PREVIEW_SHADER = "Mogwai/DirectionalTransparent"; + public static readonly int RED_ID = 8; + public static readonly int DEEP_ORANGE_ID = 14; + public static readonly int YELLOW_ID = 12; + public static readonly int WHITE_ID = 21; + public static readonly int BLACK_ID = 23; - /// - /// Must be called before used. Creates the materials for the given color codes. - /// - public static void init(MaterialLibrary materialLibrary) { - Material baseMaterial = materialLibrary.baseMaterial; - Material glassMaterial = materialLibrary.glassMaterial; - Material gemMaterial = materialLibrary.gemMaterial; - Material subtractMaterial = materialLibrary.subtractMaterial; - Material copyMaterial = materialLibrary.copyMaterial; - materials = new MaterialAndColor[rawColors.Length + 4]; - materialsWithAlbedo = new Material[rawColors.Length + 4]; - previewMaterials = new MaterialAndColor[rawColors.Length + 4]; - color32s = new Color32[rawColors.Length + 4]; - Material templateMaterial = - baseMaterial == null ? new Material(Shader.Find(BASE_SHADER)) : new Material(baseMaterial); - for (int i = 0; i < rawColors.Length; i++) { - materials[i] = new MaterialAndColor(templateMaterial, i); - materials[i].color = new Color(r(rawColors[i]), g(rawColors[i]), b(rawColors[i])); - color32s[i] = materials[i].color; - materialsWithAlbedo[i] = new Material(Shader.Find(BASE_SHADER)); - materialsWithAlbedo[i].color = new Color(r(rawColors[i]), g(rawColors[i]), b(rawColors[i])); - previewMaterials[i] = new MaterialAndColor(new Material(Shader.Find(PREVIEW_SHADER)), i); - previewMaterials[i].color = new Color(r(rawColors[i]), g(rawColors[i]), b(rawColors[i]), /* alpha */ 1.0f); - previewMaterials[i].material.SetFloat("_MultiplicitiveAlpha", 0.3f); - } - // "Special" materials. - materials[GLASS_ID] = new MaterialAndColor(glassMaterial, materialLibrary.glassSpecMaterial, glassMaterial.color, GLASS_ID); - materialsWithAlbedo[GLASS_ID] = glassMaterial; - materials[GEM_ID] = new MaterialAndColor(gemMaterial, GEM_ID); - materialsWithAlbedo[GEM_ID] = gemMaterial; - materials[PINK_WIREFRAME_ID] = new MaterialAndColor(subtractMaterial, PINK_WIREFRAME_ID); - materialsWithAlbedo[PINK_WIREFRAME_ID] = subtractMaterial; - materials[GREEN_WIREFRAME_ID] = new MaterialAndColor(copyMaterial, GREEN_WIREFRAME_ID); - materialsWithAlbedo[GREEN_WIREFRAME_ID] = copyMaterial; + private static readonly string BASE_SHADER = "Mogwai/DirectionalPaperUniform"; + private static readonly string PREVIEW_SHADER = "Mogwai/DirectionalTransparent"; - previewMaterials[GLASS_ID] = new MaterialAndColor(new Material(glassMaterial), GLASS_ID); - previewMaterials[GEM_ID] = new MaterialAndColor(new Material(gemMaterial), GEM_ID); + /// + /// Must be called before used. Creates the materials for the given color codes. + /// + public static void init(MaterialLibrary materialLibrary) + { + Material baseMaterial = materialLibrary.baseMaterial; + Material glassMaterial = materialLibrary.glassMaterial; + Material gemMaterial = materialLibrary.gemMaterial; + Material subtractMaterial = materialLibrary.subtractMaterial; + Material copyMaterial = materialLibrary.copyMaterial; + materials = new MaterialAndColor[rawColors.Length + 4]; + materialsWithAlbedo = new Material[rawColors.Length + 4]; + previewMaterials = new MaterialAndColor[rawColors.Length + 4]; + color32s = new Color32[rawColors.Length + 4]; + Material templateMaterial = + baseMaterial == null ? new Material(Shader.Find(BASE_SHADER)) : new Material(baseMaterial); + for (int i = 0; i < rawColors.Length; i++) + { + materials[i] = new MaterialAndColor(templateMaterial, i); + materials[i].color = new Color(r(rawColors[i]), g(rawColors[i]), b(rawColors[i])); + color32s[i] = materials[i].color; + materialsWithAlbedo[i] = new Material(Shader.Find(BASE_SHADER)); + materialsWithAlbedo[i].color = new Color(r(rawColors[i]), g(rawColors[i]), b(rawColors[i])); + previewMaterials[i] = new MaterialAndColor(new Material(Shader.Find(PREVIEW_SHADER)), i); + previewMaterials[i].color = new Color(r(rawColors[i]), g(rawColors[i]), b(rawColors[i]), /* alpha */ 1.0f); + previewMaterials[i].material.SetFloat("_MultiplicitiveAlpha", 0.3f); + } + // "Special" materials. + materials[GLASS_ID] = new MaterialAndColor(glassMaterial, materialLibrary.glassSpecMaterial, glassMaterial.color, GLASS_ID); + materialsWithAlbedo[GLASS_ID] = glassMaterial; + materials[GEM_ID] = new MaterialAndColor(gemMaterial, GEM_ID); + materialsWithAlbedo[GEM_ID] = gemMaterial; + materials[PINK_WIREFRAME_ID] = new MaterialAndColor(subtractMaterial, PINK_WIREFRAME_ID); + materialsWithAlbedo[PINK_WIREFRAME_ID] = subtractMaterial; + materials[GREEN_WIREFRAME_ID] = new MaterialAndColor(copyMaterial, GREEN_WIREFRAME_ID); + materialsWithAlbedo[GREEN_WIREFRAME_ID] = copyMaterial; - Color old = previewMaterials[GEM_ID].color; - previewMaterials[GEM_ID].color = new Color(old.r, old.g, old.b, 0.1f); - highlightMaterials = new MaterialAndColor[materials.Length]; - for (int i = 0; i < materials.Length; i++) { - MaterialAndColor highlightedVersion = new MaterialAndColor(materials[i].material, i); - Color32 highlightedVersionColor = highlightedVersion.color; - Color originalColor = new Color(highlightedVersionColor.r, highlightedVersionColor.g, highlightedVersionColor.b, highlightedVersionColor.a); - highlightedVersion.color = originalColor * (4.5f - originalColor.maxColorComponent * 3); - highlightMaterials[i] = highlightedVersion; - } - highlightSilhouetteMaterial = new MaterialAndColor(materialLibrary.highlightSilhouetteMaterial, - new Color32(255, 255, 255, 255), HIGHLIGHT_SILHOUETTE_ID); - } + previewMaterials[GLASS_ID] = new MaterialAndColor(new Material(glassMaterial), GLASS_ID); + previewMaterials[GEM_ID] = new MaterialAndColor(new Material(gemMaterial), GEM_ID); - private static float r(int raw) { - return ((raw >> 16) & 255) / 255.0f; - } + Color old = previewMaterials[GEM_ID].color; + previewMaterials[GEM_ID].color = new Color(old.r, old.g, old.b, 0.1f); + highlightMaterials = new MaterialAndColor[materials.Length]; + for (int i = 0; i < materials.Length; i++) + { + MaterialAndColor highlightedVersion = new MaterialAndColor(materials[i].material, i); + Color32 highlightedVersionColor = highlightedVersion.color; + Color originalColor = new Color(highlightedVersionColor.r, highlightedVersionColor.g, highlightedVersionColor.b, highlightedVersionColor.a); + highlightedVersion.color = originalColor * (4.5f - originalColor.maxColorComponent * 3); + highlightMaterials[i] = highlightedVersion; + } + highlightSilhouetteMaterial = new MaterialAndColor(materialLibrary.highlightSilhouetteMaterial, + new Color32(255, 255, 255, 255), HIGHLIGHT_SILHOUETTE_ID); + } - private static float g(int raw) { - return ((raw >> 8) & 255) / 255.0f; - } + private static float r(int raw) + { + return ((raw >> 16) & 255) / 255.0f; + } - private static float b(int raw) { - return (raw & 255) / 255.0f; - } + private static float g(int raw) + { + return ((raw >> 8) & 255) / 255.0f; + } - /// - /// Get a MaterialAndColor given a materialId. - /// - /// The material id. - /// A Material. - public static MaterialAndColor GetMaterialAndColorById(int materialId) { - // For tests, if we haven't been initialized, do it now. - if (materials == null) { - MaterialLibrary matLib = new MaterialLibrary(); - matLib.glassMaterial = new Material(Shader.Find(PREVIEW_SHADER)); - matLib.glassSpecMaterial = new Material(Shader.Find(PREVIEW_SHADER)); - matLib.gemMaterial = new Material(Shader.Find(PREVIEW_SHADER)); - matLib.copyMaterial =new Material(Shader.Find(BASE_SHADER)); - matLib.subtractMaterial = new Material(Shader.Find(BASE_SHADER)); - Debug.Log("initializing mats in wrong place - this is an error if a test isn't running."); - init(matLib); - } - return materials[materialId % materials.Length]; - } + private static float b(int raw) + { + return (raw & 255) / 255.0f; + } - /// - /// Get a Material's color given a materialId. - /// - /// The material id. - /// A Color. - public static Color GetMaterialColorById(int materialId) { - if (materialId < rawColors.Length) { - return new Color(r(rawColors[materialId]), g(rawColors[materialId]), b(rawColors[materialId])); - } - else { - return new Color(1f, 1f, 1f, 1f); - } - } - - /// - /// Get a Material's color given a materialId. - /// - /// The material id. - /// A Color. - public static Color32 GetMaterialColor32ById(int materialId) { - if (materialId < rawColors.Length) { - return color32s[materialId]; - } else { - return new Color(1f, 1f, 1f, 1f); - } - } + /// + /// Get a MaterialAndColor given a materialId. + /// + /// The material id. + /// A Material. + public static MaterialAndColor GetMaterialAndColorById(int materialId) + { + // For tests, if we haven't been initialized, do it now. + if (materials == null) + { + MaterialLibrary matLib = new MaterialLibrary(); + matLib.glassMaterial = new Material(Shader.Find(PREVIEW_SHADER)); + matLib.glassSpecMaterial = new Material(Shader.Find(PREVIEW_SHADER)); + matLib.gemMaterial = new Material(Shader.Find(PREVIEW_SHADER)); + matLib.copyMaterial = new Material(Shader.Find(BASE_SHADER)); + matLib.subtractMaterial = new Material(Shader.Find(BASE_SHADER)); + Debug.Log("initializing mats in wrong place - this is an error if a test isn't running."); + init(matLib); + } + return materials[materialId % materials.Length]; + } - /// - /// Get a Material, with albedo already set, given a materialId. - /// - /// The material id. - /// A Material with .color set. - public static Material GetMaterialWithAlbedoById(int materialId) { - return materialsWithAlbedo[materialId]; - } + /// + /// Get a Material's color given a materialId. + /// + /// The material id. + /// A Color. + public static Color GetMaterialColorById(int materialId) + { + if (materialId < rawColors.Length) + { + return new Color(r(rawColors[materialId]), g(rawColors[materialId]), b(rawColors[materialId])); + } + else + { + return new Color(1f, 1f, 1f, 1f); + } + } - /// - /// Get a preview version (low alpha) of a Material given a materialId. - /// - /// The material id. - /// A Material. - public static MaterialAndColor GetPreviewOfMaterialById(int materialId) { - // For tests, if we haven't been initialized, do it now. - if (materials == null) { - MaterialLibrary matLib = new MaterialLibrary(); - matLib.glassMaterial = new Material(Shader.Find(PREVIEW_SHADER)); - matLib.glassSpecMaterial = new Material(Shader.Find(PREVIEW_SHADER)); - matLib.gemMaterial = new Material(Shader.Find(PREVIEW_SHADER)); - matLib.copyMaterial =new Material(Shader.Find(BASE_SHADER)); - matLib.subtractMaterial = new Material(Shader.Find(BASE_SHADER)); - Debug.Log("initializing mats in wrong place - this is an error if a test isn't running."); - init(matLib); - } - return previewMaterials[materialId % materials.Length]; - } + /// + /// Get a Material's color given a materialId. + /// + /// The material id. + /// A Color. + public static Color32 GetMaterialColor32ById(int materialId) + { + if (materialId < rawColors.Length) + { + return color32s[materialId]; + } + else + { + return new Color(1f, 1f, 1f, 1f); + } + } - /// - /// Get the material we should show when a user is attempting to make an invalid reshape operation. - /// - /// A Material. - public static MaterialAndColor GetReshaperErrorMaterial() { - return previewMaterials[8]; // Red. - } + /// + /// Get a Material, with albedo already set, given a materialId. + /// + /// The material id. + /// A Material with .color set. + public static Material GetMaterialWithAlbedoById(int materialId) + { + return materialsWithAlbedo[materialId]; + } - /// - /// Gets a highlighted version of the material (brightened). - /// - /// The material id. - /// A material. - public static MaterialAndColor GetHighlightMaterialById(int materialId) { - // For tests, if we haven't been initialized, do it now. - if (highlightMaterials == null) { - MaterialLibrary matLib = new MaterialLibrary(); - matLib.glassMaterial = new Material(Shader.Find(PREVIEW_SHADER)); - matLib.glassSpecMaterial = new Material(Shader.Find(PREVIEW_SHADER)); - matLib.gemMaterial = new Material(Shader.Find(PREVIEW_SHADER)); - matLib.copyMaterial =new Material(Shader.Find(BASE_SHADER)); - matLib.subtractMaterial = new Material(Shader.Find(BASE_SHADER)); - Debug.Log("initializing mats in wrong place - this is an error if a test isn't running."); - init(matLib); - } - return highlightMaterials[materialId % highlightMaterials.Length]; - } + /// + /// Get a preview version (low alpha) of a Material given a materialId. + /// + /// The material id. + /// A Material. + public static MaterialAndColor GetPreviewOfMaterialById(int materialId) + { + // For tests, if we haven't been initialized, do it now. + if (materials == null) + { + MaterialLibrary matLib = new MaterialLibrary(); + matLib.glassMaterial = new Material(Shader.Find(PREVIEW_SHADER)); + matLib.glassSpecMaterial = new Material(Shader.Find(PREVIEW_SHADER)); + matLib.gemMaterial = new Material(Shader.Find(PREVIEW_SHADER)); + matLib.copyMaterial = new Material(Shader.Find(BASE_SHADER)); + matLib.subtractMaterial = new Material(Shader.Find(BASE_SHADER)); + Debug.Log("initializing mats in wrong place - this is an error if a test isn't running."); + init(matLib); + } + return previewMaterials[materialId % materials.Length]; + } - public static Material[] GetExportableMaterialList() { - return materialsWithAlbedo; - } + /// + /// Get the material we should show when a user is attempting to make an invalid reshape operation. + /// + /// A Material. + public static MaterialAndColor GetReshaperErrorMaterial() + { + return previewMaterials[8]; // Red. + } - public static MaterialAndColor getHighlightSilhouetteMaterial() { - return highlightSilhouetteMaterial; - } + /// + /// Gets a highlighted version of the material (brightened). + /// + /// The material id. + /// A material. + public static MaterialAndColor GetHighlightMaterialById(int materialId) + { + // For tests, if we haven't been initialized, do it now. + if (highlightMaterials == null) + { + MaterialLibrary matLib = new MaterialLibrary(); + matLib.glassMaterial = new Material(Shader.Find(PREVIEW_SHADER)); + matLib.glassSpecMaterial = new Material(Shader.Find(PREVIEW_SHADER)); + matLib.gemMaterial = new Material(Shader.Find(PREVIEW_SHADER)); + matLib.copyMaterial = new Material(Shader.Find(BASE_SHADER)); + matLib.subtractMaterial = new Material(Shader.Find(BASE_SHADER)); + Debug.Log("initializing mats in wrong place - this is an error if a test isn't running."); + init(matLib); + } + return highlightMaterials[materialId % highlightMaterials.Length]; + } - /// - /// Get the number of materials we support. - /// - /// - public static int GetNumMaterials() { - return materials.Length; - } + public static Material[] GetExportableMaterialList() + { + return materialsWithAlbedo; + } - /// - /// Returns true if the material is transparent. This may be needed to render things correctly. - /// - public static bool IsMaterialTransparent(int materialId) { - return materialId == GLASS_ID || materialId == GEM_ID; - } + public static MaterialAndColor getHighlightSilhouetteMaterial() + { + return highlightSilhouetteMaterial; + } + + /// + /// Get the number of materials we support. + /// + /// + public static int GetNumMaterials() + { + return materials.Length; + } + + /// + /// Returns true if the material is transparent. This may be needed to render things correctly. + /// + public static bool IsMaterialTransparent(int materialId) + { + return materialId == GLASS_ID || materialId == GEM_ID; + } - public static MaterialType GetMaterialType(int id) { - // Can't use a switch statement because GEM_ID and GLASS_ID aren't real constants. - if (id == GEM_ID) { - return MaterialType.GEM; - } - if (id == GLASS_ID) { - return MaterialType.GLASS; - } - return MaterialType.PAPER; + public static MaterialType GetMaterialType(int id) + { + // Can't use a switch statement because GEM_ID and GLASS_ID aren't real constants. + if (id == GEM_ID) + { + return MaterialType.GEM; + } + if (id == GLASS_ID) + { + return MaterialType.GLASS; + } + return MaterialType.PAPER; + } } - } } diff --git a/Assets/Scripts/model/render/MeshHelper.cs b/Assets/Scripts/model/render/MeshHelper.cs index 2bdb51ea..e6c54f7d 100644 --- a/Assets/Scripts/model/render/MeshHelper.cs +++ b/Assets/Scripts/model/render/MeshHelper.cs @@ -22,548 +22,623 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.model.render { - public class MeshGenContext { - public List verts = new List(); - public List triangles = new List(); - public List colors = new List(); - public List normals = new List(); - } - - /// - /// Helper methods for MeshRendering. - /// - public class MeshHelper { - private static readonly System.Random RANDOM = new System.Random(); - - /// - /// The material to be used for highlighted meshes. - /// - public static MaterialAndColor highlightSilhouetteMaterial; - /// - /// All rendered vertices will be perturbed by a random amount no greater than this constant, in all three - /// dimensions. This amount should be imperceptible. This is to mitigate the effects of z-fighting. - /// - private const float wiggleRoom = 0.00004f; - - /// - /// Gets the components (triangles, and verts, by triangulating) from an MMesh on a background thread, then calls - /// a given callback. - /// - private class GetComponentsFromMeshesWork : BackgroundWork { - List> output; - IEnumerable meshes; - bool useModelSpace; - Action>> callback; - - public GetComponentsFromMeshesWork(IEnumerable meshes, bool useModelSpace, - Action>> callback) { - this.meshes = meshes; - this.useModelSpace = useModelSpace; - this.callback = callback; - } - - public void BackgroundWork() { - output = MeshComponentsForMenu(meshes); - } - - public void PostWork() { - callback(output); - } +namespace com.google.apps.peltzer.client.model.render +{ + public class MeshGenContext + { + public List verts = new List(); + public List triangles = new List(); + public List colors = new List(); + public List normals = new List(); } /// - /// Creates a Unity Mesh that draws an MMesh. + /// Helper methods for MeshRendering. /// - /// The MMesh. - /// Whether to take model-space co-ordinates from the MMesh. - /// An override material to use for the entire generated Mesh, if desired. - /// The Unity Mesh. - public static List MeshFromMMesh(MMesh mmesh, bool useModelSpace, - out Dictionary components, MaterialAndColor materialOverride = null) { - components = MeshComponentsFromMMesh(mmesh, useModelSpace); - return ToMeshes(components, materialOverride); - } + public class MeshHelper + { + private static readonly System.Random RANDOM = new System.Random(); + + /// + /// The material to be used for highlighted meshes. + /// + public static MaterialAndColor highlightSilhouetteMaterial; + /// + /// All rendered vertices will be perturbed by a random amount no greater than this constant, in all three + /// dimensions. This amount should be imperceptible. This is to mitigate the effects of z-fighting. + /// + private const float wiggleRoom = 0.00004f; + + /// + /// Gets the components (triangles, and verts, by triangulating) from an MMesh on a background thread, then calls + /// a given callback. + /// + private class GetComponentsFromMeshesWork : BackgroundWork + { + List> output; + IEnumerable meshes; + bool useModelSpace; + Action>> callback; + + public GetComponentsFromMeshesWork(IEnumerable meshes, bool useModelSpace, + Action>> callback) + { + this.meshes = meshes; + this.useModelSpace = useModelSpace; + this.callback = callback; + } - /// - /// Generates the components of a Unity Mesh that represents a group of MMeshes, doing its work on the - /// background thread and calling the given callback once finished. - /// - private static void ComponentsFromMMeshesOnBackground(IEnumerable mmeshes, bool useModelSpace, - Action> callback) { - PeltzerMain.Instance.DoBackgroundWork(new GetComponentsFromMeshesWork(mmeshes, useModelSpace, - (List> output) => { - List allMeshes = new List(); - foreach (Dictionary setOfMeshes in output) { - allMeshes.AddRange(ToMeshes(setOfMeshes)); - } - callback(allMeshes); - })); - } + public void BackgroundWork() + { + output = MeshComponentsForMenu(meshes); + } - /// - /// Update the positions and recalculate bounds and normals for a mesh that hasn't changed geometry. - /// - /// The MMesh. - /// - /// Existing list of MeshWithMaterials from a previous meshFromMMesh call. - /// - /// The Unity Mesh. - public static void UpdateMeshes(MMesh updatedMesh, List existing) { - Vector3 wiggleVector = RandomWiggleVector(); - - // If the MMesh has some opaque faces and some transparent faces, we also want to draw - // the inside of all of the opaque faces. - bool hasMixedFaces = HasMixedFaces(updatedMesh); - - // Simpler version that replicates adding vertices in the same order so indices match up. - Dictionary> newPositionsPerMaterial = new Dictionary>(); - foreach (Face face in updatedMesh.GetFaces()) { - List newPos; - List newColors = new List(); - List newNormals = new List(); - MaterialAndColor faceMaterialAndColor = MaterialRegistry.GetMaterialAndColorById(face.properties.materialId); - if (!newPositionsPerMaterial.TryGetValue(faceMaterialAndColor, out newPos)) { - newPositionsPerMaterial[faceMaterialAndColor] = new List(); - newPos = newPositionsPerMaterial[faceMaterialAndColor]; + public void PostWork() + { + callback(output); + } } - bool drawTriangleBackside = hasMixedFaces - && !MaterialRegistry.IsMaterialTransparent(face.properties.materialId); - // This method is used to update a GameObject, and as such we do not want the vert positions in world space, - // it is the gameObject that will be placed and rotated in the world. - AddFaceVertices(updatedMesh, wiggleVector, face, ref newPos, ref newColors, ref newNormals, - /* useWorldSpace */ false); - - if (drawTriangleBackside) { - // This method is used to update a GameObject, and as such we do not want the vert positions in world space, - // it is the gameObject that will be placed and rotated in the world. - AddFaceVertices(updatedMesh, wiggleVector, face, ref newPos, ref newColors, ref newNormals, - /* useWorldSpace */ false); + + /// + /// Creates a Unity Mesh that draws an MMesh. + /// + /// The MMesh. + /// Whether to take model-space co-ordinates from the MMesh. + /// An override material to use for the entire generated Mesh, if desired. + /// The Unity Mesh. + public static List MeshFromMMesh(MMesh mmesh, bool useModelSpace, + out Dictionary components, MaterialAndColor materialOverride = null) + { + components = MeshComponentsFromMMesh(mmesh, useModelSpace); + return ToMeshes(components, materialOverride); } - } - - // Go through the existing meshes and update positions. - foreach (MeshWithMaterial uMesh in existing) { - List newPos; - if (!newPositionsPerMaterial.TryGetValue(uMesh.materialAndColor, out newPos) || - newPos.Count != uMesh.mesh.vertices.Count()) { - // If materials changed, easiest action is to remesh everything. - existing.Clear(); - // This method is used to update a GameObject, and as such we do not want the vert positions in model space, - // it is the gameObject that will be placed and rotated in the world. - foreach (MeshWithMaterial matMesh in ToMeshes(MeshComponentsFromMMesh(updatedMesh, - /* useModelSpace */ false))) { - existing.Add(matMesh); - } - return; + + /// + /// Generates the components of a Unity Mesh that represents a group of MMeshes, doing its work on the + /// background thread and calling the given callback once finished. + /// + private static void ComponentsFromMMeshesOnBackground(IEnumerable mmeshes, bool useModelSpace, + Action> callback) + { + PeltzerMain.Instance.DoBackgroundWork(new GetComponentsFromMeshesWork(mmeshes, useModelSpace, + (List> output) => + { + List allMeshes = new List(); + foreach (Dictionary setOfMeshes in output) + { + allMeshes.AddRange(ToMeshes(setOfMeshes)); + } + callback(allMeshes); + })); } - uMesh.mesh.SetVertices(newPos); - uMesh.mesh.RecalculateBounds(); - uMesh.mesh.RecalculateNormals(); - } - } - /// - /// Fetches the components (triangles, verts, vertex colours) for a Unity mesh from a given group of MMeshes, - /// optionally using world-space positions (else model-space positions). This method should only be used for - /// meshes displayed in the menu - all other uses should go down one of the mesh-space only paths for performance. - /// Guarantees that the components in any given dictionary in the output will fit into a Unity mesh. - /// - private static List> MeshComponentsForMenu(IEnumerable mmeshes) { - List> allMeshContexts = new List>(); - foreach (MMesh mmesh in mmeshes) { - // First, fetch the components for this mesh, keyed by material. - Dictionary contextByMaterialId = InternalMeshComponentsFromMMesh(mmesh, - useModelSpace:true); - - // Then, for each material, find an available dictionary for its components. - foreach (KeyValuePair pair in contextByMaterialId) { - int material = pair.Key; - MeshGenContext newContext = pair.Value; - bool addedNewContext = false; - - foreach (Dictionary contextDict in allMeshContexts) { - if (!contextDict.ContainsKey(material)) { - // If this dictionary has no entries for this material, add the new context and end the search. - contextDict.Add(material, newContext); - break; + /// + /// Update the positions and recalculate bounds and normals for a mesh that hasn't changed geometry. + /// + /// The MMesh. + /// + /// Existing list of MeshWithMaterials from a previous meshFromMMesh call. + /// + /// The Unity Mesh. + public static void UpdateMeshes(MMesh updatedMesh, List existing) + { + Vector3 wiggleVector = RandomWiggleVector(); + + // If the MMesh has some opaque faces and some transparent faces, we also want to draw + // the inside of all of the opaque faces. + bool hasMixedFaces = HasMixedFaces(updatedMesh); + + // Simpler version that replicates adding vertices in the same order so indices match up. + Dictionary> newPositionsPerMaterial = new Dictionary>(); + foreach (Face face in updatedMesh.GetFaces()) + { + List newPos; + List newColors = new List(); + List newNormals = new List(); + MaterialAndColor faceMaterialAndColor = MaterialRegistry.GetMaterialAndColorById(face.properties.materialId); + if (!newPositionsPerMaterial.TryGetValue(faceMaterialAndColor, out newPos)) + { + newPositionsPerMaterial[faceMaterialAndColor] = new List(); + newPos = newPositionsPerMaterial[faceMaterialAndColor]; + } + bool drawTriangleBackside = hasMixedFaces + && !MaterialRegistry.IsMaterialTransparent(face.properties.materialId); + // This method is used to update a GameObject, and as such we do not want the vert positions in world space, + // it is the gameObject that will be placed and rotated in the world. + AddFaceVertices(updatedMesh, wiggleVector, face, ref newPos, ref newColors, ref newNormals, + /* useWorldSpace */ false); + + if (drawTriangleBackside) + { + // This method is used to update a GameObject, and as such we do not want the vert positions in world space, + // it is the gameObject that will be placed and rotated in the world. + AddFaceVertices(updatedMesh, wiggleVector, face, ref newPos, ref newColors, ref newNormals, + /* useWorldSpace */ false); + } } - MeshGenContext existingContext = contextDict[material]; - if (existingContext.verts.Count + newContext.verts.Count > ReMesher.MAX_VERTS_PER_MESH) { - // If adding the new context to this dictionary would exceed the limits of - // a Unity mesh, continue searching. - continue; - } else { - // Else, if this new context fits into this dictionary, add it and end the search. - CombineContexts(newContext, existingContext); - addedNewContext = true; - break; + // Go through the existing meshes and update positions. + foreach (MeshWithMaterial uMesh in existing) + { + List newPos; + if (!newPositionsPerMaterial.TryGetValue(uMesh.materialAndColor, out newPos) || + newPos.Count != uMesh.mesh.vertices.Count()) + { + // If materials changed, easiest action is to remesh everything. + existing.Clear(); + // This method is used to update a GameObject, and as such we do not want the vert positions in model space, + // it is the gameObject that will be placed and rotated in the world. + foreach (MeshWithMaterial matMesh in ToMeshes(MeshComponentsFromMMesh(updatedMesh, + /* useModelSpace */ false))) + { + existing.Add(matMesh); + } + return; + } + uMesh.mesh.SetVertices(newPos); + uMesh.mesh.RecalculateBounds(); + uMesh.mesh.RecalculateNormals(); } - } - - if (!addedNewContext) { - // If no existing dictionary was able to hold this new context, create a new one and populate it with - // this entry. - Dictionary contextDict = new Dictionary(); - contextDict[material] = newContext; - allMeshContexts.Add(contextDict); - } } - } - return allMeshContexts; - } - /// - /// Add vertices and triangles to a MeshInfo. This method assumes we've ensure there is "room" in the - /// MeshInfo for the new components. - /// - public static void CombineContexts(MeshGenContext source, MeshGenContext target) { - int curSize = target.verts.Count; - target.verts.AddRange(source.verts); - target.normals.AddRange(source.normals); - target.colors.AddRange(source.colors); - for (int i = 0; i < source.triangles.Count; i++) { - target.triangles.Add(source.triangles[i] + curSize); - } - } + /// + /// Fetches the components (triangles, verts, vertex colours) for a Unity mesh from a given group of MMeshes, + /// optionally using world-space positions (else model-space positions). This method should only be used for + /// meshes displayed in the menu - all other uses should go down one of the mesh-space only paths for performance. + /// Guarantees that the components in any given dictionary in the output will fit into a Unity mesh. + /// + private static List> MeshComponentsForMenu(IEnumerable mmeshes) + { + List> allMeshContexts = new List>(); + foreach (MMesh mmesh in mmeshes) + { + // First, fetch the components for this mesh, keyed by material. + Dictionary contextByMaterialId = InternalMeshComponentsFromMMesh(mmesh, + useModelSpace: true); + + // Then, for each material, find an available dictionary for its components. + foreach (KeyValuePair pair in contextByMaterialId) + { + int material = pair.Key; + MeshGenContext newContext = pair.Value; + bool addedNewContext = false; + + foreach (Dictionary contextDict in allMeshContexts) + { + if (!contextDict.ContainsKey(material)) + { + // If this dictionary has no entries for this material, add the new context and end the search. + contextDict.Add(material, newContext); + break; + } + + MeshGenContext existingContext = contextDict[material]; + if (existingContext.verts.Count + newContext.verts.Count > ReMesher.MAX_VERTS_PER_MESH) + { + // If adding the new context to this dictionary would exceed the limits of + // a Unity mesh, continue searching. + continue; + } + else + { + // Else, if this new context fits into this dictionary, add it and end the search. + CombineContexts(newContext, existingContext); + addedNewContext = true; + break; + } + } + + if (!addedNewContext) + { + // If no existing dictionary was able to hold this new context, create a new one and populate it with + // this entry. + Dictionary contextDict = new Dictionary(); + contextDict[material] = newContext; + allMeshContexts.Add(contextDict); + } + } + } + return allMeshContexts; + } - /// - /// Triangulates a mesh and returns its faces and vertices in a dictionary keyed by material id. - /// - /// The input mesh - /// - /// If true, the vertex locations in the dictionary will be in model space, else they'll be in mesh space. - /// - /// - public static Dictionary MeshComponentsFromMMesh(MMesh mmesh, bool useModelSpace) { - return InternalMeshComponentsFromMMesh(mmesh, useModelSpace); - } + /// + /// Add vertices and triangles to a MeshInfo. This method assumes we've ensure there is "room" in the + /// MeshInfo for the new components. + /// + public static void CombineContexts(MeshGenContext source, MeshGenContext target) + { + int curSize = target.verts.Count; + target.verts.AddRange(source.verts); + target.normals.AddRange(source.normals); + target.colors.AddRange(source.colors); + for (int i = 0; i < source.triangles.Count; i++) + { + target.triangles.Add(source.triangles[i] + curSize); + } + } + /// + /// Triangulates a mesh and returns its faces and vertices in a dictionary keyed by material id. + /// + /// The input mesh + /// + /// If true, the vertex locations in the dictionary will be in model space, else they'll be in mesh space. + /// + /// + public static Dictionary MeshComponentsFromMMesh(MMesh mmesh, bool useModelSpace) + { + return InternalMeshComponentsFromMMesh(mmesh, useModelSpace); + } - private static Dictionary InternalMeshComponentsFromMMesh(MMesh mmesh, bool useModelSpace) { - Dictionary contextByMaterialId = new Dictionary(); - Vector3 wiggleVector = RandomWiggleVector(); - // If the MMesh has some opaque faces and some transparent faces, we also want to draw - // the inside of all of the opaque faces. - bool hasMixedFaces = HasMixedFaces(mmesh); + private static Dictionary InternalMeshComponentsFromMMesh(MMesh mmesh, bool useModelSpace) + { + Dictionary contextByMaterialId = new Dictionary(); + Vector3 wiggleVector = RandomWiggleVector(); + + // If the MMesh has some opaque faces and some transparent faces, we also want to draw + // the inside of all of the opaque faces. + bool hasMixedFaces = HasMixedFaces(mmesh); + + foreach (Face face in mmesh.GetFaces()) + { + int materialId = face.properties.materialId; + MeshGenContext context; + if (!contextByMaterialId.TryGetValue(face.properties.materialId, out context)) + { + context = new MeshGenContext(); + contextByMaterialId[materialId] = context; + } + + // Should we also draw the inside of the faces: + bool drawTriangleBackside = hasMixedFaces + && !MaterialRegistry.IsMaterialTransparent(face.properties.materialId); + + int offset = context.verts.Count; + int backOffset = -1; + + AddFaceVertices(mmesh, wiggleVector, face, ref context.verts, ref context.colors, ref context.normals, + useModelSpace); + + if (drawTriangleBackside) + { + // The back side of the face will have different vertex normals, + // so we need to make new vertices for those triangles: + backOffset = context.verts.Count; + AddFaceVertices(mmesh, wiggleVector, face, ref context.verts, ref context.colors, ref context.normals, + useModelSpace); + } + + List tris = face.GetRenderTriangulation(mmesh); + foreach (Triangle tri in tris) + { + context.triangles.Add(tri.vertId0 + offset); + context.triangles.Add(tri.vertId1 + offset); + context.triangles.Add(tri.vertId2 + offset); + if (drawTriangleBackside) + { + // Changing the order of the triangle vertices will draw them on the other side: + context.triangles.Add(tri.vertId0 + backOffset); + context.triangles.Add(tri.vertId2 + backOffset); + context.triangles.Add(tri.vertId1 + backOffset); + } + } + } - foreach (Face face in mmesh.GetFaces()) { - int materialId = face.properties.materialId; - MeshGenContext context; - if (!contextByMaterialId.TryGetValue(face.properties.materialId, out context)) { - context = new MeshGenContext(); - contextByMaterialId[materialId] = context; + return contextByMaterialId; } - // Should we also draw the inside of the faces: - bool drawTriangleBackside = hasMixedFaces - && !MaterialRegistry.IsMaterialTransparent(face.properties.materialId); - - int offset = context.verts.Count; - int backOffset = -1; - - AddFaceVertices(mmesh, wiggleVector, face, ref context.verts, ref context.colors, ref context.normals, - useModelSpace); + private static bool HasMixedFaces(MMesh mmesh) + { + bool hasOpaqueFaces = false; + bool hasTransparentFaces = false; + + foreach (Face face in mmesh.GetFaces()) + { + if (MaterialRegistry.IsMaterialTransparent(face.properties.materialId)) + { + hasTransparentFaces = true; + } + else + { + hasOpaqueFaces = true; + } + } - if (drawTriangleBackside) { - // The back side of the face will have different vertex normals, - // so we need to make new vertices for those triangles: - backOffset = context.verts.Count; - AddFaceVertices(mmesh, wiggleVector, face, ref context.verts, ref context.colors, ref context.normals, - useModelSpace); - } - - List tris = face.GetRenderTriangulation(mmesh); - foreach (Triangle tri in tris) { - context.triangles.Add(tri.vertId0 + offset); - context.triangles.Add(tri.vertId1 + offset); - context.triangles.Add(tri.vertId2 + offset); - if (drawTriangleBackside) { - // Changing the order of the triangle vertices will draw them on the other side: - context.triangles.Add(tri.vertId0 + backOffset); - context.triangles.Add(tri.vertId2 + backOffset); - context.triangles.Add(tri.vertId1 + backOffset); - } + return hasOpaqueFaces && hasTransparentFaces; } - } - return contextByMaterialId; - } - - private static bool HasMixedFaces(MMesh mmesh) { - bool hasOpaqueFaces = false; - bool hasTransparentFaces = false; + /// + /// Adds the locations of the vertices of a given face to a given list, applying a 'wiggle' to them to + /// avoid z-fighting. + /// + /// The mesh containing the face + /// The wiggle to avoid z-fighting + /// The face with the verts to be added + /// The vert list to which the verts will be added + /// The color list to which colors will be added + /// The normal list to which the normals will be added + /// + /// If true, the added vertex locations will be in model space, else they'll be in mesh space. + /// + private static void AddFaceVertices(MMesh mmesh, + Vector3 wiggleVector, + Face face, + ref List vertList, + ref List colorList, + ref List normalList, + bool useModelSpace) + { + + if (useModelSpace) + { + List meshSpaceVerts = face.GetMeshSpaceVertices(mmesh); + for (int i = 0; i < meshSpaceVerts.Count; i++) + { + vertList.Add((mmesh.rotation * meshSpaceVerts[i]) + mmesh.offset); + } + } + else + { + vertList.AddRange(face.GetMeshSpaceVertices(mmesh)); + } + colorList.AddRange(face.GetColors()); + normalList.AddRange(face.GetRenderNormals(mmesh)); + } - foreach (Face face in mmesh.GetFaces()) { - if (MaterialRegistry.IsMaterialTransparent(face.properties.materialId)) { - hasTransparentFaces = true; - } else { - hasOpaqueFaces = true; + /// + /// Generates a GameObject which looks identical to an MMesh. + /// + /// The transform to the world's co-ordinate system + /// The mesh to imitate with a GameObject + /// If passed, an override of the mesh's current material + /// The imitation of the mesh as a GameObject + public static GameObject GameObjectFromMMesh(WorldSpace worldSpace, MMesh mesh, MaterialAndColor materialOverride = null) + { + // Set up a GameObject and attach it to the GameObject for previewing. + GameObject meshHighlight = new GameObject(); + MMesh.AttachMeshToGameObject( + worldSpace, meshHighlight, mesh, /* updateOnly */ false, materialOverride); + + return meshHighlight; } - } - return hasOpaqueFaces && hasTransparentFaces; - } + /// + /// A helper method for rendering a group of MMeshes. Takes a GameObject ensures it has a + /// MeshWithMaterialRenderer and adds the Meshes corresponding to the MMeshes and a Script that will draw them. + /// + private static void AttachMeshesToGameObject( + WorldSpace worldSpace, GameObject gameObject, List meshes, Action callback, + bool updateOnly = false) + { + // Add renderer to GameObject. + MeshWithMaterialRenderer renderer = gameObject.AddComponent(); + renderer.Init(worldSpace); + renderer.meshes = new List(); + + // Position the gameObject so that the mesh appears to have the correct position. + Vector3 centroid = Math3d.FindCentroid(meshes); + renderer.SetPositionModelSpace(centroid); + + foreach (MMesh mesh in meshes) + { + Vector3 offsetFromCentroid = mesh.offset - centroid; + mesh.offset = gameObject.transform.position + offsetFromCentroid; + } - /// - /// Adds the locations of the vertices of a given face to a given list, applying a 'wiggle' to them to - /// avoid z-fighting. - /// - /// The mesh containing the face - /// The wiggle to avoid z-fighting - /// The face with the verts to be added - /// The vert list to which the verts will be added - /// The color list to which colors will be added - /// The normal list to which the normals will be added - /// - /// If true, the added vertex locations will be in model space, else they'll be in mesh space. - /// - private static void AddFaceVertices(MMesh mmesh, - Vector3 wiggleVector, - Face face, - ref List vertList, - ref List colorList, - ref List normalList, - bool useModelSpace) { - - if (useModelSpace) { - List meshSpaceVerts = face.GetMeshSpaceVertices(mmesh); - for (int i = 0; i < meshSpaceVerts.Count; i++) { - vertList.Add((mmesh.rotation * meshSpaceVerts[i]) + mmesh.offset); - } - } - else { - vertList.AddRange(face.GetMeshSpaceVertices(mmesh)); - } - colorList.AddRange(face.GetColors()); - normalList.AddRange(face.GetRenderNormals(mmesh)); - } + ComponentsFromMMeshesOnBackground(meshes, /* useModelSpace */ false, (List output) => + { + renderer.meshes = output; - /// - /// Generates a GameObject which looks identical to an MMesh. - /// - /// The transform to the world's co-ordinate system - /// The mesh to imitate with a GameObject - /// If passed, an override of the mesh's current material - /// The imitation of the mesh as a GameObject - public static GameObject GameObjectFromMMesh(WorldSpace worldSpace, MMesh mesh, MaterialAndColor materialOverride = null) { - // Set up a GameObject and attach it to the GameObject for previewing. - GameObject meshHighlight = new GameObject(); - MMesh.AttachMeshToGameObject( - worldSpace, meshHighlight, mesh, /* updateOnly */ false, materialOverride); - - return meshHighlight; - } + if (callback != null) + { + callback(); + } + }); + } - /// - /// A helper method for rendering a group of MMeshes. Takes a GameObject ensures it has a - /// MeshWithMaterialRenderer and adds the Meshes corresponding to the MMeshes and a Script that will draw them. - /// - private static void AttachMeshesToGameObject( - WorldSpace worldSpace, GameObject gameObject, List meshes, Action callback, - bool updateOnly = false) { - // Add renderer to GameObject. - MeshWithMaterialRenderer renderer = gameObject.AddComponent(); - renderer.Init(worldSpace); - renderer.meshes = new List(); - - // Position the gameObject so that the mesh appears to have the correct position. - Vector3 centroid = Math3d.FindCentroid(meshes); - renderer.SetPositionModelSpace(centroid); - - foreach (MMesh mesh in meshes) { - Vector3 offsetFromCentroid = mesh.offset - centroid; - mesh.offset = gameObject.transform.position + offsetFromCentroid; - } - - ComponentsFromMMeshesOnBackground(meshes, /* useModelSpace */ false, (List output) => { - renderer.meshes = output; - - if (callback != null) { - callback(); + /// + /// Generates a GameObject which looks identical to a group of MMeshes. Calls back with the GameObject after the + /// MMeshes have been attached to it on a background thread. This method should only be used for + /// meshes displayed in the menu - all other uses should go down one of the mesh-space only paths for performance. + /// + /// The transform to the world's co-ordinate system. + /// The meshes to imitate with a GameObject. + /// If passed, an override of the mesh's current material + /// The callback for the game object. + public static void GameObjectFromMMeshesForMenu(WorldSpace worldSpace, List meshes, + Action callback, MaterialAndColor materialOverride = null) + { + // Set up a GameObject and attach it to the GameObject for previewing. + GameObject meshHighlight = new GameObject(); + AttachMeshesToGameObject(worldSpace, meshHighlight, meshes, delegate () + { + callback(meshHighlight); + }); } - }); - } - - /// - /// Generates a GameObject which looks identical to a group of MMeshes. Calls back with the GameObject after the - /// MMeshes have been attached to it on a background thread. This method should only be used for - /// meshes displayed in the menu - all other uses should go down one of the mesh-space only paths for performance. - /// - /// The transform to the world's co-ordinate system. - /// The meshes to imitate with a GameObject. - /// If passed, an override of the mesh's current material - /// The callback for the game object. - public static void GameObjectFromMMeshesForMenu(WorldSpace worldSpace, List meshes, - Action callback, MaterialAndColor materialOverride = null) { - // Set up a GameObject and attach it to the GameObject for previewing. - GameObject meshHighlight = new GameObject(); - AttachMeshesToGameObject(worldSpace, meshHighlight, meshes, delegate () { - callback(meshHighlight); - }); - } - /// - /// Create an mmesh from a set of meshes - /// - /// id for new mesh - /// the meshes to construct the mmesh from - /// - public static MMesh MMeshFromMeshes(int id, Dictionary> materialsAndMeshes) { - Dictionary verticesById = new Dictionary(); - Dictionary facesById = new Dictionary(); - int vIdx = 0; - int faceIdx = 0; - foreach (KeyValuePair> pair in materialsAndMeshes) { - Material material = pair.Key; - foreach (MeshVerticesAndTriangles meshVerticesAndTriangles in pair.Value) { - Dictionary meshVertices = new Dictionary(); - for (int i = 0; i < meshVerticesAndTriangles.meshVertices.Length; i++) { - Vector3 v = meshVerticesAndTriangles.meshVertices[i]; - int vertexId = vIdx++; - Vertex vertex = new Vertex(vertexId, v); - verticesById.Add(vertexId, vertex); - meshVertices.Add(i, vertex); - } - for (int triangleIdxIdx = 0; triangleIdxIdx < meshVerticesAndTriangles.triangles.Length; triangleIdxIdx += 3) { - int idx1 = meshVerticesAndTriangles.triangles[triangleIdxIdx]; - int idx2 = meshVerticesAndTriangles.triangles[triangleIdxIdx + 1]; - int idx3 = meshVerticesAndTriangles.triangles[triangleIdxIdx + 2]; - - int newFaceId = faceIdx++; - Face face = new Face(newFaceId, new List() { + /// + /// Create an mmesh from a set of meshes + /// + /// id for new mesh + /// the meshes to construct the mmesh from + /// + public static MMesh MMeshFromMeshes(int id, Dictionary> materialsAndMeshes) + { + Dictionary verticesById = new Dictionary(); + Dictionary facesById = new Dictionary(); + int vIdx = 0; + int faceIdx = 0; + foreach (KeyValuePair> pair in materialsAndMeshes) + { + Material material = pair.Key; + foreach (MeshVerticesAndTriangles meshVerticesAndTriangles in pair.Value) + { + Dictionary meshVertices = new Dictionary(); + for (int i = 0; i < meshVerticesAndTriangles.meshVertices.Length; i++) + { + Vector3 v = meshVerticesAndTriangles.meshVertices[i]; + int vertexId = vIdx++; + Vertex vertex = new Vertex(vertexId, v); + verticesById.Add(vertexId, vertex); + meshVertices.Add(i, vertex); + } + for (int triangleIdxIdx = 0; triangleIdxIdx < meshVerticesAndTriangles.triangles.Length; triangleIdxIdx += 3) + { + int idx1 = meshVerticesAndTriangles.triangles[triangleIdxIdx]; + int idx2 = meshVerticesAndTriangles.triangles[triangleIdxIdx + 1]; + int idx3 = meshVerticesAndTriangles.triangles[triangleIdxIdx + 2]; + + int newFaceId = faceIdx++; + Face face = new Face(newFaceId, new List() { meshVertices[idx1].id, meshVertices[idx2].id, meshVertices[idx3].id, }.AsReadOnly(), verticesById, new FaceProperties(TryGetMaterialId(material))); - facesById.Add(newFaceId, face); - } + facesById.Add(newFaceId, face); + } + } + } + return new MMesh(id, Vector3.zero, Quaternion.identity, verticesById, facesById); } - } - return new MMesh(id, Vector3.zero, Quaternion.identity, verticesById, facesById); - } - private static int TryGetMaterialId(Material material) { - if (material.name.StartsWith("mat")) { - int val = 1; - if (int.TryParse(material.name.Substring(3), out val)) { - return val; + private static int TryGetMaterialId(Material material) + { + if (material.name.StartsWith("mat")) + { + int val = 1; + if (int.TryParse(material.name.Substring(3), out val)) + { + return val; + } + } + return 1; } - } - return 1; - } - private static Vector3 RandomWiggleVector() { - return new Vector3( - ((float)RANDOM.NextDouble() * 2f - 1f) * wiggleRoom, - ((float)RANDOM.NextDouble() * 2f - 1f) * wiggleRoom, - ((float)RANDOM.NextDouble() * 2f - 1f) * wiggleRoom); - } - - public static List ToMeshes(Dictionary contextByMaterialId, - MaterialAndColor materialOverride = null, - Quaternion? rotationalOffset = null, Vector3? positionalOffset = null) { - List meshes = new List(contextByMaterialId.Count); - foreach (KeyValuePair pair in contextByMaterialId) { - int materialId = pair.Key; - MeshGenContext context = pair.Value; - Mesh mesh = new Mesh(); - - // Offset the vertices, if required. - if (rotationalOffset != null && positionalOffset != null) { - List vertsInMeshSpace = new List(context.verts.Count); - Quaternion invertedRotationalOffset = Quaternion.Inverse(rotationalOffset.Value); - foreach (Vector3 vertInModelSpace in context.verts) { - vertsInMeshSpace.Add(invertedRotationalOffset * (vertInModelSpace - positionalOffset.Value)); - } - mesh.SetVertices(vertsInMeshSpace); - } else { - mesh.SetVertices(context.verts); - } - mesh.SetNormals(context.normals); - mesh.SetTriangles(context.triangles, /* Submesh */ 0); - mesh.RecalculateBounds(); - - // Add vertex colors. - MaterialAndColor materialAndColor = MaterialRegistry.GetMaterialAndColorById(materialId); - Color32[] colors = new Color32[context.verts.Count]; - Color32 color = materialOverride == null ? materialAndColor.color : materialOverride.color; - for (int i = 0; i < colors.Length; i++) { - colors[i] = color; + private static Vector3 RandomWiggleVector() + { + return new Vector3( + ((float)RANDOM.NextDouble() * 2f - 1f) * wiggleRoom, + ((float)RANDOM.NextDouble() * 2f - 1f) * wiggleRoom, + ((float)RANDOM.NextDouble() * 2f - 1f) * wiggleRoom); } - mesh.colors32 = colors; - meshes.Add(new MeshWithMaterial(mesh, materialAndColor)); - } - return meshes; - } + public static List ToMeshes(Dictionary contextByMaterialId, + MaterialAndColor materialOverride = null, + Quaternion? rotationalOffset = null, Vector3? positionalOffset = null) + { + List meshes = new List(contextByMaterialId.Count); + foreach (KeyValuePair pair in contextByMaterialId) + { + int materialId = pair.Key; + MeshGenContext context = pair.Value; + Mesh mesh = new Mesh(); + + // Offset the vertices, if required. + if (rotationalOffset != null && positionalOffset != null) + { + List vertsInMeshSpace = new List(context.verts.Count); + Quaternion invertedRotationalOffset = Quaternion.Inverse(rotationalOffset.Value); + foreach (Vector3 vertInModelSpace in context.verts) + { + vertsInMeshSpace.Add(invertedRotationalOffset * (vertInModelSpace - positionalOffset.Value)); + } + mesh.SetVertices(vertsInMeshSpace); + } + else + { + mesh.SetVertices(context.verts); + } + mesh.SetNormals(context.normals); + mesh.SetTriangles(context.triangles, /* Submesh */ 0); + mesh.RecalculateBounds(); + + // Add vertex colors. + MaterialAndColor materialAndColor = MaterialRegistry.GetMaterialAndColorById(materialId); + Color32[] colors = new Color32[context.verts.Count]; + Color32 color = materialOverride == null ? materialAndColor.color : materialOverride.color; + for (int i = 0; i < colors.Length; i++) + { + colors[i] = color; + } + mesh.colors32 = colors; + + meshes.Add(new MeshWithMaterial(mesh, materialAndColor)); + } + return meshes; + } - /// - /// Returns a list of Unity meshes corresponding to the given MMesh. - /// - /// - public static List ToUnityMeshes(MeshRepresentationCache cache, MMesh mesh) { - return ToUnityMeshes(cache.FetchComponentsForMesh(mesh)); - } + /// + /// Returns a list of Unity meshes corresponding to the given MMesh. + /// + /// + public static List ToUnityMeshes(MeshRepresentationCache cache, MMesh mesh) + { + return ToUnityMeshes(cache.FetchComponentsForMesh(mesh)); + } - private static List ToUnityMeshes(Dictionary contextByMaterialId) { - List meshes = new List(contextByMaterialId.Count); - foreach (KeyValuePair pair in contextByMaterialId) { - int materialId = pair.Key; - MeshGenContext context = pair.Value; - Mesh mesh = new Mesh(); - mesh.SetVertices(context.verts); - mesh.SetTriangles(context.triangles, /* Submesh */ 0); - mesh.SetNormals(context.normals); - mesh.SetColors(context.colors); - mesh.RecalculateBounds(); + private static List ToUnityMeshes(Dictionary contextByMaterialId) + { + List meshes = new List(contextByMaterialId.Count); + foreach (KeyValuePair pair in contextByMaterialId) + { + int materialId = pair.Key; + MeshGenContext context = pair.Value; + Mesh mesh = new Mesh(); + mesh.SetVertices(context.verts); + mesh.SetTriangles(context.triangles, /* Submesh */ 0); + mesh.SetNormals(context.normals); + mesh.SetColors(context.colors); + mesh.RecalculateBounds(); - // Add vertex colors. + // Add vertex colors. - meshes.Add(mesh); - } - return meshes; - } + meshes.Add(mesh); + } + return meshes; + } - public static bool IsQuadFaceConvex(MMesh mesh, Face face) { - AssertOrThrow.True(face.vertexIds.Count == 4, "IsQuadFaceConvex can only be used in quads."); - Vector3 a = mesh.VertexPositionInMeshCoords(face.vertexIds[0]); - Vector3 b = mesh.VertexPositionInMeshCoords(face.vertexIds[1]); - Vector3 c = mesh.VertexPositionInMeshCoords(face.vertexIds[2]); - Vector3 d = mesh.VertexPositionInMeshCoords(face.vertexIds[3]); - Vector3 normal = MeshMath.CalculateNormal(a, b, c); - // Vertices are in clockwise order: - // a +-------------+ b - // / \ - // / \ - // d +-------------------+ c - // Check if each one is convex: - return Math3d.IsConvex(/*check*/a, /*prev*/d, /*next*/b, normal) && - Math3d.IsConvex(/*check*/b, /*prev*/a, /*next*/c, normal) && - Math3d.IsConvex(/*check*/c, /*prev*/b, /*next*/d, normal) && - Math3d.IsConvex(/*check*/d, /*prev*/c, /*next*/a, normal); - } + public static bool IsQuadFaceConvex(MMesh mesh, Face face) + { + AssertOrThrow.True(face.vertexIds.Count == 4, "IsQuadFaceConvex can only be used in quads."); + Vector3 a = mesh.VertexPositionInMeshCoords(face.vertexIds[0]); + Vector3 b = mesh.VertexPositionInMeshCoords(face.vertexIds[1]); + Vector3 c = mesh.VertexPositionInMeshCoords(face.vertexIds[2]); + Vector3 d = mesh.VertexPositionInMeshCoords(face.vertexIds[3]); + Vector3 normal = MeshMath.CalculateNormal(a, b, c); + // Vertices are in clockwise order: + // a +-------------+ b + // / \ + // / \ + // d +-------------------+ c + // Check if each one is convex: + return Math3d.IsConvex(/*check*/a, /*prev*/d, /*next*/b, normal) && + Math3d.IsConvex(/*check*/b, /*prev*/a, /*next*/c, normal) && + Math3d.IsConvex(/*check*/c, /*prev*/b, /*next*/d, normal) && + Math3d.IsConvex(/*check*/d, /*prev*/c, /*next*/a, normal); + } - public static List TriangulateFace(MMesh mesh, Face face) { - return face.GetRenderTriangulation(mesh); - } + public static List TriangulateFace(MMesh mesh, Face face) + { + return face.GetRenderTriangulation(mesh); + } - /// - /// Creates triangles for the vertices of a face. - /// - /// Number of vertices of the face. - /// List of indices representing the triangles for the face. - public static List GetTrianglesAsFan(int numberOfVertices) { - List triangles = new List(); - for (int i = 1; i < (numberOfVertices - 1); i++) { - triangles.Add(0); - triangles.Add(i); - triangles.Add(i + 1); - } - return triangles; + /// + /// Creates triangles for the vertices of a face. + /// + /// Number of vertices of the face. + /// List of indices representing the triangles for the face. + public static List GetTrianglesAsFan(int numberOfVertices) + { + List triangles = new List(); + for (int i = 1; i < (numberOfVertices - 1); i++) + { + triangles.Add(0); + triangles.Add(i); + triangles.Add(i + 1); + } + return triangles; + } } - } } diff --git a/Assets/Scripts/model/render/MeshWithMaterial.cs b/Assets/Scripts/model/render/MeshWithMaterial.cs index 4738a190..80886c1e 100644 --- a/Assets/Scripts/model/render/MeshWithMaterial.cs +++ b/Assets/Scripts/model/render/MeshWithMaterial.cs @@ -14,17 +14,20 @@ using UnityEngine; -namespace com.google.apps.peltzer.client.model.render { - /// - /// Holds a pair of Mesh and Material. - /// - public struct MeshWithMaterial { - public Mesh mesh { get; private set; } - public MaterialAndColor materialAndColor { get; set; } +namespace com.google.apps.peltzer.client.model.render +{ + /// + /// Holds a pair of Mesh and Material. + /// + public struct MeshWithMaterial + { + public Mesh mesh { get; private set; } + public MaterialAndColor materialAndColor { get; set; } - public MeshWithMaterial(Mesh mesh, MaterialAndColor materialAndColor) { - this.mesh = mesh; - this.materialAndColor = materialAndColor; + public MeshWithMaterial(Mesh mesh, MaterialAndColor materialAndColor) + { + this.mesh = mesh; + this.materialAndColor = materialAndColor; + } } - } } diff --git a/Assets/Scripts/model/render/ReMesher.cs b/Assets/Scripts/model/render/ReMesher.cs index 524412d5..836fc2b6 100644 --- a/Assets/Scripts/model/render/ReMesher.cs +++ b/Assets/Scripts/model/render/ReMesher.cs @@ -20,505 +20,564 @@ using com.google.apps.peltzer.client.model.util; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.render { - - /// - /// Generates, maintains and renders a collection of Unity Meshes based on a collection of MMeshes. - /// - /// Multiple MMeshes can be coalesced into a single Unity Mesh. MMeshes that have multiple - /// different materials will be divided between multiple Unity Meshes. (Meaning that you end up with - /// a many-to-many relationship between MMeshes and Unity Meshes.) - /// - /// Every time a MMesh is added, we first retrieve its triangles from the cache (potentially calculating them on a - /// cache miss. We then add those triangles to a Unity Mesh of the same material. When that mesh is "full" (in - /// this case, has MAX_VERTS_PER_MESH vertices), we create a new Unity Mesh for that material. Each time we add - /// triangles to a Unity Mesh, we need to regenerate that mesh. - /// - /// TODO(bug): Support meshes with more than 65k verts. - public class ReMesher { - // Maximum number of vertices we'd put into a coalesced Mesh. - public const int MAX_VERTS_PER_MESH = 20000; - - // Maximum number of vertices we allow for any mesh. - private const int MAX_VERTS_PER_MMESH = 20000; - - // Maximum number of MMeshes in a single MeshInfo. - private const int MAX_MMESH_PER_MESHINFO = 128; - - // A number of static Vector2 arrays, each of which is filled with the same constant Vector2 - ie, the first array - // is all Vector2(0,0), the second is Vector2(1, 0) and so on. This allows us to use cheap Array.Copy() calls to - // fill an array with the same value. - private static List BufferCaches; - +namespace com.google.apps.peltzer.client.model.render +{ + /// - /// Info about a Unity Mesh that will be drawn at render time. MeshInfo batches a number of meshes together, and - /// renders them in a single draw call, passing an array of transform matrix uniforms to position them correctly. + /// Generates, maintains and renders a collection of Unity Meshes based on a collection of MMeshes. + /// + /// Multiple MMeshes can be coalesced into a single Unity Mesh. MMeshes that have multiple + /// different materials will be divided between multiple Unity Meshes. (Meaning that you end up with + /// a many-to-many relationship between MMeshes and Unity Meshes.) + /// + /// Every time a MMesh is added, we first retrieve its triangles from the cache (potentially calculating them on a + /// cache miss. We then add those triangles to a Unity Mesh of the same material. When that mesh is "full" (in + /// this case, has MAX_VERTS_PER_MESH vertices), we create a new Unity Mesh for that material. Each time we add + /// triangles to a Unity Mesh, we need to regenerate that mesh. /// - public class MeshInfo { - // The material we draw this mesh with. - public MaterialAndColor materialAndColor; - - // All MMeshes that contribute to this mesh. - private HashSet mmeshes = new HashSet(); - - // It's cheaper to render extra data in the form of degenerate triangles than it is to resize our vertex array, - // so all of these are preallocated. - public Vector3[] verts = new Vector3[MAX_VERTS_PER_MESH]; - public Color32[] colors = new Color32[MAX_VERTS_PER_MESH]; - public Vector3[] normals = new Vector3[MAX_VERTS_PER_MESH]; - // List here because the number of triangles isn't predictable based on the number of vertices, so we may need - // to eventually resize. - public List triangles = new List(3 * MAX_VERTS_PER_MESH); - - // Buffer that holds the transform index each vertex should use. - public Vector2[] transformIndexBuffer = new Vector2[MAX_VERTS_PER_MESH]; - - // The number of vertices tracked by this MeshInfo - defaults to 2, because we start with 2 vertices due to a - // hack to avoid the mesh being view frustum culled. - public int numVerts = 2; - - // The number of vertices that are waiting to be added to number of vertices tracked by this MeshInfo when - // Regenerate is called. - public int numPendingVerts; - - // The Unity Mesh, itself. - public Mesh mesh = new Mesh(); - - // Whether anything has been deleted from this meshInfo, causing it to need regeneration. - public bool needsRegeneration = false; - - // Each mesh needs a transformation matrix when we render. In order to batch meshes with different transforms, - // we pass the transforms as an array of uniforms, and supply each vertex with the index of the transform it - // should use. - public Dictionary mmeshToTransformIndex = new Dictionary(); - - // One available per MAX_MMESH_PER_MESHINFO - private Queue availableTransformIndices = new Queue(); - - // Transform uniforms. - public Matrix4x4[] xformMats = new Matrix4x4[MAX_MMESH_PER_MESHINFO]; - - // Map from MMesh id to the set of MeshGenContexts contained in this MeshInfo that are associated with it. - private Dictionary> meshGenContexts - = new Dictionary>(); - - /// - /// Gets the mesh ids of all meshes in this MeshInfo - /// - public HashSet GetMeshIds() { - return mmeshes; - } - - /// - /// Gets the number of meshes in this MeshInfo - /// - public int GetNumMeshes() { - return mmeshes.Count; - } - - /// - /// Returns whether this MeshInfo contains the specified mesh - /// - public bool HasMesh(int meshId) { - return mmeshes.Contains(meshId); - } - - /// - /// Builds a Unity Mesh from the given MeshInfo that has correct (in world-space) vertex positions, and does - /// not have any hacks or optimizations that ReMesher relies upon, for export. - /// - public static Mesh BuildExportableMeshFromMeshInfo(MeshInfo meshInfo) { - Mesh exportableMesh = new Mesh(); - - // We need to work around the 2 extra verts we hack into the MeshInfo mesh to work around frustrum culling. - // These verts exist in the first two indices of the verts array. - int numVertsInMesh = meshInfo.numVerts - 2; - int indexOfFirstVert = 2; - - // Generate a new list of vertices in their correct world-space positions. - Vector3[] newLocs = new Vector3[numVertsInMesh]; - for (int i = 0; i < numVertsInMesh; i++) { - int transformIndex = (int)meshInfo.transformIndexBuffer[i + indexOfFirstVert].x; - newLocs[i] = meshInfo.xformMats[transformIndex].MultiplyPoint(meshInfo.verts[i + indexOfFirstVert]); + /// TODO(bug): Support meshes with more than 65k verts. + public class ReMesher + { + // Maximum number of vertices we'd put into a coalesced Mesh. + public const int MAX_VERTS_PER_MESH = 20000; + + // Maximum number of vertices we allow for any mesh. + private const int MAX_VERTS_PER_MMESH = 20000; + + // Maximum number of MMeshes in a single MeshInfo. + private const int MAX_MMESH_PER_MESHINFO = 128; + + // A number of static Vector2 arrays, each of which is filled with the same constant Vector2 - ie, the first array + // is all Vector2(0,0), the second is Vector2(1, 0) and so on. This allows us to use cheap Array.Copy() calls to + // fill an array with the same value. + private static List BufferCaches; + + /// + /// Info about a Unity Mesh that will be drawn at render time. MeshInfo batches a number of meshes together, and + /// renders them in a single draw call, passing an array of transform matrix uniforms to position them correctly. + /// + public class MeshInfo + { + // The material we draw this mesh with. + public MaterialAndColor materialAndColor; + + // All MMeshes that contribute to this mesh. + private HashSet mmeshes = new HashSet(); + + // It's cheaper to render extra data in the form of degenerate triangles than it is to resize our vertex array, + // so all of these are preallocated. + public Vector3[] verts = new Vector3[MAX_VERTS_PER_MESH]; + public Color32[] colors = new Color32[MAX_VERTS_PER_MESH]; + public Vector3[] normals = new Vector3[MAX_VERTS_PER_MESH]; + // List here because the number of triangles isn't predictable based on the number of vertices, so we may need + // to eventually resize. + public List triangles = new List(3 * MAX_VERTS_PER_MESH); + + // Buffer that holds the transform index each vertex should use. + public Vector2[] transformIndexBuffer = new Vector2[MAX_VERTS_PER_MESH]; + + // The number of vertices tracked by this MeshInfo - defaults to 2, because we start with 2 vertices due to a + // hack to avoid the mesh being view frustum culled. + public int numVerts = 2; + + // The number of vertices that are waiting to be added to number of vertices tracked by this MeshInfo when + // Regenerate is called. + public int numPendingVerts; + + // The Unity Mesh, itself. + public Mesh mesh = new Mesh(); + + // Whether anything has been deleted from this meshInfo, causing it to need regeneration. + public bool needsRegeneration = false; + + // Each mesh needs a transformation matrix when we render. In order to batch meshes with different transforms, + // we pass the transforms as an array of uniforms, and supply each vertex with the index of the transform it + // should use. + public Dictionary mmeshToTransformIndex = new Dictionary(); + + // One available per MAX_MMESH_PER_MESHINFO + private Queue availableTransformIndices = new Queue(); + + // Transform uniforms. + public Matrix4x4[] xformMats = new Matrix4x4[MAX_MMESH_PER_MESHINFO]; + + // Map from MMesh id to the set of MeshGenContexts contained in this MeshInfo that are associated with it. + private Dictionary> meshGenContexts + = new Dictionary>(); + + /// + /// Gets the mesh ids of all meshes in this MeshInfo + /// + public HashSet GetMeshIds() + { + return mmeshes; + } + + /// + /// Gets the number of meshes in this MeshInfo + /// + public int GetNumMeshes() + { + return mmeshes.Count; + } + + /// + /// Returns whether this MeshInfo contains the specified mesh + /// + public bool HasMesh(int meshId) + { + return mmeshes.Contains(meshId); + } + + /// + /// Builds a Unity Mesh from the given MeshInfo that has correct (in world-space) vertex positions, and does + /// not have any hacks or optimizations that ReMesher relies upon, for export. + /// + public static Mesh BuildExportableMeshFromMeshInfo(MeshInfo meshInfo) + { + Mesh exportableMesh = new Mesh(); + + // We need to work around the 2 extra verts we hack into the MeshInfo mesh to work around frustrum culling. + // These verts exist in the first two indices of the verts array. + int numVertsInMesh = meshInfo.numVerts - 2; + int indexOfFirstVert = 2; + + // Generate a new list of vertices in their correct world-space positions. + Vector3[] newLocs = new Vector3[numVertsInMesh]; + for (int i = 0; i < numVertsInMesh; i++) + { + int transformIndex = (int)meshInfo.transformIndexBuffer[i + indexOfFirstVert].x; + newLocs[i] = meshInfo.xformMats[transformIndex].MultiplyPoint(meshInfo.verts[i + indexOfFirstVert]); + } + exportableMesh.vertices = newLocs; + + // Copy Colors. + Color32[] copiedColors = new Color32[numVertsInMesh]; + Array.Copy(meshInfo.colors, indexOfFirstVert, copiedColors, 0, numVertsInMesh); + exportableMesh.colors32 = copiedColors; + + // Copy Triangles. + int[] copiedTriangles = new int[meshInfo.triangles.Count]; + for (int i = 0; i < meshInfo.triangles.Count; i++) + { + copiedTriangles[i] = meshInfo.triangles[i] - indexOfFirstVert; + } + exportableMesh.triangles = copiedTriangles; + + // Copy Normals + // TODO(bug): Get rid of this recalculation and just rely on the normals when they are fixed. + exportableMesh.RecalculateNormals(); + // Vector3[] copiedNormals = new Vector3[numVertsInMesh]; + // Array.Copy(meshInfo.normals, indexOfFirstVert, copiedNormals, 0, numVertsInMesh); + // exportableMesh.normals = copiedNormals; + + return exportableMesh; + } + + /// + /// Adds the supplied MeshGenContext to this mesh under the supplied id. Multiple contexts with the same mesh id + /// can be supplied (Different mats that use the same shader to render) + /// + public void AddMesh(int meshId, MeshGenContext context) + { + if (mmeshes.Add(meshId)) + { + // Since we're only allocating one index per mmesh and have indices up to MAX_MMESH_PER_MESHINFO we're + // guaranteed to have enough. + int availableTransformIndex = availableTransformIndices.Dequeue(); + mmeshToTransformIndex.Add(meshId, availableTransformIndex); + } + HashSet contextSet; + // Append the triangles, add to the set of MeshInfos to regenerate and other bookeeping. + if (!meshGenContexts.TryGetValue(meshId, out contextSet)) + { + contextSet = new HashSet(); + meshGenContexts[meshId] = contextSet; + } + contextSet.Add(context); + if (!needsRegeneration) + { + AddContext(meshId, context); + } + else + { + numPendingVerts += context.verts.Count; + } + } + + /// + /// Removes a mesh from this MeshInfo. + /// + public void RemoveMesh(int meshId) + { + mmeshes.Remove(meshId); + availableTransformIndices.Enqueue(mmeshToTransformIndex[meshId]); + mmeshToTransformIndex.Remove(meshId); + meshGenContexts.Remove(meshId); + needsRegeneration = true; + } + + /// + /// Add vertices and triangles to a MeshInfo. This method assumes we've ensured there is "room" in the + /// MeshInfo for the new components. + /// + /// The id of the mesh whose context we are adding. + /// The MehGenContext whose data we're adding to this MeshInfo. + public void AddContext(int meshId, MeshGenContext source) + { + int transformIndex = mmeshToTransformIndex[meshId]; + Array.Copy(source.verts.ToArray(), 0, verts, numVerts, source.verts.Count); + Array.Copy(source.normals.ToArray(), 0, normals, numVerts, source.verts.Count); + Array.Copy(source.colors.ToArray(), 0, colors, numVerts, source.verts.Count); + Array.Copy(BufferCaches[transformIndex], 0, transformIndexBuffer, numVerts, source.verts.Count); + int triCount = source.triangles.Count; + for (int i = 0; i < triCount; i++) + { + triangles.Add(source.triangles[i] + numVerts); + } + numVerts += source.verts.Count; + } + + + /// + /// Updates the array of transform mats for the meshes this info renders. + /// + public void UpdateTransforms(Model model) + { + foreach (int meshId in mmeshes) + { + xformMats[mmeshToTransformIndex[meshId]] = model.GetMesh(meshId).GetJitteredTransform(); + } + } + + /// + /// Sets the transform mat array as a shader uniform for the supplied material. + /// + public void SetTransforms(Material mat) + { + mat.SetMatrixArray("_RemesherMeshTransforms", xformMats); + } + + /// + /// Regenerates the buffers used for constructing meshes. We need to do this when meshes are removed from the + /// info, as doing that invalidates our triangle indices (because they are calculated based on offset). + /// + public void Regenerate() + { + // Every vert we don't care about will form a degenerate triangle. Keeps our first two verts. + triangles.Clear(); + Array.Clear(verts, 2, verts.Length - 2); + numVerts = 2; + + foreach (int meshId in mmeshes) + { + foreach (MeshGenContext context in meshGenContexts[meshId]) + { + AddContext(meshId, context); + } + } + needsRegeneration = false; + numPendingVerts = 0; + } + + public MeshInfo() + { + // This is a really undesirable hack. Since we're passing an additional transform matrix into the shader, it breaks + // Unity's view frustum culling, and the meshinfo will disappear at various angles. By adding these two extreme + // vertices, it causes the mesh to never be culled because it has an enormous bounding box. + // While setting the bounds on the Unity mesh directly should in theory work, in practice it seems not to. + verts[0] = new Vector3(999999f, 999999f, 999999f); + verts[1] = new Vector3(-999999f, -999999f, -999999f); + numVerts = 2; + + for (int i = 0; i < MAX_MMESH_PER_MESHINFO; i++) + { + xformMats[i] = Matrix4x4.identity; + availableTransformIndices.Enqueue(i); + } + } } - exportableMesh.vertices = newLocs; - // Copy Colors. - Color32[] copiedColors = new Color32[numVertsInMesh]; - Array.Copy(meshInfo.colors, indexOfFirstVert, copiedColors, 0, numVertsInMesh); - exportableMesh.colors32 = copiedColors; + // For each MMesh, the set of MeshInfos that MMesh contributes triangles to. + private Dictionary> meshInfosByMesh = new Dictionary>(); - // Copy Triangles. - int[] copiedTriangles = new int[meshInfo.triangles.Count]; - for (int i = 0; i < meshInfo.triangles.Count; i++) { - copiedTriangles[i] = meshInfo.triangles[i] - indexOfFirstVert; - } - exportableMesh.triangles = copiedTriangles; - - // Copy Normals - // TODO(bug): Get rid of this recalculation and just rely on the normals when they are fixed. - exportableMesh.RecalculateNormals(); - // Vector3[] copiedNormals = new Vector3[numVertsInMesh]; - // Array.Copy(meshInfo.normals, indexOfFirstVert, copiedNormals, 0, numVertsInMesh); - // exportableMesh.normals = copiedNormals; - - return exportableMesh; - } - - /// - /// Adds the supplied MeshGenContext to this mesh under the supplied id. Multiple contexts with the same mesh id - /// can be supplied (Different mats that use the same shader to render) - /// - public void AddMesh(int meshId, MeshGenContext context) { - if (mmeshes.Add(meshId)) { - // Since we're only allocating one index per mmesh and have indices up to MAX_MMESH_PER_MESHINFO we're - // guaranteed to have enough. - int availableTransformIndex = availableTransformIndices.Dequeue(); - mmeshToTransformIndex.Add(meshId, availableTransformIndex); - } - HashSet contextSet; - // Append the triangles, add to the set of MeshInfos to regenerate and other bookeeping. - if (!meshGenContexts.TryGetValue(meshId, out contextSet)) { - contextSet = new HashSet(); - meshGenContexts[meshId] = contextSet; - } - contextSet.Add(context); - if (!needsRegeneration) { - AddContext(meshId, context); - } else { - numPendingVerts += context.verts.Count; - } - } - - /// - /// Removes a mesh from this MeshInfo. - /// - public void RemoveMesh(int meshId) { - mmeshes.Remove(meshId); - availableTransformIndices.Enqueue(mmeshToTransformIndex[meshId]); - mmeshToTransformIndex.Remove(meshId); - meshGenContexts.Remove(meshId); - needsRegeneration = true; - } - - /// - /// Add vertices and triangles to a MeshInfo. This method assumes we've ensured there is "room" in the - /// MeshInfo for the new components. - /// - /// The id of the mesh whose context we are adding. - /// The MehGenContext whose data we're adding to this MeshInfo. - public void AddContext(int meshId, MeshGenContext source) { - int transformIndex = mmeshToTransformIndex[meshId]; - Array.Copy(source.verts.ToArray(), 0, verts, numVerts, source.verts.Count); - Array.Copy(source.normals.ToArray(), 0, normals, numVerts, source.verts.Count); - Array.Copy(source.colors.ToArray(), 0, colors, numVerts, source.verts.Count); - Array.Copy(BufferCaches[transformIndex], 0, transformIndexBuffer, numVerts, source.verts.Count); - int triCount = source.triangles.Count; - for (int i = 0; i < triCount; i++) { - triangles.Add(source.triangles[i] + numVerts); - } - numVerts += source.verts.Count; - } + // All MeshInfos that we need to render. + private HashSet allMeshInfos = new HashSet(); + // IDs of meshes that we have yet to add-to/remove- from the ReMesher, and haven't gotten around to doing yet. + // We only add when ReMesher.Flush() is called so that a bunch of those operations can be batched. + private HashSet meshesPendingAdd = new HashSet(); + private HashSet meshesPendingRemove = new HashSet(); + private HashSet meshInfosPendingRegeneration = new HashSet(); - /// - /// Updates the array of transform mats for the meshes this info renders. - /// - public void UpdateTransforms(Model model) { - foreach (int meshId in mmeshes) { - xformMats[mmeshToTransformIndex[meshId]] = model.GetMesh(meshId).GetJitteredTransform(); - } - } - - /// - /// Sets the transform mat array as a shader uniform for the supplied material. - /// - public void SetTransforms(Material mat) { - mat.SetMatrixArray("_RemesherMeshTransforms", xformMats); - } - - /// - /// Regenerates the buffers used for constructing meshes. We need to do this when meshes are removed from the - /// info, as doing that invalidates our triangle indices (because they are calculated based on offset). - /// - public void Regenerate() { - // Every vert we don't care about will form a degenerate triangle. Keeps our first two verts. - triangles.Clear(); - Array.Clear(verts, 2, verts.Length - 2); - numVerts = 2; - - foreach (int meshId in mmeshes) { - foreach (MeshGenContext context in meshGenContexts[meshId]) { - AddContext(meshId, context); - } - } - needsRegeneration = false; - numPendingVerts = 0; - } - - public MeshInfo() { - // This is a really undesirable hack. Since we're passing an additional transform matrix into the shader, it breaks - // Unity's view frustum culling, and the meshinfo will disappear at various angles. By adding these two extreme - // vertices, it causes the mesh to never be culled because it has an enormous bounding box. - // While setting the bounds on the Unity mesh directly should in theory work, in practice it seems not to. - verts[0] = new Vector3(999999f, 999999f, 999999f); - verts[1] = new Vector3(-999999f, -999999f, -999999f); - numVerts = 2; - - for (int i = 0; i < MAX_MMESH_PER_MESHINFO; i++) { - xformMats[i] = Matrix4x4.identity; - availableTransformIndices.Enqueue(i); + // Meshinfos should not be modified outside of remesher. + // This exists to enable exporting coalesced meshes. + public HashSet GetAllMeshInfos() + { + Flush(); + return allMeshInfos; } - } - } - - // For each MMesh, the set of MeshInfos that MMesh contributes triangles to. - private Dictionary> meshInfosByMesh = new Dictionary>(); - - // All MeshInfos that we need to render. - private HashSet allMeshInfos = new HashSet(); - - // IDs of meshes that we have yet to add-to/remove- from the ReMesher, and haven't gotten around to doing yet. - // We only add when ReMesher.Flush() is called so that a bunch of those operations can be batched. - private HashSet meshesPendingAdd = new HashSet(); - private HashSet meshesPendingRemove = new HashSet(); - private HashSet meshInfosPendingRegeneration = new HashSet(); - // Meshinfos should not be modified outside of remesher. - // This exists to enable exporting coalesced meshes. - public HashSet GetAllMeshInfos() { - Flush(); - return allMeshInfos; - } - - /// - /// Initializes a set of buffers that are used to efficiently add multiples of the same value to a list. - /// Each of these lists contains an array of identical Vector2 values, which lets us efficiently use Array.Copy - /// to set a large range to the same value. - /// - public static void InitBufferCaches() { - BufferCaches = new List(MAX_MMESH_PER_MESHINFO); - for (int i = 0; i < MAX_MMESH_PER_MESHINFO; i++) { - BufferCaches.Add(new Vector2[MAX_VERTS_PER_MESH]); - Vector2 val = new Vector2(i, 0f); - for (int j = 0; j < MAX_VERTS_PER_MESH; j++) { - BufferCaches[i][j] = val; + /// + /// Initializes a set of buffers that are used to efficiently add multiples of the same value to a list. + /// Each of these lists contains an array of identical Vector2 values, which lets us efficiently use Array.Copy + /// to set a large range to the same value. + /// + public static void InitBufferCaches() + { + BufferCaches = new List(MAX_MMESH_PER_MESHINFO); + for (int i = 0; i < MAX_MMESH_PER_MESHINFO; i++) + { + BufferCaches.Add(new Vector2[MAX_VERTS_PER_MESH]); + Vector2 val = new Vector2(i, 0f); + for (int j = 0; j < MAX_VERTS_PER_MESH; j++) + { + BufferCaches[i][j] = val; + } + } } - } - } - - // All MeshInfos that have room for more triangles to be added, by material. - private Dictionary> meshInfosByMaterial = new Dictionary>(); - - public void Clear() { - meshInfosByMaterial.Clear(); - allMeshInfos.Clear(); - meshesPendingAdd.Clear(); - meshesPendingRemove.Clear(); - meshInfosPendingRegeneration.Clear(); - } - - /// - /// Add a mesh to be rendered. A mesh with the same id must exist in the model. - /// - /// The mesh. - public void AddMesh(MMesh mmesh) { - // Generate or fetch the triangles, etc for the mesh. - // TODO(bug): This only works because Model.cs happens to update the model before calling the ReMesher. - // We shoud make that more robust. - - // Generate the Unity meshes for this mesh. - // TODO(bug): We should cache these Meshes for MMeshes too, if possible. - meshesPendingAdd.Add(mmesh.id); - } - /// - /// Flushes any pending deferred operations on the ReMesher. - /// - public void Flush() { - ActuallyRemoveMeshes(); - GenerateMeshesForMMeshes(); - - // For all the MeshInfos that have had triangles modified, regenerate their Unity Meshes. - foreach (MeshInfo meshInfo in meshInfosPendingRegeneration) { - RegenerateMesh(meshInfo); - } - - meshInfosPendingRegeneration.Clear(); - } - - /// - /// Marks a mesh to be removed from being rendered. - /// Actual removal will happen in batch the next time Flush is called. - /// - /// The mesh id. - /// - /// If the given mesh is not actually being rendered. - /// - public void RemoveMesh(int meshId) { - meshesPendingAdd.Remove(meshId); - meshesPendingRemove.Add(meshId); - } + // All MeshInfos that have room for more triangles to be added, by material. + private Dictionary> meshInfosByMaterial = new Dictionary>(); - /// - /// Removes the given meshes from ReMesher immediately. - /// Note that this will update meshesPendingAdd with the IDs of meshes who contributed to the same - /// MeshInfo as a mesh being removed. - /// - private void ActuallyRemoveMeshes() { - // The meshinfos affected by removing this mesh. - foreach (int meshId in meshesPendingRemove) { - HashSet affectedMeshInfos; - if (!meshInfosByMesh.TryGetValue(meshId, out affectedMeshInfos)) { - continue; // Nothing to do here. + public void Clear() + { + meshInfosByMaterial.Clear(); + allMeshInfos.Clear(); + meshesPendingAdd.Clear(); + meshesPendingRemove.Clear(); + meshInfosPendingRegeneration.Clear(); } - // Recursively find the transitive closure of all MeshInfos we need to re-add after - // removing this mesh. - foreach (MeshInfo info in affectedMeshInfos) { - info.RemoveMesh(meshId); - meshInfosPendingRegeneration.Add(info); + /// + /// Add a mesh to be rendered. A mesh with the same id must exist in the model. + /// + /// The mesh. + public void AddMesh(MMesh mmesh) + { + // Generate or fetch the triangles, etc for the mesh. + // TODO(bug): This only works because Model.cs happens to update the model before calling the ReMesher. + // We shoud make that more robust. + + // Generate the Unity meshes for this mesh. + // TODO(bug): We should cache these Meshes for MMeshes too, if possible. + meshesPendingAdd.Add(mmesh.id); } - // Remove this mesh's entry from the list of mesh infos per mesh. - meshInfosByMesh.Remove(meshId); - } - - meshesPendingRemove.Clear(); - } + /// + /// Flushes any pending deferred operations on the ReMesher. + /// + public void Flush() + { + ActuallyRemoveMeshes(); + GenerateMeshesForMMeshes(); + + // For all the MeshInfos that have had triangles modified, regenerate their Unity Meshes. + foreach (MeshInfo meshInfo in meshInfosPendingRegeneration) + { + RegenerateMesh(meshInfo); + } + + meshInfosPendingRegeneration.Clear(); + } - /// - /// For a list of MMeshes, add their triangles to "unfull" MeshInfos. When any of those - /// MeshInfos becomes full, create a new MeshInfo. Once we've added all the triangles to MeshInfos, - /// we need to regenerate all of the Unity Meshes for those MeshInfos. - /// - /// - private void GenerateMeshesForMMeshes() { - Model model = PeltzerMain.Instance.model; - HashSet meshesStillPendingAdd = new HashSet(); - - foreach (int meshId in meshesPendingAdd) { - // Since this method is called lazily on Flush(), we may have an out of date mesh ID that no longer - // exists in the model. In that case, skip it. - if (!model.HasMesh(meshId)) continue; - - Dictionary components = - model.meshRepresentationCache.FetchMeshSpaceComponentsForMesh(meshId, /* abortOnTooManyCacheMisses */ false); - if (components == null) { - meshesStillPendingAdd.Add(meshId); - continue; + /// + /// Marks a mesh to be removed from being rendered. + /// Actual removal will happen in batch the next time Flush is called. + /// + /// The mesh id. + /// + /// If the given mesh is not actually being rendered. + /// + public void RemoveMesh(int meshId) + { + meshesPendingAdd.Remove(meshId); + meshesPendingRemove.Add(meshId); } - HashSet meshInfos = new HashSet(); - foreach (KeyValuePair pair in components) { - // Doing the Assert within an if statement to prevent the string concatenation from occurring unless the - // condition has failed. The concatenation was expensive enough to show up in profiling for large models. - if (pair.Value.verts.Count >= MAX_VERTS_PER_MMESH) { - AssertOrThrow.True(pair.Value.verts.Count < MAX_VERTS_PER_MMESH, - "MMesh has too many vertices ( " + pair.Value.verts.Count + " vs a max of " + MAX_VERTS_PER_MMESH); - } - // Find or create an unfull MeshInfo for the given material - MeshInfo infoForMaterial = GetInfoForMaterialAndVertCount(pair.Key, pair.Value.verts.Count); - - infoForMaterial.AddMesh(meshId, pair.Value); - - meshInfosPendingRegeneration.Add(infoForMaterial); - meshInfos.Add(infoForMaterial); + /// + /// Removes the given meshes from ReMesher immediately. + /// Note that this will update meshesPendingAdd with the IDs of meshes who contributed to the same + /// MeshInfo as a mesh being removed. + /// + private void ActuallyRemoveMeshes() + { + // The meshinfos affected by removing this mesh. + foreach (int meshId in meshesPendingRemove) + { + HashSet affectedMeshInfos; + if (!meshInfosByMesh.TryGetValue(meshId, out affectedMeshInfos)) + { + continue; // Nothing to do here. + } + + // Recursively find the transitive closure of all MeshInfos we need to re-add after + // removing this mesh. + foreach (MeshInfo info in affectedMeshInfos) + { + info.RemoveMesh(meshId); + meshInfosPendingRegeneration.Add(info); + } + + // Remove this mesh's entry from the list of mesh infos per mesh. + meshInfosByMesh.Remove(meshId); + } + + meshesPendingRemove.Clear(); } - meshInfosByMesh[meshId] = meshInfos; - } - meshesPendingAdd = meshesStillPendingAdd; - } + /// + /// For a list of MMeshes, add their triangles to "unfull" MeshInfos. When any of those + /// MeshInfos becomes full, create a new MeshInfo. Once we've added all the triangles to MeshInfos, + /// we need to regenerate all of the Unity Meshes for those MeshInfos. + /// + /// + private void GenerateMeshesForMMeshes() + { + Model model = PeltzerMain.Instance.model; + HashSet meshesStillPendingAdd = new HashSet(); + + foreach (int meshId in meshesPendingAdd) + { + // Since this method is called lazily on Flush(), we may have an out of date mesh ID that no longer + // exists in the model. In that case, skip it. + if (!model.HasMesh(meshId)) continue; + + Dictionary components = + model.meshRepresentationCache.FetchMeshSpaceComponentsForMesh(meshId, /* abortOnTooManyCacheMisses */ false); + if (components == null) + { + meshesStillPendingAdd.Add(meshId); + continue; + } + + HashSet meshInfos = new HashSet(); + foreach (KeyValuePair pair in components) + { + // Doing the Assert within an if statement to prevent the string concatenation from occurring unless the + // condition has failed. The concatenation was expensive enough to show up in profiling for large models. + if (pair.Value.verts.Count >= MAX_VERTS_PER_MMESH) + { + AssertOrThrow.True(pair.Value.verts.Count < MAX_VERTS_PER_MMESH, + "MMesh has too many vertices ( " + pair.Value.verts.Count + " vs a max of " + MAX_VERTS_PER_MMESH); + } + // Find or create an unfull MeshInfo for the given material + MeshInfo infoForMaterial = GetInfoForMaterialAndVertCount(pair.Key, pair.Value.verts.Count); + + infoForMaterial.AddMesh(meshId, pair.Value); + + meshInfosPendingRegeneration.Add(infoForMaterial); + meshInfos.Add(infoForMaterial); + } + meshInfosByMesh[meshId] = meshInfos; + } + + meshesPendingAdd = meshesStillPendingAdd; + } - /// - /// Gets a MeshInfo with sufficient space for the given material, or creates a new one if none currently exists. - /// - private MeshInfo GetInfoForMaterialAndVertCount(int materialId, int spaceNeeded) { - List infosForMaterial; - MaterialAndColor materialAndColor = MaterialRegistry.GetMaterialAndColorById(materialId); - meshInfosByMaterial.TryGetValue(materialAndColor.material, out infosForMaterial); - if (infosForMaterial == null) { - infosForMaterial = new List(); - meshInfosByMaterial.Add(materialAndColor.material, infosForMaterial); - } - // Just return the first info with room. - for (int i = 0; i < infosForMaterial.Count; i++) { - MeshInfo curInfo = infosForMaterial[i]; - if (curInfo.numVerts + curInfo.numPendingVerts + spaceNeeded < MAX_VERTS_PER_MESH - && curInfo.GetNumMeshes() + 1 < MAX_MMESH_PER_MESHINFO) { - return curInfo; + /// + /// Gets a MeshInfo with sufficient space for the given material, or creates a new one if none currently exists. + /// + private MeshInfo GetInfoForMaterialAndVertCount(int materialId, int spaceNeeded) + { + List infosForMaterial; + MaterialAndColor materialAndColor = MaterialRegistry.GetMaterialAndColorById(materialId); + meshInfosByMaterial.TryGetValue(materialAndColor.material, out infosForMaterial); + if (infosForMaterial == null) + { + infosForMaterial = new List(); + meshInfosByMaterial.Add(materialAndColor.material, infosForMaterial); + } + // Just return the first info with room. + for (int i = 0; i < infosForMaterial.Count; i++) + { + MeshInfo curInfo = infosForMaterial[i]; + if (curInfo.numVerts + curInfo.numPendingVerts + spaceNeeded < MAX_VERTS_PER_MESH + && curInfo.GetNumMeshes() + 1 < MAX_MMESH_PER_MESHINFO) + { + return curInfo; + } + } + // And create one if no viable option was found. + MeshInfo newInfoForMaterial = new MeshInfo(); + // Cloned to make sure it has its own matrix transform uniform, otherwise other things rendering using the + // same material will have the wrong transforms. + newInfoForMaterial.materialAndColor = materialAndColor.Clone(); + allMeshInfos.Add(newInfoForMaterial); + meshInfosByMaterial[materialAndColor.material].Add(newInfoForMaterial); + return newInfoForMaterial; } - } - // And create one if no viable option was found. - MeshInfo newInfoForMaterial = new MeshInfo(); - // Cloned to make sure it has its own matrix transform uniform, otherwise other things rendering using the - // same material will have the wrong transforms. - newInfoForMaterial.materialAndColor = materialAndColor.Clone(); - allMeshInfos.Add(newInfoForMaterial); - meshInfosByMaterial[materialAndColor.material].Add(newInfoForMaterial); - return newInfoForMaterial; - } - /// - /// Generate the Unity Mesh for a given MeshInfo. - /// - private void RegenerateMesh(MeshInfo meshInfo) { - if (meshInfo.needsRegeneration) { - meshInfo.Regenerate(); - } - meshInfo.mesh.Clear(); - meshInfo.mesh.vertices = meshInfo.verts; - meshInfo.mesh.SetTriangles(meshInfo.triangles, /* Submesh */ 0); - meshInfo.mesh.colors32 = meshInfo.colors; - meshInfo.mesh.normals = meshInfo.normals; - meshInfo.mesh.uv2 = meshInfo.transformIndexBuffer; - } + /// + /// Generate the Unity Mesh for a given MeshInfo. + /// + private void RegenerateMesh(MeshInfo meshInfo) + { + if (meshInfo.needsRegeneration) + { + meshInfo.Regenerate(); + } + meshInfo.mesh.Clear(); + meshInfo.mesh.vertices = meshInfo.verts; + meshInfo.mesh.SetTriangles(meshInfo.triangles, /* Submesh */ 0); + meshInfo.mesh.colors32 = meshInfo.colors; + meshInfo.mesh.normals = meshInfo.normals; + meshInfo.mesh.uv2 = meshInfo.transformIndexBuffer; + } - /// - /// Render the meshes. - /// - public void Render(Model model) { - // Flush to apply any outstanding changes, if necessary. - Flush(); - - WorldSpace worldSpace = PeltzerMain.Instance.worldSpace; - - foreach (MeshInfo meshInfo in allMeshInfos) { - meshInfo.UpdateTransforms(model); - meshInfo.SetTransforms(meshInfo.materialAndColor.material); - Graphics.DrawMesh(meshInfo.mesh, worldSpace.modelToWorld, meshInfo.materialAndColor.material, /* Layer */ 0); - if (meshInfo.materialAndColor.material2 != null) { - meshInfo.SetTransforms(meshInfo.materialAndColor.material2); - Graphics.DrawMesh(meshInfo.mesh, worldSpace.modelToWorld, meshInfo.materialAndColor.material2, /* Layer */ 0); - } - } - } + /// + /// Render the meshes. + /// + public void Render(Model model) + { + // Flush to apply any outstanding changes, if necessary. + Flush(); + + WorldSpace worldSpace = PeltzerMain.Instance.worldSpace; + + foreach (MeshInfo meshInfo in allMeshInfos) + { + meshInfo.UpdateTransforms(model); + meshInfo.SetTransforms(meshInfo.materialAndColor.material); + Graphics.DrawMesh(meshInfo.mesh, worldSpace.modelToWorld, meshInfo.materialAndColor.material, /* Layer */ 0); + if (meshInfo.materialAndColor.material2 != null) + { + meshInfo.SetTransforms(meshInfo.materialAndColor.material2); + Graphics.DrawMesh(meshInfo.mesh, worldSpace.modelToWorld, meshInfo.materialAndColor.material2, /* Layer */ 0); + } + } + } - /// - /// Update transformations for all meshInfos in the given model. - /// - public void UpdateTransforms(Model model) { - foreach (MeshInfo meshInfo in allMeshInfos) { - meshInfo.UpdateTransforms(model); - } - } + /// + /// Update transformations for all meshInfos in the given model. + /// + public void UpdateTransforms(Model model) + { + foreach (MeshInfo meshInfo in allMeshInfos) + { + meshInfo.UpdateTransforms(model); + } + } - // Visible for testing - public bool HasMesh(int meshId) { - return meshInfosByMesh.ContainsKey(meshId); - } + // Visible for testing + public bool HasMesh(int meshId) + { + return meshInfosByMesh.ContainsKey(meshId); + } - // Visible for testing. Walk all MeshInfos and count how many depend on a given mesh. - public int MeshInMeshInfosCount(int meshId) { - int count = 0; - foreach(MeshInfo meshInfo in allMeshInfos) { - if (meshInfo.HasMesh(meshId)) { - count++; - } - } - return count; + // Visible for testing. Walk all MeshInfos and count how many depend on a given mesh. + public int MeshInMeshInfosCount(int meshId) + { + int count = 0; + foreach (MeshInfo meshInfo in allMeshInfos) + { + if (meshInfo.HasMesh(meshId)) + { + count++; + } + } + return count; + } } - } } diff --git a/Assets/Scripts/model/render/UXEffects/ContinuousAxisStickEffect.cs b/Assets/Scripts/model/render/UXEffects/ContinuousAxisStickEffect.cs index a263ed83..e4d6738c 100644 --- a/Assets/Scripts/model/render/UXEffects/ContinuousAxisStickEffect.cs +++ b/Assets/Scripts/model/render/UXEffects/ContinuousAxisStickEffect.cs @@ -20,78 +20,87 @@ using UnityEngine; using com.google.apps.peltzer.client.tools.utils; -namespace com.google.apps.peltzer.client.model.render { - /// - /// UX Effect which renders guides for continuous edge sticking (an outline on the target edge when the source mesh - /// sticks to it.) - /// - class ContinuousAxisStickEffect : UXEffectManager.UXEffect { - private const float DEFAULT_DURATION = 1.0f; +namespace com.google.apps.peltzer.client.model.render +{ + /// + /// UX Effect which renders guides for continuous edge sticking (an outline on the target edge when the source mesh + /// sticks to it.) + /// + class ContinuousAxisStickEffect : UXEffectManager.UXEffect + { + private const float DEFAULT_DURATION = 1.0f; - Vector3 basePreviewPosition; - private Mesh previewMesh; + Vector3 basePreviewPosition; + private Mesh previewMesh; - private bool inSnapThreshhold = false; + private bool inSnapThreshhold = false; - public Vector3[] snapLines = new Vector3[0]; - public Vector3[] snapNormals = new Vector3[0]; - public Vector2[] snapSelectData = new Vector2[0]; - private int[] snapLineIndices = new int[0]; + public Vector3[] snapLines = new Vector3[0]; + public Vector3[] snapNormals = new Vector3[0]; + public Vector2[] snapSelectData = new Vector2[0]; + private int[] snapLineIndices = new int[0]; - - /// - /// Constructs the effect, Initialize must still be called before the effect starts to take place. - /// - /// The MMesh id of the target mesh to play the shader on. - public ContinuousAxisStickEffect() { - previewMesh = new Mesh(); - } - public override void Initialize(MeshRepresentationCache cache, MaterialLibrary materialLibrary, - WorldSpace worldSpace) { - base.Initialize(cache, materialLibrary.edgeHighlightMaterial, worldSpace); - } + /// + /// Constructs the effect, Initialize must still be called before the effect starts to take place. + /// + /// The MMesh id of the target mesh to play the shader on. + public ContinuousAxisStickEffect() + { + previewMesh = new Mesh(); + } - public override void Render() { - float scaleFactor = InactiveRenderer.GetEdgeScaleFactor(worldSpace); - effectMaterial.SetFloat("_PointSphereRadius", scaleFactor); - Graphics.DrawMesh(previewMesh, - worldSpace.modelToWorld, - effectMaterial, - 0); // Layer - } + public override void Initialize(MeshRepresentationCache cache, MaterialLibrary materialLibrary, + WorldSpace worldSpace) + { + base.Initialize(cache, materialLibrary.edgeHighlightMaterial, worldSpace); + } - public override void Finish() { - Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0f, 0f, 0f)); - Shader.SetGlobalVector("_FXPointLightPosition", new Vector4(0f, 0f, 0f, 1f)); - UXEffectManager.GetEffectManager().EndEffect(this); - } + public override void Render() + { + float scaleFactor = InactiveRenderer.GetEdgeScaleFactor(worldSpace); + effectMaterial.SetFloat("_PointSphereRadius", scaleFactor); + Graphics.DrawMesh(previewMesh, + worldSpace.modelToWorld, + effectMaterial, + 0); // Layer + } - /// - /// Updates the effect based on the supplied EdgeInfo. - /// - public void UpdateFromAxis(Vector3 start, Vector3 end) { - int sizeNeeded = 2; - if (snapLines.Length != sizeNeeded) { - Array.Resize(ref snapLines, sizeNeeded); - Array.Resize(ref snapLineIndices, sizeNeeded); - Array.Resize(ref snapNormals, sizeNeeded); - Array.Resize(ref snapSelectData, sizeNeeded); - for (int i = 0; i < sizeNeeded; i++) { - snapLineIndices[i] = i; - snapNormals[i] = Vector3.forward; - snapSelectData[i] = Vector2.one; + public override void Finish() + { + Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0f, 0f, 0f)); + Shader.SetGlobalVector("_FXPointLightPosition", new Vector4(0f, 0f, 0f, 1f)); + UXEffectManager.GetEffectManager().EndEffect(this); } - } - // Snap Line - snapLines[0] = start; - snapLines[1] = end; - previewMesh.Clear(); - previewMesh.vertices = snapLines; - previewMesh.normals = snapNormals; - previewMesh.uv = snapSelectData; - previewMesh.SetIndices(snapLineIndices, MeshTopology.Lines, 0 /* submesh id */, false /* recalculate bounds */); + /// + /// Updates the effect based on the supplied EdgeInfo. + /// + public void UpdateFromAxis(Vector3 start, Vector3 end) + { + int sizeNeeded = 2; + if (snapLines.Length != sizeNeeded) + { + Array.Resize(ref snapLines, sizeNeeded); + Array.Resize(ref snapLineIndices, sizeNeeded); + Array.Resize(ref snapNormals, sizeNeeded); + Array.Resize(ref snapSelectData, sizeNeeded); + for (int i = 0; i < sizeNeeded; i++) + { + snapLineIndices[i] = i; + snapNormals[i] = Vector3.forward; + snapSelectData[i] = Vector2.one; + } + } + // Snap Line + snapLines[0] = start; + snapLines[1] = end; + + previewMesh.Clear(); + previewMesh.vertices = snapLines; + previewMesh.normals = snapNormals; + previewMesh.uv = snapSelectData; + previewMesh.SetIndices(snapLineIndices, MeshTopology.Lines, 0 /* submesh id */, false /* recalculate bounds */); + } } - } } diff --git a/Assets/Scripts/model/render/UXEffects/ContinuousEdgeStickEffect.cs b/Assets/Scripts/model/render/UXEffects/ContinuousEdgeStickEffect.cs index f375955b..4f249cc4 100644 --- a/Assets/Scripts/model/render/UXEffects/ContinuousEdgeStickEffect.cs +++ b/Assets/Scripts/model/render/UXEffects/ContinuousEdgeStickEffect.cs @@ -20,78 +20,87 @@ using UnityEngine; using com.google.apps.peltzer.client.tools.utils; -namespace com.google.apps.peltzer.client.model.render { - /// - /// UX Effect which renders guides for continuous edge sticking (an outline on the target edge when the source mesh - /// sticks to it.) - /// - class ContinuousEdgeStickEffect : UXEffectManager.UXEffect { - private const float DEFAULT_DURATION = 1.0f; - - Vector3 basePreviewPosition; - private Mesh previewMesh; +namespace com.google.apps.peltzer.client.model.render +{ + /// + /// UX Effect which renders guides for continuous edge sticking (an outline on the target edge when the source mesh + /// sticks to it.) + /// + class ContinuousEdgeStickEffect : UXEffectManager.UXEffect + { + private const float DEFAULT_DURATION = 1.0f; - private bool inSnapThreshhold = false; + Vector3 basePreviewPosition; + private Mesh previewMesh; - public Vector3[] snapLines = new Vector3[0]; - public Vector3[] snapNormals = new Vector3[0]; - public Vector2[] snapSelectData = new Vector2[0]; - private int[] snapLineIndices = new int[0]; + private bool inSnapThreshhold = false; + public Vector3[] snapLines = new Vector3[0]; + public Vector3[] snapNormals = new Vector3[0]; + public Vector2[] snapSelectData = new Vector2[0]; + private int[] snapLineIndices = new int[0]; - /// - /// Constructs the effect, Initialize must still be called before the effect starts to take place. - /// - /// The MMesh id of the target mesh to play the shader on. - public ContinuousEdgeStickEffect() { - previewMesh = new Mesh(); - } - public override void Initialize(MeshRepresentationCache cache, MaterialLibrary materialLibrary, - WorldSpace worldSpace) { - base.Initialize(cache, materialLibrary.edgeHighlightMaterial, worldSpace); - } + /// + /// Constructs the effect, Initialize must still be called before the effect starts to take place. + /// + /// The MMesh id of the target mesh to play the shader on. + public ContinuousEdgeStickEffect() + { + previewMesh = new Mesh(); + } - public override void Render() { - float scaleFactor = InactiveRenderer.GetEdgeScaleFactor(worldSpace); - effectMaterial.SetFloat("_PointSphereRadius", scaleFactor); - Graphics.DrawMesh(previewMesh, - worldSpace.modelToWorld, - effectMaterial, - 0); // Layer - } + public override void Initialize(MeshRepresentationCache cache, MaterialLibrary materialLibrary, + WorldSpace worldSpace) + { + base.Initialize(cache, materialLibrary.edgeHighlightMaterial, worldSpace); + } - public override void Finish() { - Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0f, 0f, 0f)); - Shader.SetGlobalVector("_FXPointLightPosition", new Vector4(0f, 0f, 0f, 1f)); - UXEffectManager.GetEffectManager().EndEffect(this); - } + public override void Render() + { + float scaleFactor = InactiveRenderer.GetEdgeScaleFactor(worldSpace); + effectMaterial.SetFloat("_PointSphereRadius", scaleFactor); + Graphics.DrawMesh(previewMesh, + worldSpace.modelToWorld, + effectMaterial, + 0); // Layer + } - /// - /// Updates the effect based on the supplied EdgeInfo. - /// - public void UpdateFromEdge(EdgeInfo edge) { - int sizeNeeded = 2; - if (snapLines.Length != sizeNeeded) { - Array.Resize(ref snapLines, sizeNeeded); - Array.Resize(ref snapLineIndices, sizeNeeded); - Array.Resize(ref snapNormals, sizeNeeded); - Array.Resize(ref snapSelectData, sizeNeeded); - for (int i = 0; i < sizeNeeded; i++) { - snapLineIndices[i] = i; - snapNormals[i] = Vector3.up; - snapSelectData[i] = Vector2.one; + public override void Finish() + { + Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0f, 0f, 0f)); + Shader.SetGlobalVector("_FXPointLightPosition", new Vector4(0f, 0f, 0f, 1f)); + UXEffectManager.GetEffectManager().EndEffect(this); } - } - // Snap Line - snapLines[0] = edge.edgeStart; - snapLines[1] = edge.edgeStart + edge.edgeVector; - previewMesh.Clear(); - previewMesh.vertices = snapLines; - previewMesh.normals = snapNormals; - previewMesh.uv = snapSelectData; - previewMesh.SetIndices(snapLineIndices, MeshTopology.Lines, 0 /* submesh id */, false /* recalculate bounds */); + /// + /// Updates the effect based on the supplied EdgeInfo. + /// + public void UpdateFromEdge(EdgeInfo edge) + { + int sizeNeeded = 2; + if (snapLines.Length != sizeNeeded) + { + Array.Resize(ref snapLines, sizeNeeded); + Array.Resize(ref snapLineIndices, sizeNeeded); + Array.Resize(ref snapNormals, sizeNeeded); + Array.Resize(ref snapSelectData, sizeNeeded); + for (int i = 0; i < sizeNeeded; i++) + { + snapLineIndices[i] = i; + snapNormals[i] = Vector3.up; + snapSelectData[i] = Vector2.one; + } + } + // Snap Line + snapLines[0] = edge.edgeStart; + snapLines[1] = edge.edgeStart + edge.edgeVector; + + previewMesh.Clear(); + previewMesh.vertices = snapLines; + previewMesh.normals = snapNormals; + previewMesh.uv = snapSelectData; + previewMesh.SetIndices(snapLineIndices, MeshTopology.Lines, 0 /* submesh id */, false /* recalculate bounds */); + } } - } } diff --git a/Assets/Scripts/model/render/UXEffects/ContinuousFaceSnapEffect.cs b/Assets/Scripts/model/render/UXEffects/ContinuousFaceSnapEffect.cs index a03f91a8..21f26752 100644 --- a/Assets/Scripts/model/render/UXEffects/ContinuousFaceSnapEffect.cs +++ b/Assets/Scripts/model/render/UXEffects/ContinuousFaceSnapEffect.cs @@ -20,102 +20,113 @@ using UnityEngine; using com.google.apps.peltzer.client.tools.utils; -namespace com.google.apps.peltzer.client.model.render { - /// - /// UX Effect which renders guides for continuous face snapping (an outline around the target face and source face, - /// as well as a line from the source face center to the snap point on the target face) - /// - class ContinuousFaceSnapEffect : UXEffectManager.UXEffect { - private const float DEFAULT_DURATION = 1.0f; - - Vector3 basePreviewPosition; - private Mesh previewMesh; +namespace com.google.apps.peltzer.client.model.render +{ + /// + /// UX Effect which renders guides for continuous face snapping (an outline around the target face and source face, + /// as well as a line from the source face center to the snap point on the target face) + /// + class ContinuousFaceSnapEffect : UXEffectManager.UXEffect + { + private const float DEFAULT_DURATION = 1.0f; - private bool inSnapThreshhold = false; + Vector3 basePreviewPosition; + private Mesh previewMesh; - public Vector3[] snapLines = new Vector3[0]; - public Vector3[] snapNormals = new Vector3[0]; - public Vector2[] snapSelectData = new Vector2[0]; - private int[] snapLineIndices = new int[0]; + private bool inSnapThreshhold = false; + public Vector3[] snapLines = new Vector3[0]; + public Vector3[] snapNormals = new Vector3[0]; + public Vector2[] snapSelectData = new Vector2[0]; + private int[] snapLineIndices = new int[0]; - /// - /// Constructs the effect, Initialize must still be called before the effect starts to take place. - /// - /// The MMesh id of the target mesh to play the shader on. - public ContinuousFaceSnapEffect() { - previewMesh = new Mesh(); - } - public override void Initialize(MeshRepresentationCache cache, MaterialLibrary materialLibrary, - WorldSpace worldSpace) { - base.Initialize(cache, materialLibrary.edgeHighlightMaterial, worldSpace); - } + /// + /// Constructs the effect, Initialize must still be called before the effect starts to take place. + /// + /// The MMesh id of the target mesh to play the shader on. + public ContinuousFaceSnapEffect() + { + previewMesh = new Mesh(); + } - public override void Render() { - float scaleFactor = InactiveRenderer.GetEdgeScaleFactor(worldSpace); - effectMaterial.SetFloat("_PointSphereRadius", scaleFactor); - Graphics.DrawMesh(previewMesh, - worldSpace.modelToWorld, - effectMaterial, - 0); // Layer - } + public override void Initialize(MeshRepresentationCache cache, MaterialLibrary materialLibrary, + WorldSpace worldSpace) + { + base.Initialize(cache, materialLibrary.edgeHighlightMaterial, worldSpace); + } - public override void Finish() { - Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0f, 0f, 0f)); - Shader.SetGlobalVector("_FXPointLightPosition", new Vector4(0f, 0f, 0f, 1f)); - UXEffectManager.GetEffectManager().EndEffect(this); - } + public override void Render() + { + float scaleFactor = InactiveRenderer.GetEdgeScaleFactor(worldSpace); + effectMaterial.SetFloat("_PointSphereRadius", scaleFactor); + Graphics.DrawMesh(previewMesh, + worldSpace.modelToWorld, + effectMaterial, + 0); // Layer + } - /// - /// Updates the effect based on the supplied FaceSnapSpace. - /// - /// - public void UpdateFromSnapSpace(FaceSnapSpace faceSnapSpace) { - MMesh sourceMesh = faceSnapSpace.sourceMesh; - MMesh targetMesh = PeltzerMain.Instance.GetModel().GetMesh(faceSnapSpace.targetFaceKey.meshId); - Face sourceFace = sourceMesh.GetFace(faceSnapSpace.sourceFaceKey.faceId); - Face targetFace = targetMesh.GetFace(faceSnapSpace.targetFaceKey.faceId); - int sizeNeeded = 2 + sourceFace.vertexIds.Count * 2 + targetFace.vertexIds.Count * 2; - if (snapLines.Length != sizeNeeded) { - Array.Resize(ref snapLines, sizeNeeded); - Array.Resize(ref snapLineIndices, sizeNeeded); - Array.Resize(ref snapNormals, sizeNeeded); - Array.Resize(ref snapSelectData, sizeNeeded); - for (int i = 0; i < sizeNeeded; i++) { - snapLineIndices[i] = i; - snapNormals[i] = Vector3.up; - snapSelectData[i] = Vector2.one; + public override void Finish() + { + Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0f, 0f, 0f)); + Shader.SetGlobalVector("_FXPointLightPosition", new Vector4(0f, 0f, 0f, 1f)); + UXEffectManager.GetEffectManager().EndEffect(this); } - } - // Snap Line - snapLines[0] = faceSnapSpace.sourceFaceCenter; - snapLines[1] = faceSnapSpace.snapPoint; - int curStartIndex = 2; - Matrix4x4 xForm = Matrix4x4.TRS(faceSnapSpace.sourceMeshOffset, faceSnapSpace.sourceMeshRotation, Vector3.one); - // Source Face - for (int i = 0; i < sourceFace.vertexIds.Count; i++) { - snapLines[curStartIndex + 2 * i] = - xForm.MultiplyPoint(sourceMesh.VertexPositionInMeshCoords(sourceFace.vertexIds[i])); - snapLines[curStartIndex + 2 * i + 1] = - xForm.MultiplyPoint( - sourceMesh.VertexPositionInMeshCoords(sourceFace.vertexIds[(i + 1) % sourceFace.vertexIds.Count])); - } + /// + /// Updates the effect based on the supplied FaceSnapSpace. + /// + /// + public void UpdateFromSnapSpace(FaceSnapSpace faceSnapSpace) + { + MMesh sourceMesh = faceSnapSpace.sourceMesh; + MMesh targetMesh = PeltzerMain.Instance.GetModel().GetMesh(faceSnapSpace.targetFaceKey.meshId); + Face sourceFace = sourceMesh.GetFace(faceSnapSpace.sourceFaceKey.faceId); + Face targetFace = targetMesh.GetFace(faceSnapSpace.targetFaceKey.faceId); + int sizeNeeded = 2 + sourceFace.vertexIds.Count * 2 + targetFace.vertexIds.Count * 2; + if (snapLines.Length != sizeNeeded) + { + Array.Resize(ref snapLines, sizeNeeded); + Array.Resize(ref snapLineIndices, sizeNeeded); + Array.Resize(ref snapNormals, sizeNeeded); + Array.Resize(ref snapSelectData, sizeNeeded); + for (int i = 0; i < sizeNeeded; i++) + { + snapLineIndices[i] = i; + snapNormals[i] = Vector3.up; + snapSelectData[i] = Vector2.one; + } + } + // Snap Line + snapLines[0] = faceSnapSpace.sourceFaceCenter; + snapLines[1] = faceSnapSpace.snapPoint; - curStartIndex = curStartIndex + 2 * sourceFace.vertexIds.Count; - // Target Face - for (int i = 0; i < targetFace.vertexIds.Count; i++) { - snapLines[curStartIndex + 2 * i] = - targetMesh.VertexPositionInModelCoords(targetFace.vertexIds[i]); - snapLines[curStartIndex + 2 * i + 1] = - targetMesh.VertexPositionInModelCoords(targetFace.vertexIds[(i + 1) % targetFace.vertexIds.Count]); - } - previewMesh.Clear(); - previewMesh.vertices = snapLines; - previewMesh.normals = snapNormals; - previewMesh.uv = snapSelectData; - previewMesh.SetIndices(snapLineIndices, MeshTopology.Lines, 0 /* submesh id */, false /* recalculate bounds */); + int curStartIndex = 2; + Matrix4x4 xForm = Matrix4x4.TRS(faceSnapSpace.sourceMeshOffset, faceSnapSpace.sourceMeshRotation, Vector3.one); + // Source Face + for (int i = 0; i < sourceFace.vertexIds.Count; i++) + { + snapLines[curStartIndex + 2 * i] = + xForm.MultiplyPoint(sourceMesh.VertexPositionInMeshCoords(sourceFace.vertexIds[i])); + snapLines[curStartIndex + 2 * i + 1] = + xForm.MultiplyPoint( + sourceMesh.VertexPositionInMeshCoords(sourceFace.vertexIds[(i + 1) % sourceFace.vertexIds.Count])); + } + + curStartIndex = curStartIndex + 2 * sourceFace.vertexIds.Count; + // Target Face + for (int i = 0; i < targetFace.vertexIds.Count; i++) + { + snapLines[curStartIndex + 2 * i] = + targetMesh.VertexPositionInModelCoords(targetFace.vertexIds[i]); + snapLines[curStartIndex + 2 * i + 1] = + targetMesh.VertexPositionInModelCoords(targetFace.vertexIds[(i + 1) % targetFace.vertexIds.Count]); + } + previewMesh.Clear(); + previewMesh.vertices = snapLines; + previewMesh.normals = snapNormals; + previewMesh.uv = snapSelectData; + previewMesh.SetIndices(snapLineIndices, MeshTopology.Lines, 0 /* submesh id */, false /* recalculate bounds */); + } } - } } diff --git a/Assets/Scripts/model/render/UXEffects/ContinuousMeshSnapEffect.cs b/Assets/Scripts/model/render/UXEffects/ContinuousMeshSnapEffect.cs index 3971ba81..8f21947b 100644 --- a/Assets/Scripts/model/render/UXEffects/ContinuousMeshSnapEffect.cs +++ b/Assets/Scripts/model/render/UXEffects/ContinuousMeshSnapEffect.cs @@ -21,38 +21,45 @@ using Valve.VR; using com.google.apps.peltzer.client.tools.utils; -namespace com.google.apps.peltzer.client.model.render { - /// - /// UX Effect which renders mesh snap axes. - /// - class ContinuousMeshSnapEffect { - List highlightedFaces = new List(); - +namespace com.google.apps.peltzer.client.model.render +{ /// - /// Constructs the effect, Initialize must still be called before the effect starts to take place. + /// UX Effect which renders mesh snap axes. /// - /// The MMesh id of the target mesh to play the shader on. - public ContinuousMeshSnapEffect() { - } + class ContinuousMeshSnapEffect + { + List highlightedFaces = new List(); - public void Finish() { - HighlightUtils highlightUtils = PeltzerMain.Instance.highlightUtils; - foreach (FaceKey highlightedFace in highlightedFaces) { - highlightUtils.TurnOff(highlightedFace); - } - } + /// + /// Constructs the effect, Initialize must still be called before the effect starts to take place. + /// + /// The MMesh id of the target mesh to play the shader on. + public ContinuousMeshSnapEffect() + { + } + + public void Finish() + { + HighlightUtils highlightUtils = PeltzerMain.Instance.highlightUtils; + foreach (FaceKey highlightedFace in highlightedFaces) + { + highlightUtils.TurnOff(highlightedFace); + } + } - public void UpdateFromSnapSpace(MeshSnapSpace meshSnapSpace) { - MMesh targetMesh = PeltzerMain.Instance.GetModel().GetMesh(meshSnapSpace.targetMeshId); - HighlightUtils highlightUtils = PeltzerMain.Instance.highlightUtils; + public void UpdateFromSnapSpace(MeshSnapSpace meshSnapSpace) + { + MMesh targetMesh = PeltzerMain.Instance.GetModel().GetMesh(meshSnapSpace.targetMeshId); + HighlightUtils highlightUtils = PeltzerMain.Instance.highlightUtils; - foreach (Face targetFace in targetMesh.GetFaces()) { - FaceKey faceKey = new core.FaceKey(targetMesh.id, targetFace.id); - highlightUtils.TurnOn(faceKey); - highlightUtils.SetFaceStyleToSelect(faceKey, targetMesh.offset); + foreach (Face targetFace in targetMesh.GetFaces()) + { + FaceKey faceKey = new core.FaceKey(targetMesh.id, targetFace.id); + highlightUtils.TurnOn(faceKey); + highlightUtils.SetFaceStyleToSelect(faceKey, targetMesh.offset); - highlightedFaces.Add(faceKey); - } + highlightedFaces.Add(faceKey); + } + } } - } } diff --git a/Assets/Scripts/model/render/UXEffects/ContinuousPointStickEffect.cs b/Assets/Scripts/model/render/UXEffects/ContinuousPointStickEffect.cs index 5ac1d5b7..33da1d43 100644 --- a/Assets/Scripts/model/render/UXEffects/ContinuousPointStickEffect.cs +++ b/Assets/Scripts/model/render/UXEffects/ContinuousPointStickEffect.cs @@ -20,76 +20,85 @@ using UnityEngine; using com.google.apps.peltzer.client.tools.utils; -namespace com.google.apps.peltzer.client.model.render { - /// - /// UX Effect which renders guides for continuous edge sticking (an outline on the target edge when the source mesh - /// sticks to it.) - /// - class ContinuousPointStickEffect : UXEffectManager.UXEffect { - private const float DEFAULT_DURATION = 1.0f; +namespace com.google.apps.peltzer.client.model.render +{ + /// + /// UX Effect which renders guides for continuous edge sticking (an outline on the target edge when the source mesh + /// sticks to it.) + /// + class ContinuousPointStickEffect : UXEffectManager.UXEffect + { + private const float DEFAULT_DURATION = 1.0f; - Vector3 basePreviewPosition; - private Mesh previewMesh; + Vector3 basePreviewPosition; + private Mesh previewMesh; - private bool inSnapThreshhold = false; + private bool inSnapThreshhold = false; - public Vector3[] snapLines = new Vector3[0]; - public Vector3[] snapNormals = new Vector3[0]; - public Vector2[] snapSelectData = new Vector2[0]; - private int[] snapLineIndices = new int[0]; + public Vector3[] snapLines = new Vector3[0]; + public Vector3[] snapNormals = new Vector3[0]; + public Vector2[] snapSelectData = new Vector2[0]; + private int[] snapLineIndices = new int[0]; - - /// - /// Constructs the effect, Initialize must still be called before the effect starts to take place. - /// - /// The MMesh id of the target mesh to play the shader on. - public ContinuousPointStickEffect() { - previewMesh = new Mesh(); - } - public override void Initialize(MeshRepresentationCache cache, MaterialLibrary materialLibrary, - WorldSpace worldSpace) { - base.Initialize(cache, materialLibrary.pointHighlightMaterial, worldSpace); - } + /// + /// Constructs the effect, Initialize must still be called before the effect starts to take place. + /// + /// The MMesh id of the target mesh to play the shader on. + public ContinuousPointStickEffect() + { + previewMesh = new Mesh(); + } - public override void Render() { - float scaleFactor = InactiveRenderer.GetVertScaleFactor(worldSpace); - effectMaterial.SetFloat("_PointSphereRadius", scaleFactor); - Graphics.DrawMesh(previewMesh, - worldSpace.modelToWorld, - effectMaterial, - 0); // Layer - } + public override void Initialize(MeshRepresentationCache cache, MaterialLibrary materialLibrary, + WorldSpace worldSpace) + { + base.Initialize(cache, materialLibrary.pointHighlightMaterial, worldSpace); + } - public override void Finish() { - Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0f, 0f, 0f)); - Shader.SetGlobalVector("_FXPointLightPosition", new Vector4(0f, 0f, 0f, 1f)); - UXEffectManager.GetEffectManager().EndEffect(this); - } + public override void Render() + { + float scaleFactor = InactiveRenderer.GetVertScaleFactor(worldSpace); + effectMaterial.SetFloat("_PointSphereRadius", scaleFactor); + Graphics.DrawMesh(previewMesh, + worldSpace.modelToWorld, + effectMaterial, + 0); // Layer + } - /// - /// Updates the effect based on the supplied EdgeInfo. - /// - public void UpdateFromPoint(Vector3 point) { - int sizeNeeded = 1; - if (snapLines.Length != sizeNeeded) { - Array.Resize(ref snapLines, sizeNeeded); - Array.Resize(ref snapLineIndices, sizeNeeded); - Array.Resize(ref snapNormals, sizeNeeded); - Array.Resize(ref snapSelectData, sizeNeeded); - for (int i = 0; i < sizeNeeded; i++) { - snapLineIndices[i] = i; - snapNormals[i] = Vector3.forward; - snapSelectData[i] = Vector2.one; + public override void Finish() + { + Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0f, 0f, 0f)); + Shader.SetGlobalVector("_FXPointLightPosition", new Vector4(0f, 0f, 0f, 1f)); + UXEffectManager.GetEffectManager().EndEffect(this); } - } - // Snap Line - snapLines[0] = point; - previewMesh.vertices = snapLines; - previewMesh.uv = snapSelectData; - // Since we're using a point geometry shader we need to set the mesh up to supply data as points. - previewMesh.SetIndices(snapLineIndices, MeshTopology.Points, 0 /* submesh id */, false /* recalculate bounds */); + /// + /// Updates the effect based on the supplied EdgeInfo. + /// + public void UpdateFromPoint(Vector3 point) + { + int sizeNeeded = 1; + if (snapLines.Length != sizeNeeded) + { + Array.Resize(ref snapLines, sizeNeeded); + Array.Resize(ref snapLineIndices, sizeNeeded); + Array.Resize(ref snapNormals, sizeNeeded); + Array.Resize(ref snapSelectData, sizeNeeded); + for (int i = 0; i < sizeNeeded; i++) + { + snapLineIndices[i] = i; + snapNormals[i] = Vector3.forward; + snapSelectData[i] = Vector2.one; + } + } + // Snap Line + snapLines[0] = point; + + previewMesh.vertices = snapLines; + previewMesh.uv = snapSelectData; + // Since we're using a point geometry shader we need to set the mesh up to supply data as points. + previewMesh.SetIndices(snapLineIndices, MeshTopology.Points, 0 /* submesh id */, false /* recalculate bounds */); + } } - } } diff --git a/Assets/Scripts/model/render/UXEffects/FaceSnapEffect.cs b/Assets/Scripts/model/render/UXEffects/FaceSnapEffect.cs index a16ddc95..11c2b1b3 100644 --- a/Assets/Scripts/model/render/UXEffects/FaceSnapEffect.cs +++ b/Assets/Scripts/model/render/UXEffects/FaceSnapEffect.cs @@ -17,65 +17,77 @@ using com.google.apps.peltzer.client.model.main; using UnityEngine; -namespace com.google.apps.peltzer.client.model.render { - class FaceSnapEffect : UXEffectManager.UXEffect { - private const float DEFAULT_DURATION = 1.0f; +namespace com.google.apps.peltzer.client.model.render +{ + class FaceSnapEffect : UXEffectManager.UXEffect + { + private const float DEFAULT_DURATION = 1.0f; - private int snapTarget = -1; - Vector3 basePreviewPosition; - private List previewMeshes; + private int snapTarget = -1; + Vector3 basePreviewPosition; + private List previewMeshes; - private bool inSnapThreshhold = false; + private bool inSnapThreshhold = false; - /// - /// Constructs the effect, Initialize must still be called before the effect starts to take place. - /// - /// The MMesh id of the target mesh to play the shader on. - public FaceSnapEffect(int snapTarget) { - this.snapTarget = snapTarget; - } + /// + /// Constructs the effect, Initialize must still be called before the effect starts to take place. + /// + /// The MMesh id of the target mesh to play the shader on. + public FaceSnapEffect(int snapTarget) + { + this.snapTarget = snapTarget; + } - public override void Initialize(MeshRepresentationCache cache, MaterialLibrary materialLibrary, - WorldSpace worldSpace) { - base.Initialize(cache, materialLibrary.snapEffectMaterial, worldSpace); - if (snapTarget != -1) { - previewMeshes = - MeshHelper.ToUnityMeshes(cache, PeltzerMain.Instance.model.GetMesh(snapTarget)); - } - } + public override void Initialize(MeshRepresentationCache cache, MaterialLibrary materialLibrary, + WorldSpace worldSpace) + { + base.Initialize(cache, materialLibrary.snapEffectMaterial, worldSpace); + if (snapTarget != -1) + { + previewMeshes = + MeshHelper.ToUnityMeshes(cache, PeltzerMain.Instance.model.GetMesh(snapTarget)); + } + } - public override void Render() { - foreach (Mesh subMesh in previewMeshes) { - Graphics.DrawMesh(subMesh, - worldSpace.modelToWorld, - effectMaterial, - 0); // Layer - } - } + public override void Render() + { + foreach (Mesh subMesh in previewMeshes) + { + Graphics.DrawMesh(subMesh, + worldSpace.modelToWorld, + effectMaterial, + 0); // Layer + } + } - public override void Finish() { - Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0f, 0f, 0f)); - Shader.SetGlobalVector("_FXPointLightPosition", new Vector4(0f, 0f, 0f, 1f)); - UXEffectManager.GetEffectManager().EndEffect(this); - } + public override void Finish() + { + Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0f, 0f, 0f)); + Shader.SetGlobalVector("_FXPointLightPosition", new Vector4(0f, 0f, 0f, 1f)); + UXEffectManager.GetEffectManager().EndEffect(this); + } - /// - /// Updates the face snap shader with info about the current snap. - /// - public void UpdateSnapEffect(SnapInfo snapInfo) { - Vector3 snapFaceWorld = worldSpace.ModelToWorld(snapInfo.snappingFacePosition); - effectMaterial.SetVector("_ImpactPointWorld", worldSpace.ModelToWorld(snapInfo.snapPoint)); - effectMaterial.SetVector("_ImpactNormalWorld", worldSpace.ModelVectorToWorld(snapInfo.snapNormal)); - effectMaterial.SetVector("_ImpactObjectPosWorld", snapFaceWorld); - Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0.8f, .4f, 1f)); - Shader.SetGlobalVector("_FXPointLightPosition", - new Vector4(snapFaceWorld.x, snapFaceWorld.y, snapFaceWorld.z, 1f)); - if (!inSnapThreshhold && snapInfo.inSurfaceThreshhold) { - inSnapThreshhold = true; - effectMaterial.SetFloat("_EffectStartTime", Time.time); - } else if (!snapInfo.inSurfaceThreshhold) { - inSnapThreshhold = false; - } + /// + /// Updates the face snap shader with info about the current snap. + /// + public void UpdateSnapEffect(SnapInfo snapInfo) + { + Vector3 snapFaceWorld = worldSpace.ModelToWorld(snapInfo.snappingFacePosition); + effectMaterial.SetVector("_ImpactPointWorld", worldSpace.ModelToWorld(snapInfo.snapPoint)); + effectMaterial.SetVector("_ImpactNormalWorld", worldSpace.ModelVectorToWorld(snapInfo.snapNormal)); + effectMaterial.SetVector("_ImpactObjectPosWorld", snapFaceWorld); + Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0.8f, .4f, 1f)); + Shader.SetGlobalVector("_FXPointLightPosition", + new Vector4(snapFaceWorld.x, snapFaceWorld.y, snapFaceWorld.z, 1f)); + if (!inSnapThreshhold && snapInfo.inSurfaceThreshhold) + { + inSnapThreshhold = true; + effectMaterial.SetFloat("_EffectStartTime", Time.time); + } + else if (!snapInfo.inSurfaceThreshhold) + { + inSnapThreshhold = false; + } + } } - } } diff --git a/Assets/Scripts/model/render/UXEffects/MeshInsertEffect.cs b/Assets/Scripts/model/render/UXEffects/MeshInsertEffect.cs index 1226e226..c4cca525 100644 --- a/Assets/Scripts/model/render/UXEffects/MeshInsertEffect.cs +++ b/Assets/Scripts/model/render/UXEffects/MeshInsertEffect.cs @@ -18,91 +18,103 @@ using com.google.apps.peltzer.client.tools.utils; using UnityEngine; -namespace com.google.apps.peltzer.client.model.render { - class MeshInsertEffect : UXEffectManager.UXEffect { - // The base length of the duration, to be scaled by the size of the mesh being sized. - // Not marked as const, as it is editable from the debug console. - public static float DURATION_BASE = 0.6f; - - // How long the animation will play for. - private float duration = DURATION_BASE; - - private static MaterialCycler insertCycler; - - public static void Setup(MaterialLibrary library) { - insertCycler = new MaterialCycler(library.meshInsertEffectMaterial, 10); - } - - - private int snapTarget = -1; - private MMesh insertionMesh; - Vector3 basePreviewPosition; - private List previewMeshes; - - private bool inSnapThreshhold = false; - private Model model; - private float startTime = 0f; - private float pctDone = 0f; - - /// - /// Constructs the effect, Initialize must still be called before the effect starts to take place. - /// - /// The MMesh id of the target mesh to play the shader on. - public MeshInsertEffect(MMesh insertionMesh, Model model) { - this.insertionMesh = insertionMesh; - this.model = model; - - } - - public override void Initialize(MeshRepresentationCache cache, MaterialLibrary materialLibrary, - WorldSpace worldSpace) { - - base.Initialize(cache, insertCycler.GetInstanceOfMaterial(), worldSpace); - if (insertionMesh != null) { - previewMeshes = - MeshHelper.ToUnityMeshes(cache, PeltzerMain.Instance.model.GetMesh(insertionMesh.id)); - } - else { - UXEffectManager.GetEffectManager().EndEffect(this); - return; - } - startTime = Time.time; - - Vector3 minBoundsWorld = worldSpace.ModelToWorld(insertionMesh.bounds.min); - Vector3 maxBoundsWorld = worldSpace.ModelToWorld(insertionMesh.bounds.max); - effectMaterial.SetVector("_MeshShaderBounds", new Vector4(minBoundsWorld.y, maxBoundsWorld.y, 0f, 0f)); - // Adjust for constant velocity so that effect works for big and small meshes. - duration = DURATION_BASE * Mathf.Sqrt(maxBoundsWorld.y - minBoundsWorld.y); - } - - public override void Render() { - foreach (Mesh subMesh in previewMeshes) { - Graphics.DrawMesh(subMesh, - worldSpace.modelToWorld, - effectMaterial, - 0); // Layer - } - } - - public override void Finish() { - Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0f, 0f, 0f)); - Shader.SetGlobalVector("_FXPointLightPosition", new Vector4(0f, 0f, 0f, 1f)); - UXEffectManager.GetEffectManager().EndEffect(this); - model.AddToRemesher(insertionMesh.id); - } - - /// - /// Updates the face snap shader with info about the current snap. - /// - public override void Update() { - pctDone = Mathf.Min(1f, (Time.time - startTime) / duration); - // Insertion doesn't get an effect light, so turn it off. - Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0f, 0f, 0f)); - Shader.SetGlobalVector("_FXPointLightPosition", new Vector4(0f, 0f, 0f, 1f)); - effectMaterial.SetFloat("_AnimPct", pctDone); - if (pctDone >= 1f) { - Finish(); - } +namespace com.google.apps.peltzer.client.model.render +{ + class MeshInsertEffect : UXEffectManager.UXEffect + { + // The base length of the duration, to be scaled by the size of the mesh being sized. + // Not marked as const, as it is editable from the debug console. + public static float DURATION_BASE = 0.6f; + + // How long the animation will play for. + private float duration = DURATION_BASE; + + private static MaterialCycler insertCycler; + + public static void Setup(MaterialLibrary library) + { + insertCycler = new MaterialCycler(library.meshInsertEffectMaterial, 10); + } + + + private int snapTarget = -1; + private MMesh insertionMesh; + Vector3 basePreviewPosition; + private List previewMeshes; + + private bool inSnapThreshhold = false; + private Model model; + private float startTime = 0f; + private float pctDone = 0f; + + /// + /// Constructs the effect, Initialize must still be called before the effect starts to take place. + /// + /// The MMesh id of the target mesh to play the shader on. + public MeshInsertEffect(MMesh insertionMesh, Model model) + { + this.insertionMesh = insertionMesh; + this.model = model; + + } + + public override void Initialize(MeshRepresentationCache cache, MaterialLibrary materialLibrary, + WorldSpace worldSpace) + { + + base.Initialize(cache, insertCycler.GetInstanceOfMaterial(), worldSpace); + if (insertionMesh != null) + { + previewMeshes = + MeshHelper.ToUnityMeshes(cache, PeltzerMain.Instance.model.GetMesh(insertionMesh.id)); + } + else + { + UXEffectManager.GetEffectManager().EndEffect(this); + return; + } + startTime = Time.time; + + Vector3 minBoundsWorld = worldSpace.ModelToWorld(insertionMesh.bounds.min); + Vector3 maxBoundsWorld = worldSpace.ModelToWorld(insertionMesh.bounds.max); + effectMaterial.SetVector("_MeshShaderBounds", new Vector4(minBoundsWorld.y, maxBoundsWorld.y, 0f, 0f)); + // Adjust for constant velocity so that effect works for big and small meshes. + duration = DURATION_BASE * Mathf.Sqrt(maxBoundsWorld.y - minBoundsWorld.y); + } + + public override void Render() + { + foreach (Mesh subMesh in previewMeshes) + { + Graphics.DrawMesh(subMesh, + worldSpace.modelToWorld, + effectMaterial, + 0); // Layer + } + } + + public override void Finish() + { + Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0f, 0f, 0f)); + Shader.SetGlobalVector("_FXPointLightPosition", new Vector4(0f, 0f, 0f, 1f)); + UXEffectManager.GetEffectManager().EndEffect(this); + model.AddToRemesher(insertionMesh.id); + } + + /// + /// Updates the face snap shader with info about the current snap. + /// + public override void Update() + { + pctDone = Mathf.Min(1f, (Time.time - startTime) / duration); + // Insertion doesn't get an effect light, so turn it off. + Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0f, 0f, 0f)); + Shader.SetGlobalVector("_FXPointLightPosition", new Vector4(0f, 0f, 0f, 1f)); + effectMaterial.SetFloat("_AnimPct", pctDone); + if (pctDone >= 1f) + { + Finish(); + } + } } - } } diff --git a/Assets/Scripts/model/render/UXEffects/UXEffectManager.cs b/Assets/Scripts/model/render/UXEffects/UXEffectManager.cs index 2587a089..bfd45c92 100644 --- a/Assets/Scripts/model/render/UXEffects/UXEffectManager.cs +++ b/Assets/Scripts/model/render/UXEffects/UXEffectManager.cs @@ -21,118 +21,138 @@ using System.Text; using UnityEngine; -namespace com.google.apps.peltzer.client.model.render { - /// - /// Singleton class for managing UX effects which may have lifecycles beyond the tool that generates them. - /// - class UXEffectManager { - private static UXEffectManager instance; - - public enum UXEffectType { FACE_SNAP}; - - public abstract class UXEffect { - internal UXEffectType type; - internal float startTime; - public Material effectMaterial; - protected WorldSpace worldSpace; - - /// - /// Set up the effect - generate needed preview meshes, etc. This is called by UXEffectManager, so all params - /// are general. Params specific to an effect type should be supplied in the effect specific constructor by the - /// tool which is triggering the effect. - /// - public void Initialize(MeshRepresentationCache cache, Material effectMaterial, WorldSpace worldSpace) { - this.effectMaterial = effectMaterial; - this.worldSpace = worldSpace; - } - - public virtual void Initialize(MeshRepresentationCache cache, MaterialLibrary materialLibrary, - WorldSpace worldSpace) { } - - /// - /// Update the effect - /// - public virtual void Update() { - } - - /// - /// Render the effect - /// - public virtual void Render() { - } - - /// - /// Clean up the effect - kill preview meshes, for example. - /// - public virtual void Finish() { - } - } +namespace com.google.apps.peltzer.client.model.render +{ + /// + /// Singleton class for managing UX effects which may have lifecycles beyond the tool that generates them. + /// + class UXEffectManager + { + private static UXEffectManager instance; + + public enum UXEffectType { FACE_SNAP }; + + public abstract class UXEffect + { + internal UXEffectType type; + internal float startTime; + public Material effectMaterial; + protected WorldSpace worldSpace; + + /// + /// Set up the effect - generate needed preview meshes, etc. This is called by UXEffectManager, so all params + /// are general. Params specific to an effect type should be supplied in the effect specific constructor by the + /// tool which is triggering the effect. + /// + public void Initialize(MeshRepresentationCache cache, Material effectMaterial, WorldSpace worldSpace) + { + this.effectMaterial = effectMaterial; + this.worldSpace = worldSpace; + } + + public virtual void Initialize(MeshRepresentationCache cache, MaterialLibrary materialLibrary, + WorldSpace worldSpace) + { } + + /// + /// Update the effect + /// + public virtual void Update() + { + } + + /// + /// Render the effect + /// + public virtual void Render() + { + } + + /// + /// Clean up the effect - kill preview meshes, for example. + /// + public virtual void Finish() + { + } + } - private MeshRepresentationCache componentCache; - //TODO maintain dictionary of material per effect type. Figure out a more organized way of managing these mats. - private MaterialLibrary materialLibrary; + private MeshRepresentationCache componentCache; + //TODO maintain dictionary of material per effect type. Figure out a more organized way of managing these mats. + private MaterialLibrary materialLibrary; - private HashSet effects; - private HashSet effectsToRemove; + private HashSet effects; + private HashSet effectsToRemove; - public WorldSpace worldSpace; + public WorldSpace worldSpace; - private UXEffectManager(MeshRepresentationCache meshRepresentationCache, MaterialLibrary materialLibrary, - WorldSpace worldSpace) { - this.effects = new HashSet(); - this.effectsToRemove = new HashSet(); - this.componentCache = meshRepresentationCache; - this.materialLibrary = materialLibrary; - this.worldSpace = worldSpace; - MeshInsertEffect.Setup(materialLibrary); - } + private UXEffectManager(MeshRepresentationCache meshRepresentationCache, MaterialLibrary materialLibrary, + WorldSpace worldSpace) + { + this.effects = new HashSet(); + this.effectsToRemove = new HashSet(); + this.componentCache = meshRepresentationCache; + this.materialLibrary = materialLibrary; + this.worldSpace = worldSpace; + MeshInsertEffect.Setup(materialLibrary); + } - // Begin the given UXEffect. Caller is still responsible for calling the effect specific update method - // if it requires updated information. - public void StartEffect(UXEffect effect) { - effect.Initialize(componentCache, materialLibrary, worldSpace); - effects.Add(effect); - if (effectsToRemove.Contains(effect)) { - effectsToRemove.Remove(effect); - } - } + // Begin the given UXEffect. Caller is still responsible for calling the effect specific update method + // if it requires updated information. + public void StartEffect(UXEffect effect) + { + effect.Initialize(componentCache, materialLibrary, worldSpace); + effects.Add(effect); + if (effectsToRemove.Contains(effect)) + { + effectsToRemove.Remove(effect); + } + } - public void EndEffect(UXEffect effect) { - effectsToRemove.Add(effect); - } + public void EndEffect(UXEffect effect) + { + effectsToRemove.Add(effect); + } - public void Update() { - foreach (UXEffect effect in effects) { - effect.Update(); - } - foreach (UXEffect effect in effectsToRemove) { - if (effects.Contains(effect)) { - effects.Remove(effect); + public void Update() + { + foreach (UXEffect effect in effects) + { + effect.Update(); + } + foreach (UXEffect effect in effectsToRemove) + { + if (effects.Contains(effect)) + { + effects.Remove(effect); + } + } + effectsToRemove.Clear(); } - } - effectsToRemove.Clear(); - } - public void Render() { - foreach (UXEffect effect in effects) { - effect.Render(); - } - } + public void Render() + { + foreach (UXEffect effect in effects) + { + effect.Render(); + } + } - // Set up the singleton. This is called from PeltzerMain, so should always be set up. - public static void Setup(MeshRepresentationCache meshRepresentationCache, - MaterialLibrary materialLibrary, - WorldSpace worldSpace) { - instance = new UXEffectManager(meshRepresentationCache, materialLibrary, worldSpace); - Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0f, 0f, 0f)); - Shader.SetGlobalVector("_FXPointLightPosition", new Vector4(0f, 0f, 0f, 1f)); - } + // Set up the singleton. This is called from PeltzerMain, so should always be set up. + public static void Setup(MeshRepresentationCache meshRepresentationCache, + MaterialLibrary materialLibrary, + WorldSpace worldSpace) + { + instance = new UXEffectManager(meshRepresentationCache, materialLibrary, worldSpace); + Shader.SetGlobalVector("_FXPointLightColorStrength", new Vector4(0f, 0f, 0f, 0f)); + Shader.SetGlobalVector("_FXPointLightPosition", new Vector4(0f, 0f, 0f, 1f)); + } - // Gets the singleton UXEffectManager - public static UXEffectManager GetEffectManager() { - return instance; - } + // Gets the singleton UXEffectManager + public static UXEffectManager GetEffectManager() + { + return instance; + } - } + } } diff --git a/Assets/Scripts/model/util/AssertOrThrow.cs b/Assets/Scripts/model/util/AssertOrThrow.cs index 0e2097ef..5652fc76 100644 --- a/Assets/Scripts/model/util/AssertOrThrow.cs +++ b/Assets/Scripts/model/util/AssertOrThrow.cs @@ -14,43 +14,51 @@ using System; -namespace com.google.apps.peltzer.client.model.util { - /// - /// Runtime assertions. - /// - public class AssertOrThrow { - +namespace com.google.apps.peltzer.client.model.util +{ /// - /// Verifies the condition is true. Otherwise throws an Exception. + /// Runtime assertions. /// - /// Condition to assert. - /// Message to include with exception, if thrown. - /// - /// Thrown when condition is false. - public static void True(bool condition, string msg) { - if (!condition) { - throw new Exception(msg); - } - } + public class AssertOrThrow + { - /// - /// Verifies the condition is false. Otherwise throws an Exception. - /// - /// Condition to assert. - /// Message to include with exception, if thrown. - /// - /// Thrown when condition is true. - public static void False(bool condition, string msg) { - if (condition) { - throw new Exception(msg); - } - } + /// + /// Verifies the condition is true. Otherwise throws an Exception. + /// + /// Condition to assert. + /// Message to include with exception, if thrown. + /// + /// Thrown when condition is false. + public static void True(bool condition, string msg) + { + if (!condition) + { + throw new Exception(msg); + } + } + + /// + /// Verifies the condition is false. Otherwise throws an Exception. + /// + /// Condition to assert. + /// Message to include with exception, if thrown. + /// + /// Thrown when condition is true. + public static void False(bool condition, string msg) + { + if (condition) + { + throw new Exception(msg); + } + } - internal static T NotNull(T val, string msg) { - if (val == null) { - throw new Exception(msg); - } - return val; + internal static T NotNull(T val, string msg) + { + if (val == null) + { + throw new Exception(msg); + } + return val; + } } - } } diff --git a/Assets/Scripts/model/util/BackgroundWork.cs b/Assets/Scripts/model/util/BackgroundWork.cs index 2ea91469..65d8f445 100644 --- a/Assets/Scripts/model/util/BackgroundWork.cs +++ b/Assets/Scripts/model/util/BackgroundWork.cs @@ -17,21 +17,23 @@ using System.Linq; using System.Text; -namespace com.google.apps.peltzer.client.model.util { - - /// - /// Work to be done on a background thread. - /// - public interface BackgroundWork { +namespace com.google.apps.peltzer.client.model.util +{ /// - /// The work to be done on the background. + /// Work to be done on a background thread. /// - void BackgroundWork(); + public interface BackgroundWork + { - /// - /// Work to be done on the main thread after the background work is completed. - /// - void PostWork(); - } + /// + /// The work to be done on the background. + /// + void BackgroundWork(); + + /// + /// Work to be done on the main thread after the background work is completed. + /// + void PostWork(); + } } diff --git a/Assets/Scripts/model/util/CollisionSystem.cs b/Assets/Scripts/model/util/CollisionSystem.cs index a40896fa..1766dd0c 100644 --- a/Assets/Scripts/model/util/CollisionSystem.cs +++ b/Assets/Scripts/model/util/CollisionSystem.cs @@ -17,86 +17,88 @@ using System; using com.google.apps.peltzer.client.model.core; -namespace com.google.apps.peltzer.client.model.util { - - /// - /// Spatial index for objects based on their Bounds. - /// - public interface CollisionSystem { - +namespace com.google.apps.peltzer.client.model.util +{ /// - /// Add an item to the CollisionSystem. + /// Spatial index for objects based on their Bounds. /// - /// The item to add - /// The item's initial bounds. - /// - /// Thrown when bounds is not contained by the CollisionSystem's bounds. - void Add(T item, Bounds bounds); + public interface CollisionSystem + { - /// - /// Update the bounds of an item. The item must already exist - /// in the index. - /// - /// The item to update. - /// The item's updated bounds. - /// - /// Thrown when the item isn't in the tree. - void UpdateItemBounds(T item, Bounds bounds); - /// - /// Remove an item from the index. - /// - /// Item to remove. - /// - /// Thrown when the item isn't in the tree. - void Remove(T item); + /// + /// Add an item to the CollisionSystem. + /// + /// The item to add + /// The item's initial bounds. + /// + /// Thrown when bounds is not contained by the CollisionSystem's bounds. + void Add(T item, Bounds bounds); - /// - /// Find items contained entirely within the given bounds. - /// This method will create a set when the number of items - /// is greater than zero. - /// - /// Containing bounds. - /// Set of items found. Null when this - /// method returns false. - /// Maximum number of items to find. - /// true if any items are found. - bool ContainedBy(Bounds bounds, out HashSet items, - int limit = SpatialIndex.MAX_INTERSECT_RESULTS); + /// + /// Update the bounds of an item. The item must already exist + /// in the index. + /// + /// The item to update. + /// The item's updated bounds. + /// + /// Thrown when the item isn't in the tree. + void UpdateItemBounds(T item, Bounds bounds); - /// - /// Find items that intersect the given bounds. - /// This method will create a Set when the number of items - /// is greater than zero. - /// - /// Intersecting bounds. - /// Set of items found. Null when this - /// method returns false. - /// Maximum number of items to find. - /// true if any items are found. - bool IntersectedBy(Bounds bounds, out HashSet items, - int limit = SpatialIndex.MAX_INTERSECT_RESULTS); - - /// - /// Find items that intersect the given bounds. - /// This method will create a Set when the number of items - /// is greater than zero. - /// - /// Intersecting bounds. - /// Set of items found. Null when this - /// method returns false. - /// Maximum number of items to find. - /// true if any items are found. - bool IntersectedByPreallocated(Bounds bounds, ref HashSet items, - int limit = SpatialIndex.MAX_INTERSECT_RESULTS); + /// + /// Remove an item from the index. + /// + /// Item to remove. + /// + /// Thrown when the item isn't in the tree. + void Remove(T item); - /// - /// True if the given item is in the tree. - /// - bool HasItem(T item); + /// + /// Find items contained entirely within the given bounds. + /// This method will create a set when the number of items + /// is greater than zero. + /// + /// Containing bounds. + /// Set of items found. Null when this + /// method returns false. + /// Maximum number of items to find. + /// true if any items are found. + bool ContainedBy(Bounds bounds, out HashSet items, + int limit = SpatialIndex.MAX_INTERSECT_RESULTS); + + /// + /// Find items that intersect the given bounds. + /// This method will create a Set when the number of items + /// is greater than zero. + /// + /// Intersecting bounds. + /// Set of items found. Null when this + /// method returns false. + /// Maximum number of items to find. + /// true if any items are found. + bool IntersectedBy(Bounds bounds, out HashSet items, + int limit = SpatialIndex.MAX_INTERSECT_RESULTS); + + /// + /// Find items that intersect the given bounds. + /// This method will create a Set when the number of items + /// is greater than zero. + /// + /// Intersecting bounds. + /// Set of items found. Null when this + /// method returns false. + /// Maximum number of items to find. + /// true if any items are found. + bool IntersectedByPreallocated(Bounds bounds, ref HashSet items, + int limit = SpatialIndex.MAX_INTERSECT_RESULTS); + + /// + /// True if the given item is in the tree. + /// + bool HasItem(T item); - // Return the bounds specified when the item was inserted or updated. - Bounds BoundsForItem(T item); - } + // Return the bounds specified when the item was inserted or updated. + Bounds BoundsForItem(T item); + } } diff --git a/Assets/Scripts/model/util/ConcurrentQueue.cs b/Assets/Scripts/model/util/ConcurrentQueue.cs index b188a678..4bfd6c2e 100644 --- a/Assets/Scripts/model/util/ConcurrentQueue.cs +++ b/Assets/Scripts/model/util/ConcurrentQueue.cs @@ -15,101 +15,122 @@ using System.Collections.Generic; using System.Threading; -namespace com.google.apps.peltzer.client.model.util { - /// - /// Simple thread-safe queue for sending work across threads. Unfortunately, - /// Unity doesn't have support for System.Collection.Concurrent, so we have - /// to roll our own. - /// - /// - public class ConcurrentQueue { - - // The non-thread-safe queue used as a backing store. - private Queue queue = new Queue(); - - // Count of items in the queue, so that they can be queried without locking the queue. - private volatile int volatileCount; - +namespace com.google.apps.peltzer.client.model.util +{ /// - /// Returns the count of items in the queue without locking the queue. Use this value - /// with caution, since the queue's count may be updated concurrently by threads - /// enqueueing and dequeueing items. - /// - /// This is safe to use in some scenarios: for example, if you have one thread that only - /// enqueues things and another thread that only dequeues things, and they are the only - /// threads that use the queue, then you can safely assume that when you check VolatileCount - /// from the dequeuing thread, it will be less than or equal to the actual number of - /// items on the queue, since it can only have increased since you checked. + /// Simple thread-safe queue for sending work across threads. Unfortunately, + /// Unity doesn't have support for System.Collection.Concurrent, so we have + /// to roll our own. /// - public int VolatileCount { get { return volatileCount; } } + /// + public class ConcurrentQueue + { - /// - /// Add something to the back of the Queue. This can be called from any thread. - /// - /// The object to enqueue. - public void Enqueue(T obj) { - Monitor.Enter(queue); - try { - queue.Enqueue(obj); - volatileCount = queue.Count; - Monitor.Pulse(queue); - } finally { - Monitor.Exit(queue); - } - } + // The non-thread-safe queue used as a backing store. + private Queue queue = new Queue(); - /// - /// Try to remove something from the front of the queue. If the queue is empty, - /// the default value (usually null) is returned. - /// - /// The object taken from the queue. - /// True if we were able to remove an item successfully. - public bool Dequeue(out T obj) { - Monitor.Enter(queue); - try { - if (queue.Count > 0) { - obj = queue.Dequeue(); - volatileCount = queue.Count; - return true; - } else { - obj = default(T); - return false; + // Count of items in the queue, so that they can be queried without locking the queue. + private volatile int volatileCount; + + /// + /// Returns the count of items in the queue without locking the queue. Use this value + /// with caution, since the queue's count may be updated concurrently by threads + /// enqueueing and dequeueing items. + /// + /// This is safe to use in some scenarios: for example, if you have one thread that only + /// enqueues things and another thread that only dequeues things, and they are the only + /// threads that use the queue, then you can safely assume that when you check VolatileCount + /// from the dequeuing thread, it will be less than or equal to the actual number of + /// items on the queue, since it can only have increased since you checked. + /// + public int VolatileCount { get { return volatileCount; } } + + /// + /// Add something to the back of the Queue. This can be called from any thread. + /// + /// The object to enqueue. + public void Enqueue(T obj) + { + Monitor.Enter(queue); + try + { + queue.Enqueue(obj); + volatileCount = queue.Count; + Monitor.Pulse(queue); + } + finally + { + Monitor.Exit(queue); + } } - } finally { - Monitor.Exit(queue); - } - } - /// - /// Try to remove something from the front of the queue. If the queue is empty, wait the - /// given amount of time for something to be put into the queue. - /// If nothing is found, the default value (usually null) is returned. - /// - /// Maximum time to wait for an item. - /// The object from the queue. - /// True if we were able to remove something successfully. - public bool WaitAndDequeue(int waitTime, out T obj) { - Monitor.Enter(queue); - try { - // If something is in the queue, return immediately - if (queue.Count > 0) { - obj = queue.Dequeue(); - volatileCount = queue.Count; - return true; + /// + /// Try to remove something from the front of the queue. If the queue is empty, + /// the default value (usually null) is returned. + /// + /// The object taken from the queue. + /// True if we were able to remove an item successfully. + public bool Dequeue(out T obj) + { + Monitor.Enter(queue); + try + { + if (queue.Count > 0) + { + obj = queue.Dequeue(); + volatileCount = queue.Count; + return true; + } + else + { + obj = default(T); + return false; + } + } + finally + { + Monitor.Exit(queue); + } } - // Otherwise wait for a notification that something was added - Monitor.Wait(queue, waitTime); - if (queue.Count > 0) { - obj = queue.Dequeue(); - volatileCount = queue.Count; - return true; - } else { - obj = default(T); - return false; + + /// + /// Try to remove something from the front of the queue. If the queue is empty, wait the + /// given amount of time for something to be put into the queue. + /// If nothing is found, the default value (usually null) is returned. + /// + /// Maximum time to wait for an item. + /// The object from the queue. + /// True if we were able to remove something successfully. + public bool WaitAndDequeue(int waitTime, out T obj) + { + Monitor.Enter(queue); + try + { + // If something is in the queue, return immediately + if (queue.Count > 0) + { + obj = queue.Dequeue(); + volatileCount = queue.Count; + return true; + } + // Otherwise wait for a notification that something was added + Monitor.Wait(queue, waitTime); + if (queue.Count > 0) + { + obj = queue.Dequeue(); + volatileCount = queue.Count; + return true; + } + else + { + obj = default(T); + return false; + } + } + finally + { + Monitor.Exit(queue); + } } - } finally { - Monitor.Exit(queue); - } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/util/DebugMarker.cs b/Assets/Scripts/model/util/DebugMarker.cs index a22192af..78f68d10 100644 --- a/Assets/Scripts/model/util/DebugMarker.cs +++ b/Assets/Scripts/model/util/DebugMarker.cs @@ -18,110 +18,119 @@ using com.google.apps.peltzer.client.model.render; using com.google.apps.peltzer.client.model.main; -namespace com.google.apps.peltzer.client.model.util { - /// - /// Debug utility to place a visible marker to indicate a position in model or world space. - /// - /// HOW TO USE: - /// - /// DebugMarker marker; - /// private void Update() { - /// // Show a position in model space: - /// Vector3 buggyPosition = ...some buggy code generating it...; - /// - /// if (!marker) marker = DebugMarker.Create(); - /// marker.UpdateInModelSpace(buggyPosition); - /// - /// // RESULT: you will see a little cube marker at the given position in model space. - /// // It will follow model space, so the marker will correctly reposition in world space as you rotate/scale - /// // the world. - /// } - /// - public class DebugMarker : MonoBehaviour { - private const float FORWARD_GIZMO_AXIS_LENGTH = 5.0f; - private const float RIGHT_GIZMO_AXIS_LENGTH = 2.0f; - private const float UP_GIZMO_AXIS_LENGTH = 2.0f; - private const float GIZMO_AXIS_THICKNESS = 0.1f; - private const float DEFAULT_MARKER_SIZE = 0.02f; // 2 cm by default. - - private bool isInModelSpace; - private Vector3 positionModelSpace = Vector3.zero; - private Quaternion rotationModelSpace = Quaternion.identity; - private Vector3 scaleModelSpace = Vector3.one * DEFAULT_MARKER_SIZE; - +namespace com.google.apps.peltzer.client.model.util +{ /// - /// Creates a debug marker of the given color. + /// Debug utility to place a visible marker to indicate a position in model or world space. + /// + /// HOW TO USE: + /// + /// DebugMarker marker; + /// private void Update() { + /// // Show a position in model space: + /// Vector3 buggyPosition = ...some buggy code generating it...; + /// + /// if (!marker) marker = DebugMarker.Create(); + /// marker.UpdateInModelSpace(buggyPosition); + /// + /// // RESULT: you will see a little cube marker at the given position in model space. + /// // It will follow model space, so the marker will correctly reposition in world space as you rotate/scale + /// // the world. + /// } /// - /// Name of the marker to show in the Unity hierarchy. - /// Color of the marker (defaults to green if omitted). - /// The new debug marker. - public static DebugMarker Create(string name = "Untitled", Color? color = null) { - DebugMarker marker = CreatePlainCube().AddComponent(); - marker.gameObject.name = "DEBUG_MARKER: " + name; - Mesh mesh = marker.gameObject.GetComponent().mesh; - Material baseMat = marker.gameObject.GetComponent().material; - Material mat = new Material(baseMat); - mat.color = color != null ? color.Value : Color.green; - marker.gameObject.GetComponent().material = mat; + public class DebugMarker : MonoBehaviour + { + private const float FORWARD_GIZMO_AXIS_LENGTH = 5.0f; + private const float RIGHT_GIZMO_AXIS_LENGTH = 2.0f; + private const float UP_GIZMO_AXIS_LENGTH = 2.0f; + private const float GIZMO_AXIS_THICKNESS = 0.1f; + private const float DEFAULT_MARKER_SIZE = 0.02f; // 2 cm by default. - // Add axis gizmos to indicate what the axes are: - MakeAxisGizmo(Vector3.forward, FORWARD_GIZMO_AXIS_LENGTH, Color.blue, baseMat, marker.gameObject); - MakeAxisGizmo(Vector3.right, RIGHT_GIZMO_AXIS_LENGTH, Color.red, baseMat, marker.gameObject); - MakeAxisGizmo(Vector3.up, UP_GIZMO_AXIS_LENGTH, Color.green, baseMat, marker.gameObject); + private bool isInModelSpace; + private Vector3 positionModelSpace = Vector3.zero; + private Quaternion rotationModelSpace = Quaternion.identity; + private Vector3 scaleModelSpace = Vector3.one * DEFAULT_MARKER_SIZE; - marker.transform.localScale = Vector3.one * DEFAULT_MARKER_SIZE; - return marker; - } + /// + /// Creates a debug marker of the given color. + /// + /// Name of the marker to show in the Unity hierarchy. + /// Color of the marker (defaults to green if omitted). + /// The new debug marker. + public static DebugMarker Create(string name = "Untitled", Color? color = null) + { + DebugMarker marker = CreatePlainCube().AddComponent(); + marker.gameObject.name = "DEBUG_MARKER: " + name; + Mesh mesh = marker.gameObject.GetComponent().mesh; + Material baseMat = marker.gameObject.GetComponent().material; + Material mat = new Material(baseMat); + mat.color = color != null ? color.Value : Color.green; + marker.gameObject.GetComponent().material = mat; - /// - /// Updates the position of the marker in world space. - /// - public void UpdateInWorldSpace(Vector3? position = null, Quaternion? rotation = null, Vector3? scale = null) { - isInModelSpace = false; - if (position != null) gameObject.transform.position = position.Value; - if (rotation != null) gameObject.transform.rotation = rotation.Value; - if (scale != null) gameObject.transform.localScale = scale.Value; - } + // Add axis gizmos to indicate what the axes are: + MakeAxisGizmo(Vector3.forward, FORWARD_GIZMO_AXIS_LENGTH, Color.blue, baseMat, marker.gameObject); + MakeAxisGizmo(Vector3.right, RIGHT_GIZMO_AXIS_LENGTH, Color.red, baseMat, marker.gameObject); + MakeAxisGizmo(Vector3.up, UP_GIZMO_AXIS_LENGTH, Color.green, baseMat, marker.gameObject); - /// - /// Updates the position of the marker in model space. - /// - public void UpdateInModelSpace(Vector3? position = null, Quaternion? rotation = null, Vector3? scale = null) { - isInModelSpace = true; - if (position != null) positionModelSpace = position.Value; - if (rotation != null) rotationModelSpace = rotation.Value; - if (scale != null) scaleModelSpace = scale.Value; - } + marker.transform.localScale = Vector3.one * DEFAULT_MARKER_SIZE; + return marker; + } - private void Update() { - if (isInModelSpace) { - gameObject.transform.position = PeltzerMain.Instance.worldSpace.ModelToWorld(positionModelSpace); - gameObject.transform.rotation = PeltzerMain.Instance.worldSpace.ModelOrientationToWorld(rotationModelSpace); - gameObject.transform.localScale = PeltzerMain.Instance.worldSpace.scale * scaleModelSpace; - } - } + /// + /// Updates the position of the marker in world space. + /// + public void UpdateInWorldSpace(Vector3? position = null, Quaternion? rotation = null, Vector3? scale = null) + { + isInModelSpace = false; + if (position != null) gameObject.transform.position = position.Value; + if (rotation != null) gameObject.transform.rotation = rotation.Value; + if (scale != null) gameObject.transform.localScale = scale.Value; + } - private static GameObject CreatePlainCube() { - GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube); - AssertOrThrow.NotNull(cube, "Failed to create cube primitive"); - Collider col = cube.GetComponent(); - // We don't want a collider. - if (col != null) Destroy(col); - return cube; - } + /// + /// Updates the position of the marker in model space. + /// + public void UpdateInModelSpace(Vector3? position = null, Quaternion? rotation = null, Vector3? scale = null) + { + isInModelSpace = true; + if (position != null) positionModelSpace = position.Value; + if (rotation != null) rotationModelSpace = rotation.Value; + if (scale != null) scaleModelSpace = scale.Value; + } + + private void Update() + { + if (isInModelSpace) + { + gameObject.transform.position = PeltzerMain.Instance.worldSpace.ModelToWorld(positionModelSpace); + gameObject.transform.rotation = PeltzerMain.Instance.worldSpace.ModelOrientationToWorld(rotationModelSpace); + gameObject.transform.localScale = PeltzerMain.Instance.worldSpace.scale * scaleModelSpace; + } + } + + private static GameObject CreatePlainCube() + { + GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube); + AssertOrThrow.NotNull(cube, "Failed to create cube primitive"); + Collider col = cube.GetComponent(); + // We don't want a collider. + if (col != null) Destroy(col); + return cube; + } - private static GameObject MakeAxisGizmo(Vector3 axis, float length, Color color, Material baseMat, - GameObject parent) { - GameObject gizmo = CreatePlainCube(); - gizmo.transform.SetParent(parent.transform, /* worldPositionStays */ false); - gizmo.transform.localRotation = Quaternion.identity; - // Axes must start at the center and extend in their respective direction; however, since the standard - // Unity cube is centered on the origin, we have to position it along the axis by half of its length: - gizmo.transform.localPosition = axis * length * 0.5f; - gizmo.transform.localScale = Vector3.one * GIZMO_AXIS_THICKNESS + axis * (length - GIZMO_AXIS_THICKNESS); - gizmo.GetComponent().material = new Material(baseMat); - gizmo.GetComponent().material.color = color; - return gizmo; + private static GameObject MakeAxisGizmo(Vector3 axis, float length, Color color, Material baseMat, + GameObject parent) + { + GameObject gizmo = CreatePlainCube(); + gizmo.transform.SetParent(parent.transform, /* worldPositionStays */ false); + gizmo.transform.localRotation = Quaternion.identity; + // Axes must start at the center and extend in their respective direction; however, since the standard + // Unity cube is centered on the origin, we have to position it along the axis by half of its length: + gizmo.transform.localPosition = axis * length * 0.5f; + gizmo.transform.localScale = Vector3.one * GIZMO_AXIS_THICKNESS + axis * (length - GIZMO_AXIS_THICKNESS); + gizmo.GetComponent().material = new Material(baseMat); + gizmo.GetComponent().material.color = color; + return gizmo; + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/util/DebugUtils.cs b/Assets/Scripts/model/util/DebugUtils.cs index d397d469..cfb4c5ed 100644 --- a/Assets/Scripts/model/util/DebugUtils.cs +++ b/Assets/Scripts/model/util/DebugUtils.cs @@ -19,65 +19,77 @@ using com.google.apps.peltzer.client.model.core; using UnityEngine; -namespace com.google.apps.peltzer.client.model.util { - /// - /// Debug utilities. - /// - public class DebugUtils { +namespace com.google.apps.peltzer.client.model.util +{ /// - /// Converts a Vector3 to string. Use this instead of Vector3.ToString() because we need 3 decimal - /// places of precision (Vector3.ToString uses only 1). + /// Debug utilities. /// - public static string Vector3ToString(Vector3 v) { - return string.Format("{0:F3},{1:F3},{2:F3}", v.x, v.y, v.z); - } - /// - /// Converts a Bounds object to string, including center, size, extents, min, max (which are not - /// included in the regular Bounds.ToString() method). - /// - public static string BoundsToString(Bounds b) { - return string.Format("Center={0}, Size={1}, Extents={2}, Min={3}, Max={4}", - Vector3ToString(b.center), Vector3ToString(b.size), Vector3ToString(b.extents), Vector3ToString(b.min), - Vector3ToString(b.max)); - } - /// - /// Converts a Vector3 to string. Use this instead of Vector3.ToString() because we need 3 decimal - /// places of precision (Vector3.ToString uses only 1). - /// - public static string Vector3sToString(IEnumerable vs) { - StringBuilder outString = new StringBuilder(); - int count = 1; - outString.Append("["); - foreach (Vector3 vec in vs) { - if (count < vs.Count()) { - outString.Append(string.Format("<{0:F3},{1:F3},{2:F3}>, ", vec.x, vec.y, vec.z)); + public class DebugUtils + { + /// + /// Converts a Vector3 to string. Use this instead of Vector3.ToString() because we need 3 decimal + /// places of precision (Vector3.ToString uses only 1). + /// + public static string Vector3ToString(Vector3 v) + { + return string.Format("{0:F3},{1:F3},{2:F3}", v.x, v.y, v.z); } - else { - outString.Append(string.Format("<{0:F3},{1:F3},{2:F3}>]", vec.x, vec.y, vec.z)); + /// + /// Converts a Bounds object to string, including center, size, extents, min, max (which are not + /// included in the regular Bounds.ToString() method). + /// + public static string BoundsToString(Bounds b) + { + return string.Format("Center={0}, Size={1}, Extents={2}, Min={3}, Max={4}", + Vector3ToString(b.center), Vector3ToString(b.size), Vector3ToString(b.extents), Vector3ToString(b.min), + Vector3ToString(b.max)); } - count++; - } - return outString.ToString(); - } - - /// - /// Converts a Vector3 to string. Use this instead of Vector3.ToString() because we need 3 decimal - /// places of precision (Vector3.ToString uses only 1). - /// - public static string MMeshVertsToString(MMesh mesh) { - StringBuilder outString = new StringBuilder(); - int count = 1; - outString.Append("["); - foreach (Vertex vec in mesh.GetVertices()) { - if (count < mesh.vertexCount) { - outString.Append(string.Format("<{0:F3},{1:F3},{2:F3}>, ", vec.loc.x, vec.loc.x, vec.loc.x)); + /// + /// Converts a Vector3 to string. Use this instead of Vector3.ToString() because we need 3 decimal + /// places of precision (Vector3.ToString uses only 1). + /// + public static string Vector3sToString(IEnumerable vs) + { + StringBuilder outString = new StringBuilder(); + int count = 1; + outString.Append("["); + foreach (Vector3 vec in vs) + { + if (count < vs.Count()) + { + outString.Append(string.Format("<{0:F3},{1:F3},{2:F3}>, ", vec.x, vec.y, vec.z)); + } + else + { + outString.Append(string.Format("<{0:F3},{1:F3},{2:F3}>]", vec.x, vec.y, vec.z)); + } + count++; + } + return outString.ToString(); } - else { - outString.Append(string.Format("<{0:F3},{1:F3},{2:F3}>]", vec.loc.x, vec.loc.x, vec.loc.x)); + + /// + /// Converts a Vector3 to string. Use this instead of Vector3.ToString() because we need 3 decimal + /// places of precision (Vector3.ToString uses only 1). + /// + public static string MMeshVertsToString(MMesh mesh) + { + StringBuilder outString = new StringBuilder(); + int count = 1; + outString.Append("["); + foreach (Vertex vec in mesh.GetVertices()) + { + if (count < mesh.vertexCount) + { + outString.Append(string.Format("<{0:F3},{1:F3},{2:F3}>, ", vec.loc.x, vec.loc.x, vec.loc.x)); + } + else + { + outString.Append(string.Format("<{0:F3},{1:F3},{2:F3}>]", vec.loc.x, vec.loc.x, vec.loc.x)); + } + count++; + } + return outString.ToString(); } - count++; - } - return outString.ToString(); } - } } diff --git a/Assets/Scripts/model/util/DisjointSet.cs b/Assets/Scripts/model/util/DisjointSet.cs index 163903fa..d4080432 100644 --- a/Assets/Scripts/model/util/DisjointSet.cs +++ b/Assets/Scripts/model/util/DisjointSet.cs @@ -14,143 +14,155 @@ using System.Collections.Generic; -namespace com.google.apps.peltzer.client.model.util { - /// - /// A classic (if naive) implementation of a disjoint set with "Union-Find" algorithm, with path optimization. - /// Implemented based on what I remember from CS classes in college, I can't really pinpoint the exact - /// reference text. - /// - /// A disjoint set is a data structure that contains many sets of elements. The fundamental mutations are - /// adding a new element (which becomes a new set) and joining sets together (union). The caller can also - /// ask if any two given elements are in the same set. - /// - /// But for more info on disjoint sets: - /// https://en.wikipedia.org/wiki/Disjoint-set_data_structure - /// - /// The type of the elements in the disjoint set. - public class DisjointSet { +namespace com.google.apps.peltzer.client.model.util +{ /// - /// This dictionary is our the implementation of the disjoint set data structure. It's the representation - /// of a graph: each element has a PARENT. Many elements can have the same parent. Elements who are parents - /// of themselves are called "roots". If you take any element and follow its ancestry line up from parent to - /// parent until you get to the root, you've found the "root of the element". Hence the fundamental property - /// of the representation: ELEMENTS ARE IN THE SAME SET IF AND ONLY IF THEY HAVE THE SAME ROOT. - /// So, using the notation 'X -> Y' to denote that X's parent is Y (lines go from child to parent), - /// let's say we are in this state: - /// - /// A -> B -> E F -> G -> H I -> I - /// ^ - /// | - /// C -> D + /// A classic (if naive) implementation of a disjoint set with "Union-Find" algorithm, with path optimization. + /// Implemented based on what I remember from CS classes in college, I can't really pinpoint the exact + /// reference text. /// - /// Then our sets are { A, B, C, D, E }, { F, G, H } and { I }. The root of A is E. - /// The root of B is also E. The root of C is also E. The root of G is H. The root of I - /// is itself. + /// A disjoint set is a data structure that contains many sets of elements. The fundamental mutations are + /// adding a new element (which becomes a new set) and joining sets together (union). The caller can also + /// ask if any two given elements are in the same set. /// - /// Note that the property is observed: - /// "An element has the same root as another element if and only if they are IN THE SAME SET." + /// But for more info on disjoint sets: + /// https://en.wikipedia.org/wiki/Disjoint-set_data_structure /// - private Dictionary parentOf = new Dictionary(); + /// The type of the elements in the disjoint set. + public class DisjointSet + { + /// + /// This dictionary is our the implementation of the disjoint set data structure. It's the representation + /// of a graph: each element has a PARENT. Many elements can have the same parent. Elements who are parents + /// of themselves are called "roots". If you take any element and follow its ancestry line up from parent to + /// parent until you get to the root, you've found the "root of the element". Hence the fundamental property + /// of the representation: ELEMENTS ARE IN THE SAME SET IF AND ONLY IF THEY HAVE THE SAME ROOT. + /// So, using the notation 'X -> Y' to denote that X's parent is Y (lines go from child to parent), + /// let's say we are in this state: + /// + /// A -> B -> E F -> G -> H I -> I + /// ^ + /// | + /// C -> D + /// + /// Then our sets are { A, B, C, D, E }, { F, G, H } and { I }. The root of A is E. + /// The root of B is also E. The root of C is also E. The root of G is H. The root of I + /// is itself. + /// + /// Note that the property is observed: + /// "An element has the same root as another element if and only if they are IN THE SAME SET." + /// + private Dictionary parentOf = new Dictionary(); - public DisjointSet() {} + public DisjointSet() { } - /// - /// If the element isn't already in the structure, adds it as a new set that contains only the element. - /// If the element is already in the structure, nothing is changed. - /// - /// - public void Add(T element) { - if (!parentOf.ContainsKey(element)) { - // An element that is its own parent represents a standalone set with only the element. - parentOf[element] = element; - } - } + /// + /// If the element isn't already in the structure, adds it as a new set that contains only the element. + /// If the element is already in the structure, nothing is changed. + /// + /// + public void Add(T element) + { + if (!parentOf.ContainsKey(element)) + { + // An element that is its own parent represents a standalone set with only the element. + parentOf[element] = element; + } + } - /// - /// Joins the sets to which the elements belong. - /// Adds the elements to the disjoint set if they were not there before. - /// - /// The first element. - /// The second element. - public void Join(T element1, T element2) { - Add(element1); - Add(element2); - T root1 = GetRoot(element1); - T root2 = GetRoot(element2); - // To unite the two sets, all we have to do is set the parent of one root to the other root. - // This will guarantee that the root of all the elements in both sets are the same. - if (!root1.Equals(root2)) { - parentOf[root1] = root2; - } - } + /// + /// Joins the sets to which the elements belong. + /// Adds the elements to the disjoint set if they were not there before. + /// + /// The first element. + /// The second element. + public void Join(T element1, T element2) + { + Add(element1); + Add(element2); + T root1 = GetRoot(element1); + T root2 = GetRoot(element2); + // To unite the two sets, all we have to do is set the parent of one root to the other root. + // This will guarantee that the root of all the elements in both sets are the same. + if (!root1.Equals(root2)) + { + parentOf[root1] = root2; + } + } - /// - /// Returns whether or not an element is in the structure. - /// - /// The element to check. - /// True if and only if the element is in the structure. - public bool Contains(T element) { - return parentOf.ContainsKey(element); - } + /// + /// Returns whether or not an element is in the structure. + /// + /// The element to check. + /// True if and only if the element is in the structure. + public bool Contains(T element) + { + return parentOf.ContainsKey(element); + } - /// - /// Returns whether or not the two given elements are in the same set. - /// - /// The first element. - /// The second element. - /// True if and only if the elements are in the data structure and are in the same set. - public bool AreInSameSet(T element1, T element2) { - // If either element is not even in the structure, clearly they can't be in the same set. - if (!Contains(element1) || !Contains(element2)) return false; - // To tell whether two elements are in the same set, all we have to do is compare their roots. - return GetRoot(element1).Equals(GetRoot(element2)); - } + /// + /// Returns whether or not the two given elements are in the same set. + /// + /// The first element. + /// The second element. + /// True if and only if the elements are in the data structure and are in the same set. + public bool AreInSameSet(T element1, T element2) + { + // If either element is not even in the structure, clearly they can't be in the same set. + if (!Contains(element1) || !Contains(element2)) return false; + // To tell whether two elements are in the same set, all we have to do is compare their roots. + return GetRoot(element1).Equals(GetRoot(element2)); + } - /// - /// Looks up the root of the given element. - /// - /// The element whose root should be looked up. - /// The root element of the set to which the element belongs. - private T GetRoot(T element) { - // Move up until we find the root (an element whose parent is itself). - T current = element; - while (!parentOf[current].Equals(current)) { - current = parentOf[current]; - } - // 'current' is now the root. - // This is our opportunity to optimize the paths in the data structure, now that we know the root of - // that 'element' and all its ancestors. - OptimizePath(element, current); - return current; - } + /// + /// Looks up the root of the given element. + /// + /// The element whose root should be looked up. + /// The root element of the set to which the element belongs. + private T GetRoot(T element) + { + // Move up until we find the root (an element whose parent is itself). + T current = element; + while (!parentOf[current].Equals(current)) + { + current = parentOf[current]; + } + // 'current' is now the root. + // This is our opportunity to optimize the paths in the data structure, now that we know the root of + // that 'element' and all its ancestors. + OptimizePath(element, current); + return current; + } - /// - /// Optimizes the data structure by shortening the paths to the root that pass through the - /// given element. All elements that are ancestors of the given element will be made to point directly at the - /// root node. Denoting the element as E and the root as R, here is what we would have before: - /// - /// E -> p1 -> p2 -> p3 -> p4 -> R - /// - /// Note how root lookups for E would take 5 hops, lookups for p1 would take 4 hops, etc. How inefficient! - /// - /// After the operation, all nodes in the path will point directly to the root node, optimizing - /// future lookups so that they can be done directly in 1 hop: - /// - /// E -> R - /// p1 -> R - /// p2 -> R - /// p3 -> R - /// p4 -> R - /// - /// The element. - /// The known root of the element. - private void OptimizePath(T element, T root) { - T current = element; - while (!parentOf[current].Equals(current)) { - T parent = parentOf[current]; - parentOf[current] = root; - current = parent; - } + /// + /// Optimizes the data structure by shortening the paths to the root that pass through the + /// given element. All elements that are ancestors of the given element will be made to point directly at the + /// root node. Denoting the element as E and the root as R, here is what we would have before: + /// + /// E -> p1 -> p2 -> p3 -> p4 -> R + /// + /// Note how root lookups for E would take 5 hops, lookups for p1 would take 4 hops, etc. How inefficient! + /// + /// After the operation, all nodes in the path will point directly to the root node, optimizing + /// future lookups so that they can be done directly in 1 hop: + /// + /// E -> R + /// p1 -> R + /// p2 -> R + /// p3 -> R + /// p4 -> R + /// + /// The element. + /// The known root of the element. + private void OptimizePath(T element, T root) + { + T current = element; + while (!parentOf[current].Equals(current)) + { + T parent = parentOf[current]; + parentOf[current] = root; + current = parent; + } + } } - } } diff --git a/Assets/Scripts/model/util/Edge.cs b/Assets/Scripts/model/util/Edge.cs index eacb089b..327ac851 100644 --- a/Assets/Scripts/model/util/Edge.cs +++ b/Assets/Scripts/model/util/Edge.cs @@ -17,40 +17,48 @@ using System.Linq; using System.Text; -namespace com.google.apps.peltzer.client.model.util { - /// - /// A pair that represents a line segment. Makes it easy to lookup segments in a Hashtable. - /// - internal struct Edge { - private readonly int startId; - private readonly int endId; +namespace com.google.apps.peltzer.client.model.util +{ + /// + /// A pair that represents a line segment. Makes it easy to lookup segments in a Hashtable. + /// + internal struct Edge + { + private readonly int startId; + private readonly int endId; - internal Edge(int startId, int endId) { - this.startId = startId; - this.endId = endId; - } + internal Edge(int startId, int endId) + { + this.startId = startId; + this.endId = endId; + } - public override bool Equals(object obj) { - if (obj is Edge) { - Edge other = (Edge)obj; - return startId == other.startId && endId == other.endId; - } - return false; - } + public override bool Equals(object obj) + { + if (obj is Edge) + { + Edge other = (Edge)obj; + return startId == other.startId && endId == other.endId; + } + return false; + } - public Edge Reverse() { - return new Edge(endId, startId); - } + public Edge Reverse() + { + return new Edge(endId, startId); + } - public override int GetHashCode() { - int hc = 271; - hc = (hc * 257) + startId; - hc = (hc * 257) + endId; - return hc; - } + public override int GetHashCode() + { + int hc = 271; + hc = (hc * 257) + startId; + hc = (hc * 257) + endId; + return hc; + } - public override string ToString() { - return startId + " - " + endId; + public override string ToString() + { + return startId + " - " + endId; + } } - } } diff --git a/Assets/Scripts/model/util/Math3d.cs b/Assets/Scripts/model/util/Math3d.cs index 04aa7aca..46b56eaf 100644 --- a/Assets/Scripts/model/util/Math3d.cs +++ b/Assets/Scripts/model/util/Math3d.cs @@ -20,446 +20,492 @@ using com.google.apps.peltzer.client.model.core; -namespace com.google.apps.peltzer.client.model.util { - /// - /// Some 3d math utilities. - /// Adapted from: http://wiki.unity3d.com/index.php/3d_Math_functions . - /// - public class Math3d { - - public const float MERGE_DISTANCE = 0.008f; - public const float EPSILON = 0.0001f; - - /// - /// Resize a collection of vertices by using a scale vector. - /// - /// Original vertices. - /// Scale vector. - /// An IEnumerable to enumerate through the scaled vertex collection. - public static IEnumerable ScaleVertices(IEnumerable vertices, Vector3 scale) { - return vertices.Select(v => new Vertex(v.id, Vector3.Scale(v.loc, scale))); - } - - /// - /// Convert a plane defined by 3 points to a plane defined by a vector and a point. - /// The plane point is the middle of the triangle defined by the 3 points. - /// - /// Normal of the output plane. - /// A point on the output plane. - /// A point on the plane. - /// A point on the plane. - /// A point on the plane. - public static void PlaneFrom3Points(out Vector3 planeNormal, out Vector3 planePoint, - Vector3 pointA, Vector3 pointB, Vector3 pointC) { - - planeNormal = Vector3.zero; - planePoint = Vector3.zero; - - //Make two vectors from the 3 input points, originating from point A - Vector3 AB = pointB - pointA; - Vector3 AC = pointC - pointA; - - //Calculate the normal - planeNormal = Vector3.Normalize(Vector3.Cross(AB, AC)); - - //Get the points in the middle AB and AC - Vector3 middleAB = pointA + (AB / 2.0f); - Vector3 middleAC = pointA + (AC / 2.0f); - - //Get vectors from the middle of AB and AC to the point which is not on that line. - Vector3 middleABtoC = pointC - middleAB; - Vector3 middleACtoB = pointB - middleAC; - - //Calculate the intersection between the two lines. This will be the center - //of the triangle defined by the 3 points. - //We could use LineLineIntersection instead of ClosestPointsOnTwoLines but due to rounding errors - //this sometimes doesn't work. - Vector3 temp; - ClosestPointsOnTwoLines(out planePoint, out temp, middleAB, middleABtoC, middleAC, middleACtoB); - } - - /// - /// Two non-parallel lines which may or may not touch each other have a point on each line which are closest - /// to each other. This function finds those two points. If the lines are not parallel, the function - /// outputs true, otherwise false. - /// - /// Closest point on first line. - /// Closest point on second line. - /// Point on first line. - /// Direction of first line. - /// Point on second line. - /// Direction of second line. - /// True if lines are parallel. - public static bool ClosestPointsOnTwoLines(out Vector3 closestPointLine1, out Vector3 closestPointLine2, - Vector3 linePoint1, Vector3 lineVec1, Vector3 linePoint2, Vector3 lineVec2) { - - closestPointLine1 = Vector3.zero; - closestPointLine2 = Vector3.zero; - - float a = Vector3.Dot(lineVec1, lineVec1); - float b = Vector3.Dot(lineVec1, lineVec2); - float e = Vector3.Dot(lineVec2, lineVec2); - - float d = a * e - b * b; - - //lines are not parallel - if (d != 0.0f) { - - Vector3 r = linePoint1 - linePoint2; - float c = Vector3.Dot(lineVec1, r); - float f = Vector3.Dot(lineVec2, r); - - float s = (b * f - c * e) / d; - float t = (a * f - c * b) / d; - - closestPointLine1 = linePoint1 + lineVec1 * s; - closestPointLine2 = linePoint2 + lineVec2 * t; - - return true; - } else { - return false; - } - } - - /// - /// Returns a point which is a projection from a point to a plane. - /// - /// Plane's normal. - /// Point on plane. - /// The point to project. - /// The projection. - public static Vector3 ProjectPointOnPlane(Vector3 planeNormal, Vector3 planePoint, Vector3 point) { - //First calculate the distance from the point to the plane: - float distance = SignedDistancePlanePoint(planeNormal, planePoint, point); - - //Reverse the sign of the distance - distance *= -1; - - //Get a translation vector - Vector3 translationVector = planeNormal.normalized * distance; - - //Translate the point to form a projection - return point + translationVector; - } - - /// - /// Get the shortest distance between a point and a plane. The output is signed so it holds information - /// as to which side of the plane normal the point is. - /// - /// Plane's normal. - /// Point on plane. - /// The point. - /// The (signed) distance. - public static float SignedDistancePlanePoint(Vector3 planeNormal, Vector3 planePoint, Vector3 point) { - return Vector3.Dot(point - planePoint, planeNormal.normalized); - } - - /// - /// Check if a given polygon's vertex is a convex vertex, i.e. forms an acute - /// angle on the side bounding the area. - /// - /// The vertex to check. - /// The previous vertex in a clockwise wind. - /// The next vertex in a clockwise wind. - /// The normal vector of the polygon. - /// True if vertex is convex, false if it's reflex. - public static bool IsConvex(Vector3 check, Vector3 prev, Vector3 next, Vector3 faceNormal) { - return Vector3.Dot(MeshMath.CalculateNormal(prev, check, next), faceNormal) > 0; - } - +namespace com.google.apps.peltzer.client.model.util +{ /// - /// Simple check for arbitrary triangle in 3D being instersected by Ray. - /// The triangle must be given with the vertices in clockwise order. + /// Some 3d math utilities. + /// Adapted from: http://wiki.unity3d.com/index.php/3d_Math_functions . /// - /// The ray to cast at the triangle. - /// The max distance for the ray. Minimum is assumed 0 - /// A vertex of the triangle. - /// A vertex of the triangle. - /// A vertex of the triangle. - /// The triangle's normal. - /// True if point is contained (and coplanar), false otherwise. - public static bool RayIntersectsTriangle(Ray ray, Vector3 a, Vector3 b, Vector3 c, Vector3 normal) { - float distance; - if (!new Plane(a, b, c).Raycast(ray, out distance)) { - return false; - } - // This is the intersection point between the ray and the plane that contains the triangle. - Vector3 point = ray.origin + ray.direction * distance; - // Now we have to check if the point is in the triangle. - // To do that, we check each segment (AB, BC, CA) and verify that the test point is on the same side of - // the line as the remaining vertex. Since we know the triangle's normal, we can speed up this calculation - // by doing the math directly inline instead of relying on SameSide(), etc: - return Vector3.Dot(MeshMath.CalculateNormal(a, b, point), normal) >= 0 && - Vector3.Dot(MeshMath.CalculateNormal(b, c, point), normal) >= 0 && - Vector3.Dot(MeshMath.CalculateNormal(c, a, point), normal) >= 0; - } + public class Math3d + { + + public const float MERGE_DISTANCE = 0.008f; + public const float EPSILON = 0.0001f; + + /// + /// Resize a collection of vertices by using a scale vector. + /// + /// Original vertices. + /// Scale vector. + /// An IEnumerable to enumerate through the scaled vertex collection. + public static IEnumerable ScaleVertices(IEnumerable vertices, Vector3 scale) + { + return vertices.Select(v => new Vertex(v.id, Vector3.Scale(v.loc, scale))); + } - /// - /// Simple check for arbitrary triangle in 3D containing an arbitrary point in 3D. - /// Uses barycentric coordinated to check that v >= 0, w >=0, and v + w <= 1 - /// - /// A vertex of the triangle. - /// A vertex of the triangle. - /// A vertex of the triangle. - /// A vertex to check. - /// True if point is contained (and coplanar), false otherwise. - public static bool TriangleContainsPoint(Vector3 a, Vector3 b, Vector3 c, Vector3 check) { - Vector3 barycentricCoords = Bary(check, a, b, c); - return (barycentricCoords.x >= 0.0f - && barycentricCoords.y >= 0.0f - && (barycentricCoords.x + barycentricCoords.y) <= 1.0f); - } + /// + /// Convert a plane defined by 3 points to a plane defined by a vector and a point. + /// The plane point is the middle of the triangle defined by the 3 points. + /// + /// Normal of the output plane. + /// A point on the output plane. + /// A point on the plane. + /// A point on the plane. + /// A point on the plane. + public static void PlaneFrom3Points(out Vector3 planeNormal, out Vector3 planePoint, + Vector3 pointA, Vector3 pointB, Vector3 pointC) + { + + planeNormal = Vector3.zero; + planePoint = Vector3.zero; + + //Make two vectors from the 3 input points, originating from point A + Vector3 AB = pointB - pointA; + Vector3 AC = pointC - pointA; + + //Calculate the normal + planeNormal = Vector3.Normalize(Vector3.Cross(AB, AC)); + + //Get the points in the middle AB and AC + Vector3 middleAB = pointA + (AB / 2.0f); + Vector3 middleAC = pointA + (AC / 2.0f); + + //Get vectors from the middle of AB and AC to the point which is not on that line. + Vector3 middleABtoC = pointC - middleAB; + Vector3 middleACtoB = pointB - middleAC; + + //Calculate the intersection between the two lines. This will be the center + //of the triangle defined by the 3 points. + //We could use LineLineIntersection instead of ClosestPointsOnTwoLines but due to rounding errors + //this sometimes doesn't work. + Vector3 temp; + ClosestPointsOnTwoLines(out planePoint, out temp, middleAB, middleABtoC, middleAC, middleACtoB); + } - //Barycentric coordinate algorithm from Real Time Collision Detection - //Barycentric coordinates paramaterize space - in this space with respect to three points of a triangle. - //The u, v, and w coordinates allow you to calculate a point's position on a plane using the three points of - //a triangle ABC on that plane P = uA + vB + wC, and have the property that u + v + w = 1. Additionally, if the - //barycentric coordinates are such that 0 <= u, v, w <= 1 it implies that the point lies within the triangle. - //This can also be presented as v >= 0, w >= 0. and v + w <= 1. - public static Vector3 Bary(Vector3 point, Vector3 a, Vector3 b, Vector3 c) { - Vector3 v0 = b - a; - Vector3 v1 = c - a; - Vector3 v2 = point - a; - float d00 = Vector3.Dot(v0, v0); - float d01 = Vector3.Dot(v0, v1); - float d11 = Vector3.Dot(v1, v1); - float d20 = Vector3.Dot(v2, v0); - float d21 = Vector3.Dot(v2, v1); - float denom = d00 * d11 - d01 * d01; - float v = (d11 * d20 - d01 * d21) / denom; - float w = (d00 * d21 - d01 * d20) / denom; - return new Vector3(v, w, 1.0f - v - w); - } + /// + /// Two non-parallel lines which may or may not touch each other have a point on each line which are closest + /// to each other. This function finds those two points. If the lines are not parallel, the function + /// outputs true, otherwise false. + /// + /// Closest point on first line. + /// Closest point on second line. + /// Point on first line. + /// Direction of first line. + /// Point on second line. + /// Direction of second line. + /// True if lines are parallel. + public static bool ClosestPointsOnTwoLines(out Vector3 closestPointLine1, out Vector3 closestPointLine2, + Vector3 linePoint1, Vector3 lineVec1, Vector3 linePoint2, Vector3 lineVec2) + { + + closestPointLine1 = Vector3.zero; + closestPointLine2 = Vector3.zero; + + float a = Vector3.Dot(lineVec1, lineVec1); + float b = Vector3.Dot(lineVec1, lineVec2); + float e = Vector3.Dot(lineVec2, lineVec2); + + float d = a * e - b * b; + + //lines are not parallel + if (d != 0.0f) + { + + Vector3 r = linePoint1 - linePoint2; + float c = Vector3.Dot(lineVec1, r); + float f = Vector3.Dot(lineVec2, r); + + float s = (b * f - c * e) / d; + float t = (a * f - c * b) / d; + + closestPointLine1 = linePoint1 + lineVec1 * s; + closestPointLine2 = linePoint2 + lineVec2 * t; + + return true; + } + else + { + return false; + } + } - /// - /// Check if a point is inside the border of a convex polygon (point must be coplanar with the - /// polygon already). - /// - public static bool IsInside(List poly, Vector3 point) { - // This returns incorrect results for very small edges due to floating point precision. Multiplying by a big - // number fixes this (or at least makes the degerate case much harder to hit) - Vector3 p2 = point * 10000; - Vector3 baseVertex = poly[0] * 10000; - for (int i = 1; i < poly.Count - 1; i++) { - Vector3 a = poly[i] * 10000; - Vector3 b = poly[(i + 1)] * 10000; - - // If point is in the triangle, return early bc it is therefore also in the polygon. - if (TriangleContainsPoint(baseVertex, a, b, p2)) { - return true; + /// + /// Returns a point which is a projection from a point to a plane. + /// + /// Plane's normal. + /// Point on plane. + /// The point to project. + /// The projection. + public static Vector3 ProjectPointOnPlane(Vector3 planeNormal, Vector3 planePoint, Vector3 point) + { + //First calculate the distance from the point to the plane: + float distance = SignedDistancePlanePoint(planeNormal, planePoint, point); + + //Reverse the sign of the distance + distance *= -1; + + //Get a translation vector + Vector3 translationVector = planeNormal.normalized * distance; + + //Translate the point to form a projection + return point + translationVector; } - } - return false; - } - /// - /// Check if three ordered points are colinear. - /// - public static bool AreColinear(Vector3 a, Vector3 b, Vector3 c) { - return (b - a).normalized == (c - a).normalized; - } + /// + /// Get the shortest distance between a point and a plane. The output is signed so it holds information + /// as to which side of the plane normal the point is. + /// + /// Plane's normal. + /// Point on plane. + /// The point. + /// The (signed) distance. + public static float SignedDistancePlanePoint(Vector3 planeNormal, Vector3 planePoint, Vector3 point) + { + return Vector3.Dot(point - planePoint, planeNormal.normalized); + } - private static bool SameSide(Vector3 a, Vector3 b, Vector3 check, Vector3 reference) { - // The direction of the cross product is opposite for either side of the a-b line. - Vector3 checkSide = MeshMath.CalculateNormal(a, b, check); - Vector3 referenceSide = MeshMath.CalculateNormal(a, b, reference); - // A point on the line is considered to be in both halves for the purposes of triangle - // containing a point. However, the refence point being on the line is probably a - // semantic error. - return checkSide == Vector3.zero || Mathf.Abs(Vector3.Dot(checkSide, referenceSide) - 1.0f) < EPSILON; - } + /// + /// Check if a given polygon's vertex is a convex vertex, i.e. forms an acute + /// angle on the side bounding the area. + /// + /// The vertex to check. + /// The previous vertex in a clockwise wind. + /// The next vertex in a clockwise wind. + /// The normal vector of the polygon. + /// True if vertex is convex, false if it's reflex. + public static bool IsConvex(Vector3 check, Vector3 prev, Vector3 next, Vector3 faceNormal) + { + return Vector3.Dot(MeshMath.CalculateNormal(prev, check, next), faceNormal) > 0; + } - /// - /// Whether one set of bounds is contained by another. - /// - /// The presumed outer bounds. - /// The presumed inner bounds. - /// Whether the inner bounds are entirely contained by the outer bounds. - public static bool ContainsBounds(Bounds outer, Bounds inner) { - return outer.Contains(inner.min) && outer.Contains(inner.max); - } + /// + /// Simple check for arbitrary triangle in 3D being instersected by Ray. + /// The triangle must be given with the vertices in clockwise order. + /// + /// The ray to cast at the triangle. + /// The max distance for the ray. Minimum is assumed 0 + /// A vertex of the triangle. + /// A vertex of the triangle. + /// A vertex of the triangle. + /// The triangle's normal. + /// True if point is contained (and coplanar), false otherwise. + public static bool RayIntersectsTriangle(Ray ray, Vector3 a, Vector3 b, Vector3 c, Vector3 normal) + { + float distance; + if (!new Plane(a, b, c).Raycast(ray, out distance)) + { + return false; + } + // This is the intersection point between the ray and the plane that contains the triangle. + Vector3 point = ray.origin + ray.direction * distance; + // Now we have to check if the point is in the triangle. + // To do that, we check each segment (AB, BC, CA) and verify that the test point is on the same side of + // the line as the remaining vertex. Since we know the triangle's normal, we can speed up this calculation + // by doing the math directly inline instead of relying on SameSide(), etc: + return Vector3.Dot(MeshMath.CalculateNormal(a, b, point), normal) >= 0 && + Vector3.Dot(MeshMath.CalculateNormal(b, c, point), normal) >= 0 && + Vector3.Dot(MeshMath.CalculateNormal(c, a, point), normal) >= 0; + } - // Returns the centroid of a group of vectors. - public static Vector3 FindCentroid(IEnumerable vectors) { - Vector3 tally = Vector3.zero; - foreach (Vector3 vec in vectors) { - tally += vec; - } - return tally / vectors.Count(); - } + /// + /// Simple check for arbitrary triangle in 3D containing an arbitrary point in 3D. + /// Uses barycentric coordinated to check that v >= 0, w >=0, and v + w <= 1 + /// + /// A vertex of the triangle. + /// A vertex of the triangle. + /// A vertex of the triangle. + /// A vertex to check. + /// True if point is contained (and coplanar), false otherwise. + public static bool TriangleContainsPoint(Vector3 a, Vector3 b, Vector3 c, Vector3 check) + { + Vector3 barycentricCoords = Bary(check, a, b, c); + return (barycentricCoords.x >= 0.0f + && barycentricCoords.y >= 0.0f + && (barycentricCoords.x + barycentricCoords.y) <= 1.0f); + } - // Returns the centroid of a list of vectors. - public static Vector3 FindCentroid(List vectors) { - Vector3 tally = Vector3.zero; - for (int i = 0; i < vectors.Count; i++) { - tally += vectors[i]; - } - return tally / vectors.Count(); - } + //Barycentric coordinate algorithm from Real Time Collision Detection + //Barycentric coordinates paramaterize space - in this space with respect to three points of a triangle. + //The u, v, and w coordinates allow you to calculate a point's position on a plane using the three points of + //a triangle ABC on that plane P = uA + vB + wC, and have the property that u + v + w = 1. Additionally, if the + //barycentric coordinates are such that 0 <= u, v, w <= 1 it implies that the point lies within the triangle. + //This can also be presented as v >= 0, w >= 0. and v + w <= 1. + public static Vector3 Bary(Vector3 point, Vector3 a, Vector3 b, Vector3 c) + { + Vector3 v0 = b - a; + Vector3 v1 = c - a; + Vector3 v2 = point - a; + float d00 = Vector3.Dot(v0, v0); + float d01 = Vector3.Dot(v0, v1); + float d11 = Vector3.Dot(v1, v1); + float d20 = Vector3.Dot(v2, v0); + float d21 = Vector3.Dot(v2, v1); + float denom = d00 * d11 - d01 * d01; + float v = (d11 * d20 - d01 * d21) / denom; + float w = (d00 * d21 - d01 * d20) / denom; + return new Vector3(v, w, 1.0f - v - w); + } - // Returns the centroid of a group of MMesh offsets. - public static Vector3 FindCentroid(IEnumerable meshes) { - Vector3 tally = Vector3.zero; - foreach (MMesh mesh in meshes) { - tally += mesh.offset; - } - return tally / meshes.Count(); - } + /// + /// Check if a point is inside the border of a convex polygon (point must be coplanar with the + /// polygon already). + /// + public static bool IsInside(List poly, Vector3 point) + { + // This returns incorrect results for very small edges due to floating point precision. Multiplying by a big + // number fixes this (or at least makes the degerate case much harder to hit) + Vector3 p2 = point * 10000; + Vector3 baseVertex = poly[0] * 10000; + for (int i = 1; i < poly.Count - 1; i++) + { + Vector3 a = poly[i] * 10000; + Vector3 b = poly[(i + 1)] * 10000; + + // If point is in the triangle, return early bc it is therefore also in the polygon. + if (TriangleContainsPoint(baseVertex, a, b, p2)) + { + return true; + } + } + return false; + } - // Returns the centroid of a list of MMesh offsets. - public static Vector3 FindCentroid(List meshes) { - Vector3 tally = Vector3.zero; - for (int i = 0; i < meshes.Count; i++) { - tally += meshes[i].offset; - } - return tally / meshes.Count(); - } + /// + /// Check if three ordered points are colinear. + /// + public static bool AreColinear(Vector3 a, Vector3 b, Vector3 c) + { + return (b - a).normalized == (c - a).normalized; + } - public static Vector3 RotatePointAroundPivot(Vector3 point, Vector3 pivot, Quaternion rotation) { - Vector3 dir = point - pivot; - return rotation * dir + pivot; - } + private static bool SameSide(Vector3 a, Vector3 b, Vector3 check, Vector3 reference) + { + // The direction of the cross product is opposite for either side of the a-b line. + Vector3 checkSide = MeshMath.CalculateNormal(a, b, check); + Vector3 referenceSide = MeshMath.CalculateNormal(a, b, reference); + // A point on the line is considered to be in both halves for the purposes of triangle + // containing a point. However, the refence point being on the line is probably a + // semantic error. + return checkSide == Vector3.zero || Mathf.Abs(Vector3.Dot(checkSide, referenceSide) - 1.0f) < EPSILON; + } - // Find the most common rotation of a group of rotations. - // Ties are broken deterministically by precedence in the passed collection. - // This method could probably be smarter. - public static Quaternion MostCommonRotation(IEnumerable rotations) { - Dictionary rotationCounts = new Dictionary(); - int highestCount = 0; - Quaternion mostCommonRotation = Quaternion.identity; - - foreach (Quaternion rotation in rotations) { - if (rotationCounts.ContainsKey(rotation)) { - rotationCounts[rotation] = rotationCounts[rotation] + 1; - } else { - rotationCounts.Add(rotation, 1); + /// + /// Whether one set of bounds is contained by another. + /// + /// The presumed outer bounds. + /// The presumed inner bounds. + /// Whether the inner bounds are entirely contained by the outer bounds. + public static bool ContainsBounds(Bounds outer, Bounds inner) + { + return outer.Contains(inner.min) && outer.Contains(inner.max); } - if (rotationCounts[rotation] > highestCount) { - highestCount = rotationCounts[rotation]; - mostCommonRotation = rotation; + // Returns the centroid of a group of vectors. + public static Vector3 FindCentroid(IEnumerable vectors) + { + Vector3 tally = Vector3.zero; + foreach (Vector3 vec in vectors) + { + tally += vec; + } + return tally / vectors.Count(); } - } - return mostCommonRotation; - } + // Returns the centroid of a list of vectors. + public static Vector3 FindCentroid(List vectors) + { + Vector3 tally = Vector3.zero; + for (int i = 0; i < vectors.Count; i++) + { + tally += vectors[i]; + } + return tally / vectors.Count(); + } - /// - /// Given a position and a list of points finds which point is nearest to position. - /// - /// The position being compared to the points. - /// The list of possible nearest points. - /// The nearest point. - public static Vector3 NearestPoint(Vector3 position, List points) { - float nearestDistance = Mathf.Infinity; - Vector3 nearestPoint = new Vector3(); - - foreach (Vector3 point in points) { - float distance = Vector3.SqrMagnitude(point - position); - - if (distance < nearestDistance) { - nearestDistance = distance; - nearestPoint = point; + // Returns the centroid of a group of MMesh offsets. + public static Vector3 FindCentroid(IEnumerable meshes) + { + Vector3 tally = Vector3.zero; + foreach (MMesh mesh in meshes) + { + tally += mesh.offset; + } + return tally / meshes.Count(); } - } - return nearestPoint; - } + // Returns the centroid of a list of MMesh offsets. + public static Vector3 FindCentroid(List meshes) + { + Vector3 tally = Vector3.zero; + for (int i = 0; i < meshes.Count; i++) + { + tally += meshes[i].offset; + } + return tally / meshes.Count(); + } - /// - /// Takes a position and projects it onto a line. - /// - /// The point to project onto the line. - /// The line represented as a vector being projected onto. - /// A reference point on the line. - /// The toSnap position projected onto the line. - public static Vector3 ProjectPointOntoLine(Vector3 toSnap, Vector3 line, Vector3 origin) { - // Find the distance from the origin to the toSnap position. - float projectedDistance = - Mathf.Cos(Vector3.Angle(toSnap - origin, line) * Mathf.Deg2Rad) * Vector3.Distance(origin, toSnap); - - return origin + (line.normalized * projectedDistance); - } + public static Vector3 RotatePointAroundPivot(Vector3 point, Vector3 pivot, Quaternion rotation) + { + Vector3 dir = point - pivot; + return rotation * dir + pivot; + } - /// - /// Compares two vectors for equality. - /// - /// The first vector. - /// The second vector. - /// The floating point error. - /// True if the vectors are equal. - public static bool CompareVectors(Vector3 v1, Vector3 v2, float epsilon) { - if (!(Mathf.Abs(v1.x - v2.x) < epsilon)) - return false; + // Find the most common rotation of a group of rotations. + // Ties are broken deterministically by precedence in the passed collection. + // This method could probably be smarter. + public static Quaternion MostCommonRotation(IEnumerable rotations) + { + Dictionary rotationCounts = new Dictionary(); + int highestCount = 0; + Quaternion mostCommonRotation = Quaternion.identity; + + foreach (Quaternion rotation in rotations) + { + if (rotationCounts.ContainsKey(rotation)) + { + rotationCounts[rotation] = rotationCounts[rotation] + 1; + } + else + { + rotationCounts.Add(rotation, 1); + } + + if (rotationCounts[rotation] > highestCount) + { + highestCount = rotationCounts[rotation]; + mostCommonRotation = rotation; + } + } + + return mostCommonRotation; + } - if (!(Mathf.Abs(v1.y - v2.y) < epsilon)) - return false; + /// + /// Given a position and a list of points finds which point is nearest to position. + /// + /// The position being compared to the points. + /// The list of possible nearest points. + /// The nearest point. + public static Vector3 NearestPoint(Vector3 position, List points) + { + float nearestDistance = Mathf.Infinity; + Vector3 nearestPoint = new Vector3(); + + foreach (Vector3 point in points) + { + float distance = Vector3.SqrMagnitude(point - position); + + if (distance < nearestDistance) + { + nearestDistance = distance; + nearestPoint = point; + } + } + + return nearestPoint; + } - if (!(Mathf.Abs(v1.z - v2.z) < epsilon)) - return false; + /// + /// Takes a position and projects it onto a line. + /// + /// The point to project onto the line. + /// The line represented as a vector being projected onto. + /// A reference point on the line. + /// The toSnap position projected onto the line. + public static Vector3 ProjectPointOntoLine(Vector3 toSnap, Vector3 line, Vector3 origin) + { + // Find the distance from the origin to the toSnap position. + float projectedDistance = + Mathf.Cos(Vector3.Angle(toSnap - origin, line) * Mathf.Deg2Rad) * Vector3.Distance(origin, toSnap); + + return origin + (line.normalized * projectedDistance); + } - return true; - } + /// + /// Compares two vectors for equality. + /// + /// The first vector. + /// The second vector. + /// The floating point error. + /// True if the vectors are equal. + public static bool CompareVectors(Vector3 v1, Vector3 v2, float epsilon) + { + if (!(Mathf.Abs(v1.x - v2.x) < epsilon)) + return false; + + if (!(Mathf.Abs(v1.y - v2.y) < epsilon)) + return false; + + if (!(Mathf.Abs(v1.z - v2.z) < epsilon)) + return false; + + return true; + } - /// - /// Test if a quaternion is valid for rotation (ie, has a magnitude of 1). - /// - /// The quaternion to test - /// The acceptable amount or error. - /// True if the Quaternion is a valid rotation quaternion - public static bool QuaternionIsValidRotation(Quaternion testQuaternion, float epsilon = EPSILON) { - return Mathf.Abs(testQuaternion.x * testQuaternion.x - + testQuaternion.y * testQuaternion.y - + testQuaternion.z * testQuaternion.z - + testQuaternion.w * testQuaternion.w - 1.0f) < epsilon; - } + /// + /// Test if a quaternion is valid for rotation (ie, has a magnitude of 1). + /// + /// The quaternion to test + /// The acceptable amount or error. + /// True if the Quaternion is a valid rotation quaternion + public static bool QuaternionIsValidRotation(Quaternion testQuaternion, float epsilon = EPSILON) + { + return Mathf.Abs(testQuaternion.x * testQuaternion.x + + testQuaternion.y * testQuaternion.y + + testQuaternion.z * testQuaternion.z + + testQuaternion.w * testQuaternion.w - 1.0f) < epsilon; + } - /// - /// Normalizes a quaternion. - /// - /// The Quaternion to normalize/ - /// The normalized Quaternion. - public static Quaternion Normalize(Quaternion q) { - float mag = Mathf.Sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w); - return new Quaternion(q.x / mag, q.y / mag, q.z / mag, q.w / mag); - } + /// + /// Normalizes a quaternion. + /// + /// The Quaternion to normalize/ + /// The normalized Quaternion. + public static Quaternion Normalize(Quaternion q) + { + float mag = Mathf.Sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w); + return new Quaternion(q.x / mag, q.y / mag, q.z / mag, q.w / mag); + } - public static Vector3 Normalize(Vector3 vec) { - Vector3 scaledVec = 1000000f * vec; - return scaledVec / scaledVec.magnitude; - } + public static Vector3 Normalize(Vector3 vec) + { + Vector3 scaledVec = 1000000f * vec; + return scaledVec / scaledVec.magnitude; + } - /// - /// Compares two quaternions for equality. - /// - /// The first quaternion. - /// The second quaternion. - /// The floating point error. - /// True if the quaternions are equal. - public static bool CompareQuaternions(Quaternion q1, Quaternion q2, float epsilon) { - Vector3 q1Euler = q1.eulerAngles; - Vector3 q2Euler = q2.eulerAngles; + /// + /// Compares two quaternions for equality. + /// + /// The first quaternion. + /// The second quaternion. + /// The floating point error. + /// True if the quaternions are equal. + public static bool CompareQuaternions(Quaternion q1, Quaternion q2, float epsilon) + { + Vector3 q1Euler = q1.eulerAngles; + Vector3 q2Euler = q2.eulerAngles; - if (!(Mathf.Abs(q1Euler.x - q2Euler.x) < epsilon)) - return false; + if (!(Mathf.Abs(q1Euler.x - q2Euler.x) < epsilon)) + return false; - if (!(Mathf.Abs(q1Euler.y - q2Euler.y) < epsilon)) - return false; + if (!(Mathf.Abs(q1Euler.y - q2Euler.y) < epsilon)) + return false; - if (!(Mathf.Abs(q1Euler.z - q2Euler.z) < epsilon)) - return false; + if (!(Mathf.Abs(q1Euler.z - q2Euler.z) < epsilon)) + return false; - return true; - } + return true; + } - /// - /// Returns a given value on a cubic bezier curve defined by A, B, C and D. - /// - /// WARNING: There seems to be unknown constraints for which this function doesn't work. It does work for the - /// current use cases. - /// - public static float CubicBezierEasing(float A, float B, float C, float D, float t) { - return A + 3.0f * t * (B - A) + 3.0f * t * t * (C - 2.0f * B + A) + t * t * t * (D - 3.0f * C + 3.0f * B - A); + /// + /// Returns a given value on a cubic bezier curve defined by A, B, C and D. + /// + /// WARNING: There seems to be unknown constraints for which this function doesn't work. It does work for the + /// current use cases. + /// + public static float CubicBezierEasing(float A, float B, float C, float D, float t) + { + return A + 3.0f * t * (B - A) + 3.0f * t * t * (C - 2.0f * B + A) + t * t * t * (D - 3.0f * C + 3.0f * B - A); + } } - } } diff --git a/Assets/Scripts/model/util/MultiDict.cs b/Assets/Scripts/model/util/MultiDict.cs index 3619fde6..90078904 100644 --- a/Assets/Scripts/model/util/MultiDict.cs +++ b/Assets/Scripts/model/util/MultiDict.cs @@ -17,36 +17,44 @@ using System.Linq; using System.Text; -namespace com.google.apps.peltzer.client.model.util { - /// - /// Dictionary that stores a list of values for a key. - /// - public class MultiDict { - private readonly Dictionary> mainDict = new Dictionary>(); +namespace com.google.apps.peltzer.client.model.util +{ + /// + /// Dictionary that stores a list of values for a key. + /// + public class MultiDict + { + private readonly Dictionary> mainDict = new Dictionary>(); - public void Add(K key, V value) { - List values; - if (!mainDict.TryGetValue(key, out values)) { - values = new List(); - mainDict[key] = values; - } - values.Add(value); - } + public void Add(K key, V value) + { + List values; + if (!mainDict.TryGetValue(key, out values)) + { + values = new List(); + mainDict[key] = values; + } + values.Add(value); + } - public bool TryGetValues(K key, out List values) { - return mainDict.TryGetValue(key, out values); - } + public bool TryGetValues(K key, out List values) + { + return mainDict.TryGetValue(key, out values); + } - public List GetValues(K key) { - return mainDict[key]; - } + public List GetValues(K key) + { + return mainDict[key]; + } - public Dictionary>.KeyCollection Keys { - get { return mainDict.Keys; } - } + public Dictionary>.KeyCollection Keys + { + get { return mainDict.Keys; } + } - public bool ContainsKey(K id) { - return mainDict.ContainsKey(id); + public bool ContainsKey(K id) + { + return mainDict.ContainsKey(id); + } } - } } diff --git a/Assets/Scripts/model/util/NativeSpatial.cs b/Assets/Scripts/model/util/NativeSpatial.cs index e1bd8875..18e1d980 100644 --- a/Assets/Scripts/model/util/NativeSpatial.cs +++ b/Assets/Scripts/model/util/NativeSpatial.cs @@ -25,31 +25,32 @@ /// /// Class exposing native functions which implement our collision system. /// -public static class NativeSpatialFunction { - [DllImport("BlocksNativeLib", EntryPoint="AllocSpatialPartitioner")] - public static extern int AllocSpatialPartitioner(Vector3 center, Vector3 size); +public static class NativeSpatialFunction +{ + [DllImport("BlocksNativeLib", EntryPoint = "AllocSpatialPartitioner")] + public static extern int AllocSpatialPartitioner(Vector3 center, Vector3 size); - [DllImport("BlocksNativeLib")] - public static extern void SpatialPartitionerAddItem(int SpatialPartitionerHandle, int itemId, - Vector3 center, Vector3 extents); + [DllImport("BlocksNativeLib")] + public static extern void SpatialPartitionerAddItem(int SpatialPartitionerHandle, int itemId, + Vector3 center, Vector3 extents); - [DllImport("BlocksNativeLib")] - public static extern void SpatialPartitionerUpdateItem(int SpatialPartitionerHandle, int itemId, - Vector3 center, Vector3 extents); + [DllImport("BlocksNativeLib")] + public static extern void SpatialPartitionerUpdateItem(int SpatialPartitionerHandle, int itemId, + Vector3 center, Vector3 extents); - [DllImport("BlocksNativeLib")] - public static extern void SpatialPartitionerRemoveItem(int SpatialPartitionerHandle, int itemId); + [DllImport("BlocksNativeLib")] + public static extern void SpatialPartitionerRemoveItem(int SpatialPartitionerHandle, int itemId); - [DllImport("BlocksNativeLib")] - public static extern int SpatialPartitionerContainedBy(int SpatialPartitionerHandle, Vector3 testCenter, - Vector3 testExtents, int[] returnArray, int returnArrayMaxSize); + [DllImport("BlocksNativeLib")] + public static extern int SpatialPartitionerContainedBy(int SpatialPartitionerHandle, Vector3 testCenter, + Vector3 testExtents, int[] returnArray, int returnArrayMaxSize); - [DllImport("BlocksNativeLib")] - public static extern int SpatialPartitionerIntersectedBy(int SpatialPartitionerHandle, Vector3 testCenter, - Vector3 testExtents, int[] returnArray, int returnArrayMaxSize); + [DllImport("BlocksNativeLib")] + public static extern int SpatialPartitionerIntersectedBy(int SpatialPartitionerHandle, Vector3 testCenter, + Vector3 testExtents, int[] returnArray, int returnArrayMaxSize); - [DllImport("BlocksNativeLib")] - public static extern int SpatialPartitionerHasItem(int SpatialPartitionerHandle, int itemHandle); + [DllImport("BlocksNativeLib")] + public static extern int SpatialPartitionerHasItem(int SpatialPartitionerHandle, int itemHandle); } /// @@ -58,136 +59,149 @@ public static extern int SpatialPartitionerIntersectedBy(int SpatialPartitionerH /// so implementing this natively is a big win. /// /// -public class NativeSpatial : CollisionSystem { - - // A unique handle that identifies this collision system to the native code. - private int spatialPartitionId; - // Used for super cheap id allocation - we use this id and increment. - private int nextHandleId = 0; - // A mapping from the items in this system to their numeric handle used to identify them to native. - private Dictionary itemIds = new Dictionary(); - // A mapping of numeric handles to items in the system. - private Dictionary idsToItems = new Dictionary(); - // A mapping of items in the system to their Bounds. - private Dictionary itemBounds = new Dictionary(); - // A preallocated array for retrieving results from native code. - private int[] results = new int[SpatialIndex.MAX_INTERSECT_RESULTS]; - - // Mutex for controlling concurrent access to this system. - private Mutex nativeSpatialMutex = new Mutex(); - - public NativeSpatial() { - // We call this to ensure that the callback that allows debug statements from native is set up. - FbxExporter.Setup(); - spatialPartitionId = NativeSpatialFunction.AllocSpatialPartitioner(Vector3.up, Vector3.back); - } - - /// - /// Adds an item to the CollisionSystem. - /// - public void Add(T item, Bounds bounds) { - nativeSpatialMutex.WaitOne(); - int id = nextHandleId++; - itemIds[item] = id; - idsToItems[id] = item; - itemBounds[item] = bounds; - NativeSpatialFunction.SpatialPartitionerAddItem(spatialPartitionId, id, bounds.center, bounds.extents); - nativeSpatialMutex.ReleaseMutex(); - } - - /// - /// Updates the bounding box of an item already in the collision system. - /// - public void UpdateItemBounds(T item, Bounds bounds) { - nativeSpatialMutex.WaitOne(); - int id = itemIds[item]; - NativeSpatialFunction.SpatialPartitionerUpdateItem(spatialPartitionId, id, bounds.center, bounds.extents); - nativeSpatialMutex.ReleaseMutex(); - } - - /// - /// Remove an item from the system. - /// - /// Item to remove. - /// - /// Thrown when the item isn't in the tree. - public void Remove(T item) { - nativeSpatialMutex.WaitOne(); - int id = itemIds[item]; - itemIds.Remove(item); - idsToItems.Remove(id); - itemBounds.Remove(item); - NativeSpatialFunction.SpatialPartitionerRemoveItem(spatialPartitionId, id); - nativeSpatialMutex.ReleaseMutex(); - } - - /// - /// Find items contained entirely within the given bounds. - /// This method will create a set when the number of items - /// is greater than zero. - /// - /// Containing bounds. - /// Set of items found. Null when this - /// method returns false. - /// Maximum number of items to find. - /// true if any items are found. - public bool ContainedBy(Bounds bounds, out HashSet items, - int limit = SpatialIndex.MAX_INTERSECT_RESULTS) { - nativeSpatialMutex.WaitOne(); - int numResults = NativeSpatialFunction.SpatialPartitionerContainedBy(spatialPartitionId, bounds.center, - bounds.extents, results, limit); - items = new HashSet(); - for (int i = 0; i < numResults; i++) { - items.Add(idsToItems[results[i]]); +public class NativeSpatial : CollisionSystem +{ + + // A unique handle that identifies this collision system to the native code. + private int spatialPartitionId; + // Used for super cheap id allocation - we use this id and increment. + private int nextHandleId = 0; + // A mapping from the items in this system to their numeric handle used to identify them to native. + private Dictionary itemIds = new Dictionary(); + // A mapping of numeric handles to items in the system. + private Dictionary idsToItems = new Dictionary(); + // A mapping of items in the system to their Bounds. + private Dictionary itemBounds = new Dictionary(); + // A preallocated array for retrieving results from native code. + private int[] results = new int[SpatialIndex.MAX_INTERSECT_RESULTS]; + + // Mutex for controlling concurrent access to this system. + private Mutex nativeSpatialMutex = new Mutex(); + + public NativeSpatial() + { + // We call this to ensure that the callback that allows debug statements from native is set up. + FbxExporter.Setup(); + spatialPartitionId = NativeSpatialFunction.AllocSpatialPartitioner(Vector3.up, Vector3.back); } - nativeSpatialMutex.ReleaseMutex(); - return items.Count > 0; - } - - /// - /// Returns whether the item is tracked in this system. - /// - public bool HasItem(T item) { - nativeSpatialMutex.WaitOne(); - bool inItems = itemIds.ContainsKey(item); - nativeSpatialMutex.ReleaseMutex(); - return inItems; - } - - /// - /// Checks whether the supplied Bounds intersects anything in the system, and returns a HashSet - /// of intersection objects. Returns true if there were any intersections. - /// - public bool IntersectedBy(Bounds bounds, out HashSet items, - int limit = SpatialIndex.MAX_INTERSECT_RESULTS) { - nativeSpatialMutex.WaitOne(); - int numResults = NativeSpatialFunction.SpatialPartitionerIntersectedBy(spatialPartitionId, bounds.center, - bounds.extents, results, limit); - items = new HashSet(); - for (int i = 0; i < numResults; i++) { - items.Add(idsToItems[results[i]]); + + /// + /// Adds an item to the CollisionSystem. + /// + public void Add(T item, Bounds bounds) + { + nativeSpatialMutex.WaitOne(); + int id = nextHandleId++; + itemIds[item] = id; + idsToItems[id] = item; + itemBounds[item] = bounds; + NativeSpatialFunction.SpatialPartitionerAddItem(spatialPartitionId, id, bounds.center, bounds.extents); + nativeSpatialMutex.ReleaseMutex(); + } + + /// + /// Updates the bounding box of an item already in the collision system. + /// + public void UpdateItemBounds(T item, Bounds bounds) + { + nativeSpatialMutex.WaitOne(); + int id = itemIds[item]; + NativeSpatialFunction.SpatialPartitionerUpdateItem(spatialPartitionId, id, bounds.center, bounds.extents); + nativeSpatialMutex.ReleaseMutex(); } - nativeSpatialMutex.ReleaseMutex(); - return items.Count > 0; - } - - /// - /// Checks whether the supplied Bounds intersects anything in the system, and fills the supplied preallocated Hashset - /// with intersected items. Returns true if there were any intersections. - /// - public bool IntersectedByPreallocated(Bounds bounds, ref HashSet items, - int limit = SpatialIndex.MAX_INTERSECT_RESULTS) { - nativeSpatialMutex.WaitOne(); - int numResults = NativeSpatialFunction.SpatialPartitionerIntersectedBy(spatialPartitionId, bounds.center, - bounds.extents, results, limit); - for (int i = 0; i < numResults; i++) { - items.Add(idsToItems[results[i]]); + + /// + /// Remove an item from the system. + /// + /// Item to remove. + /// + /// Thrown when the item isn't in the tree. + public void Remove(T item) + { + nativeSpatialMutex.WaitOne(); + int id = itemIds[item]; + itemIds.Remove(item); + idsToItems.Remove(id); + itemBounds.Remove(item); + NativeSpatialFunction.SpatialPartitionerRemoveItem(spatialPartitionId, id); + nativeSpatialMutex.ReleaseMutex(); } - nativeSpatialMutex.ReleaseMutex(); - return items.Count > 0; - } - public Bounds BoundsForItem(T item) { - return itemBounds[item]; - } + /// + /// Find items contained entirely within the given bounds. + /// This method will create a set when the number of items + /// is greater than zero. + /// + /// Containing bounds. + /// Set of items found. Null when this + /// method returns false. + /// Maximum number of items to find. + /// true if any items are found. + public bool ContainedBy(Bounds bounds, out HashSet items, + int limit = SpatialIndex.MAX_INTERSECT_RESULTS) + { + nativeSpatialMutex.WaitOne(); + int numResults = NativeSpatialFunction.SpatialPartitionerContainedBy(spatialPartitionId, bounds.center, + bounds.extents, results, limit); + items = new HashSet(); + for (int i = 0; i < numResults; i++) + { + items.Add(idsToItems[results[i]]); + } + nativeSpatialMutex.ReleaseMutex(); + return items.Count > 0; + } + + /// + /// Returns whether the item is tracked in this system. + /// + public bool HasItem(T item) + { + nativeSpatialMutex.WaitOne(); + bool inItems = itemIds.ContainsKey(item); + nativeSpatialMutex.ReleaseMutex(); + return inItems; + } + + /// + /// Checks whether the supplied Bounds intersects anything in the system, and returns a HashSet + /// of intersection objects. Returns true if there were any intersections. + /// + public bool IntersectedBy(Bounds bounds, out HashSet items, + int limit = SpatialIndex.MAX_INTERSECT_RESULTS) + { + nativeSpatialMutex.WaitOne(); + int numResults = NativeSpatialFunction.SpatialPartitionerIntersectedBy(spatialPartitionId, bounds.center, + bounds.extents, results, limit); + items = new HashSet(); + for (int i = 0; i < numResults; i++) + { + items.Add(idsToItems[results[i]]); + } + nativeSpatialMutex.ReleaseMutex(); + return items.Count > 0; + } + + /// + /// Checks whether the supplied Bounds intersects anything in the system, and fills the supplied preallocated Hashset + /// with intersected items. Returns true if there were any intersections. + /// + public bool IntersectedByPreallocated(Bounds bounds, ref HashSet items, + int limit = SpatialIndex.MAX_INTERSECT_RESULTS) + { + nativeSpatialMutex.WaitOne(); + int numResults = NativeSpatialFunction.SpatialPartitionerIntersectedBy(spatialPartitionId, bounds.center, + bounds.extents, results, limit); + for (int i = 0; i < numResults; i++) + { + items.Add(idsToItems[results[i]]); + } + nativeSpatialMutex.ReleaseMutex(); + return items.Count > 0; + } + + public Bounds BoundsForItem(T item) + { + return itemBounds[item]; + } } diff --git a/Assets/Scripts/model/util/OctreeImpl.cs b/Assets/Scripts/model/util/OctreeImpl.cs index 77fbdb67..54d2a1b1 100644 --- a/Assets/Scripts/model/util/OctreeImpl.cs +++ b/Assets/Scripts/model/util/OctreeImpl.cs @@ -17,298 +17,345 @@ using System; using com.google.apps.peltzer.client.model.core; -namespace com.google.apps.peltzer.client.model.util { - - /// - /// Spatial index for objects based on their Bounds. - /// - public class OctreeImpl : CollisionSystem { - // Number of items stored in a Node before we decide to split that node. - private static readonly int SPLIT_SIZE = 10; - // Max depth of the tree. Since the tree divides the axis by two at - // each level, the size of the smallest node is initial_size/2^MAX_DEPTH. - // 10 is a reasonable default. Can be an Octree param if needed. - private static readonly int MAX_DEPTH = 10; - private readonly Bounds bounds; - private readonly OTNode root; - private readonly Dictionary itemBounds = - new Dictionary(); - private readonly Dictionary itemNode = - new Dictionary(); +namespace com.google.apps.peltzer.client.model.util +{ /// - /// Create an empty Octree. + /// Spatial index for objects based on their Bounds. /// - /// Bounds of the Octree. - /// All items added to this index must be within these bounds. - /// - public OctreeImpl(Bounds bounds) { - this.bounds = bounds; - root = new OTNode(this, bounds, 0 /* Depth */); - } - - /// - /// Add an item to the Octree. - /// - /// The item to add - /// The item's initial bounds. - /// - /// Thrown when bounds is not contained by the Octree's bounds. - public void Add(T item, Bounds bounds) { - AssertOrThrow.False(itemBounds.ContainsKey(item), - "Cannot re-add item using the same key. Use Update to change an item's bounds."); - itemBounds[item] = bounds; - OTNode node = root.Add(item, bounds); - itemNode[item] = node; - } - - /// - /// Update the bounds of an item. The item must already exist - /// in the index. - /// - /// The item to update. - /// The item's updated bounds. - /// - /// Thrown when the item isn't in the tree. - public void UpdateItemBounds(T item, Bounds bounds) { - OTNode oldNode = itemNode[item]; - oldNode.Remove(item); - itemBounds[item] = bounds; - itemNode[item] = root.Add(item, bounds); - } + public class OctreeImpl : CollisionSystem + { + // Number of items stored in a Node before we decide to split that node. + private static readonly int SPLIT_SIZE = 10; + // Max depth of the tree. Since the tree divides the axis by two at + // each level, the size of the smallest node is initial_size/2^MAX_DEPTH. + // 10 is a reasonable default. Can be an Octree param if needed. + private static readonly int MAX_DEPTH = 10; + private readonly Bounds bounds; + private readonly OTNode root; + private readonly Dictionary itemBounds = + new Dictionary(); + private readonly Dictionary itemNode = + new Dictionary(); - /// - /// Remove an item from the index. - /// - /// Item to remove. - /// - /// Thrown when the item isn't in the tree. - public void Remove(T item) { - AssertOrThrow.True(itemNode.ContainsKey(item), - "Item is specified for removal but is not in the tree."); - OTNode oldNode = itemNode[item]; - oldNode.Remove(item); - itemNode.Remove(item); - itemBounds.Remove(item); - } + /// + /// Create an empty Octree. + /// + /// Bounds of the Octree. + /// All items added to this index must be within these bounds. + /// + public OctreeImpl(Bounds bounds) + { + this.bounds = bounds; + root = new OTNode(this, bounds, 0 /* Depth */); + } - /// - /// Find items contained entirely within the given bounds. - /// This method will create a set when the number of items - /// is greater than zero. - /// - /// Containing bounds. - /// Set of items found. Null when this - /// method returns false. - /// Maximum number of items to find. - /// true if any items are found. - public bool ContainedBy(Bounds bounds, out HashSet items, - int limit = SpatialIndex.MAX_INTERSECT_RESULTS) { - items = null; - return root.ContainedBy(bounds, ref items, limit); - } + /// + /// Add an item to the Octree. + /// + /// The item to add + /// The item's initial bounds. + /// + /// Thrown when bounds is not contained by the Octree's bounds. + public void Add(T item, Bounds bounds) + { + AssertOrThrow.False(itemBounds.ContainsKey(item), + "Cannot re-add item using the same key. Use Update to change an item's bounds."); + itemBounds[item] = bounds; + OTNode node = root.Add(item, bounds); + itemNode[item] = node; + } - /// - /// Find items that intersect the given bounds. - /// This method will create a Set when the number of items - /// is greater than zero. - /// - /// Intersecting bounds. - /// Set of items found. Null when this - /// method returns false. - /// Maximum number of items to find. - /// true if any items are found. - public bool IntersectedBy(Bounds bounds, out HashSet items, - int limit = SpatialIndex.MAX_INTERSECT_RESULTS) { - items = null; - return root.IntersectedBy(bounds, ref items, limit); - } + /// + /// Update the bounds of an item. The item must already exist + /// in the index. + /// + /// The item to update. + /// The item's updated bounds. + /// + /// Thrown when the item isn't in the tree. + public void UpdateItemBounds(T item, Bounds bounds) + { + OTNode oldNode = itemNode[item]; + oldNode.Remove(item); + itemBounds[item] = bounds; + itemNode[item] = root.Add(item, bounds); + } - /// - /// True if the given item is in the tree. - /// - public bool HasItem(T item) { - return itemNode.ContainsKey(item); - } + /// + /// Remove an item from the index. + /// + /// Item to remove. + /// + /// Thrown when the item isn't in the tree. + public void Remove(T item) + { + AssertOrThrow.True(itemNode.ContainsKey(item), + "Item is specified for removal but is not in the tree."); + OTNode oldNode = itemNode[item]; + oldNode.Remove(item); + itemNode.Remove(item); + itemBounds.Remove(item); + } - // Return the bounds specified when the item was inserted or updated. - public Bounds BoundsForItem(T item) { - return itemBounds[item]; - } + /// + /// Find items contained entirely within the given bounds. + /// This method will create a set when the number of items + /// is greater than zero. + /// + /// Containing bounds. + /// Set of items found. Null when this + /// method returns false. + /// Maximum number of items to find. + /// true if any items are found. + public bool ContainedBy(Bounds bounds, out HashSet items, + int limit = SpatialIndex.MAX_INTERSECT_RESULTS) + { + items = null; + return root.ContainedBy(bounds, ref items, limit); + } - private OTNode NodeForItem(T item) { - return itemNode[item]; - } + /// + /// Find items that intersect the given bounds. + /// This method will create a Set when the number of items + /// is greater than zero. + /// + /// Intersecting bounds. + /// Set of items found. Null when this + /// method returns false. + /// Maximum number of items to find. + /// true if any items are found. + public bool IntersectedBy(Bounds bounds, out HashSet items, + int limit = SpatialIndex.MAX_INTERSECT_RESULTS) + { + items = null; + return root.IntersectedBy(bounds, ref items, limit); + } - private void UpdateItemNode(T item, OTNode node) { - itemNode[item] = node; - } - - /// - /// Checks whether the supplied Bounds intersects anything in the system, and fills the supplied preallocated Hashset - /// with intersected items. Returns true if there were any intersections. - /// - /// - /// - /// - /// - public bool IntersectedByPreallocated(Bounds bounds, ref HashSet items, - int limit = SpatialIndex.MAX_INTERSECT_RESULTS) { - return true; - } + /// + /// True if the given item is in the tree. + /// + public bool HasItem(T item) + { + return itemNode.ContainsKey(item); + } - // Public for testing. - public static Bounds SubBounds(Bounds parent, int idx) { - Vector3 childSize = parent.size / 2.0f; - Vector3 extents = parent.extents / 2.0f; - Vector3 center = parent.center; - Vector3 childCenter = new Vector3( - (idx & 1) > 0 ? center.x - extents.x : center.x + extents.x, - (idx & 2) > 0 ? center.y - extents.y : center.y + extents.y, - (idx & 4) > 0 ? center.z - extents.z : center.z + extents.z); - return new Bounds(childCenter, childSize); - } + // Return the bounds specified when the item was inserted or updated. + public Bounds BoundsForItem(T item) + { + return itemBounds[item]; + } - // VisibleForTesting - public OTNode GetRootNode() { - return root; - } + private OTNode NodeForItem(T item) + { + return itemNode[item]; + } - // VisibleForTesting - public Bounds GetBounds() { - return bounds; - } + private void UpdateItemNode(T item, OTNode node) + { + itemNode[item] = node; + } + /// + /// Checks whether the supplied Bounds intersects anything in the system, and fills the supplied preallocated Hashset + /// with intersected items. Returns true if there were any intersections. + /// + /// + /// + /// + /// + public bool IntersectedByPreallocated(Bounds bounds, ref HashSet items, + int limit = SpatialIndex.MAX_INTERSECT_RESULTS) + { + return true; + } - // Tree structure to contain items. - public class OTNode { - private readonly int depth; - private readonly OctreeImpl tree; - private readonly Bounds bounds; - private HashSet items = new HashSet(); - private OTNode[] childNodes = null; - - internal OTNode(OctreeImpl tree, Bounds bounds, int depth) { - this.tree = tree; - this.bounds = bounds; - this.depth = depth; - } - - internal OTNode Add(T item, Bounds itemBounds) { - AssertOrThrow.True(Math3d.ContainsBounds(bounds, itemBounds), - "Item has bounds outside of tree bounds"); - if (childNodes == null) { - if (items.Count >= SPLIT_SIZE && depth < MAX_DEPTH) { - SplitNode(); - // Recursively re-call this function, post-split - return Add(item, itemBounds); - } else { - items.Add(item); - return this; - } - } else { - for (int i = 0; i < 8; i++) { - if (Math3d.ContainsBounds(SubBounds(bounds, i), itemBounds)) { - if (childNodes[i] == null) { - childNodes[i] = new OTNode( - tree, SubBounds(bounds, i), depth + 1); - } - return childNodes[i].Add(item, itemBounds); - } - } - // Wasn't bounded by any children, add it locally. - items.Add(item); - return this; + // Public for testing. + public static Bounds SubBounds(Bounds parent, int idx) + { + Vector3 childSize = parent.size / 2.0f; + Vector3 extents = parent.extents / 2.0f; + Vector3 center = parent.center; + Vector3 childCenter = new Vector3( + (idx & 1) > 0 ? center.x - extents.x : center.x + extents.x, + (idx & 2) > 0 ? center.y - extents.y : center.y + extents.y, + (idx & 4) > 0 ? center.z - extents.z : center.z + extents.z); + return new Bounds(childCenter, childSize); } - } - - internal void SplitNode() { - childNodes = new OTNode[8]; - // Take all local items, remove them, then re-add them. - HashSet toAdd = items; - items = new HashSet(); - foreach (T item in toAdd) { - OTNode addedTo = Add(item, tree.BoundsForItem(item)); - tree.UpdateItemNode(item, addedTo); + + // VisibleForTesting + public OTNode GetRootNode() + { + return root; } - } - - internal void Remove(T item) { - AssertOrThrow.True(items.Remove(item), - "Item is specified for removal but is not in the tree."); - } - - // Add items to the set from here and below within the tree. - // The resulting set is created on-demand. Returns 'true' - // if items match the query. - internal bool ContainedBy( - Bounds container, ref HashSet contained, int limit) { - bool foundItems = false; - - foreach (T item in items) { - if (contained != null && contained.Count >= limit) { - return true; - } - if (Math3d.ContainsBounds(container, tree.BoundsForItem(item))) { - EnsureSet(ref contained); - foundItems = true; - contained.Add(item); - } + + // VisibleForTesting + public Bounds GetBounds() + { + return bounds; } - if (childNodes != null) { - for (int i = 0; i < 8; i++) { - if (childNodes[i] != null - && childNodes[i].bounds.Intersects(container)) { - foundItems = childNodes[i].ContainedBy( - container, ref contained, limit) - || foundItems; + + // Tree structure to contain items. + public class OTNode + { + private readonly int depth; + private readonly OctreeImpl tree; + private readonly Bounds bounds; + private HashSet items = new HashSet(); + private OTNode[] childNodes = null; + + internal OTNode(OctreeImpl tree, Bounds bounds, int depth) + { + this.tree = tree; + this.bounds = bounds; + this.depth = depth; } - } - } - return foundItems; - } + internal OTNode Add(T item, Bounds itemBounds) + { + AssertOrThrow.True(Math3d.ContainsBounds(bounds, itemBounds), + "Item has bounds outside of tree bounds"); + if (childNodes == null) + { + if (items.Count >= SPLIT_SIZE && depth < MAX_DEPTH) + { + SplitNode(); + // Recursively re-call this function, post-split + return Add(item, itemBounds); + } + else + { + items.Add(item); + return this; + } + } + else + { + for (int i = 0; i < 8; i++) + { + if (Math3d.ContainsBounds(SubBounds(bounds, i), itemBounds)) + { + if (childNodes[i] == null) + { + childNodes[i] = new OTNode( + tree, SubBounds(bounds, i), depth + 1); + } + return childNodes[i].Add(item, itemBounds); + } + } + // Wasn't bounded by any children, add it locally. + items.Add(item); + return this; + } + } - // Add items to the set from here and below within the tree. - // The resulting set is created on-demand. Returns 'true' - // if items match the query. - internal bool IntersectedBy( - Bounds intersectBounds, ref HashSet intersected, int limit) { - bool foundItems = false; + internal void SplitNode() + { + childNodes = new OTNode[8]; + // Take all local items, remove them, then re-add them. + HashSet toAdd = items; + items = new HashSet(); + foreach (T item in toAdd) + { + OTNode addedTo = Add(item, tree.BoundsForItem(item)); + tree.UpdateItemNode(item, addedTo); + } + } - foreach (T item in items) { - if (intersected != null && intersected.Count >= limit) { - return true; - } - if (tree.BoundsForItem(item).Intersects(intersectBounds)) { - EnsureSet(ref intersected); - foundItems = true; - intersected.Add(item); - } - } + internal void Remove(T item) + { + AssertOrThrow.True(items.Remove(item), + "Item is specified for removal but is not in the tree."); + } + + // Add items to the set from here and below within the tree. + // The resulting set is created on-demand. Returns 'true' + // if items match the query. + internal bool ContainedBy( + Bounds container, ref HashSet contained, int limit) + { + bool foundItems = false; + + foreach (T item in items) + { + if (contained != null && contained.Count >= limit) + { + return true; + } + if (Math3d.ContainsBounds(container, tree.BoundsForItem(item))) + { + EnsureSet(ref contained); + foundItems = true; + contained.Add(item); + } + } + + if (childNodes != null) + { + for (int i = 0; i < 8; i++) + { + if (childNodes[i] != null + && childNodes[i].bounds.Intersects(container)) + { + foundItems = childNodes[i].ContainedBy( + container, ref contained, limit) + || foundItems; + } + } + } - if (childNodes != null) { - for (int i = 0; i < 8; i++) { - if (childNodes[i] != null - && childNodes[i].bounds.Intersects(intersectBounds)) { - foundItems = childNodes[i].IntersectedBy( - intersectBounds, ref intersected, limit) - || foundItems; + return foundItems; + } + + // Add items to the set from here and below within the tree. + // The resulting set is created on-demand. Returns 'true' + // if items match the query. + internal bool IntersectedBy( + Bounds intersectBounds, ref HashSet intersected, int limit) + { + bool foundItems = false; + + foreach (T item in items) + { + if (intersected != null && intersected.Count >= limit) + { + return true; + } + if (tree.BoundsForItem(item).Intersects(intersectBounds)) + { + EnsureSet(ref intersected); + foundItems = true; + intersected.Add(item); + } + } + + if (childNodes != null) + { + for (int i = 0; i < 8; i++) + { + if (childNodes[i] != null + && childNodes[i].bounds.Intersects(intersectBounds)) + { + foundItems = childNodes[i].IntersectedBy( + intersectBounds, ref intersected, limit) + || foundItems; + } + } + } + return foundItems; } - } - } - return foundItems; - } - private void EnsureSet(ref HashSet set) { - set = set != null ? set : new HashSet(); - } + private void EnsureSet(ref HashSet set) + { + set = set != null ? set : new HashSet(); + } - // VisibleForTesting - public OTNode[] GetChildNodes() { - return childNodes; - } + // VisibleForTesting + public OTNode[] GetChildNodes() + { + return childNodes; + } + } } - } } diff --git a/Assets/Scripts/model/util/PersistentBlobCache.cs b/Assets/Scripts/model/util/PersistentBlobCache.cs index 8e4a62c8..16734e5f 100644 --- a/Assets/Scripts/model/util/PersistentBlobCache.cs +++ b/Assets/Scripts/model/util/PersistentBlobCache.cs @@ -25,370 +25,415 @@ // Based on // https://cs/google3/vr/assets/client/unity/ZandriaUnityClient/Assets/ZUC/Scripts/zuc_internal/caching/PersistentBlobCache.cs -namespace com.google.apps.peltzer.client.model.util { - /// - /// A persistent disk-based LRU cache that can store associations of strings to arbitrary data. - /// - /// This can be used, for example, to implement a download cache for remote assets. This - /// class is agnostic to the actual meaning of the keys and values. To this class, keys are just - /// unique strings and values are just opaque byte arrays. - /// - /// This cache automatically offloads heavy work (I/O, decoding, etc) to a background thread to avoid - /// blocking the main thread. - /// - /// NOTE: We're not currently handling I/O errors -- the cache assumes that the file system works - /// perfectly, which is a pretty fair assumption since we're using a hidden directory under - /// AppData\Local\.... that normal users don't normally access (or even know about). - /// Unless the user goes and messes around with the permissions of the directory, everything should - /// work correctly. If we do get an I/O error (which would be rare), then we will just crash. - /// - [ExecuteInEditMode] - public class PersistentBlobCache : MonoBehaviour { - private const string BLOB_FILE_EXT = ".blob"; - - public delegate void CacheReadCallback(bool success, byte[] data); - - /// - /// Indicates whether Setup() was completed. - /// - private bool setupDone = false; - - /// - /// Maximum number of entries allowed in the cache. - /// - private int maxEntries; - - /// - /// Maximum total bytes allowed in the cache. - /// - private long maxSizeBytes; - - /// - /// Root path to the cache. - /// - private string rootPath; - - /// - /// MD5 hash computing function. - /// - private MD5 md5; - - /// - /// Maps key hash to cache entry. - /// This is owned by the BACKGROUND thread. - /// - private Dictionary cacheEntries = new Dictionary(); - - /// - /// Requests that are pending background work. - /// - private ConcurrentQueue requestsPendingWork = new ConcurrentQueue(); - - /// - /// Requests for which the background work is done, and which are pending delivery of callback in the - /// main thread. - /// - private ConcurrentQueue requestsPendingDelivery = new ConcurrentQueue(); - - /// - /// Recycle pool of requests (to avoid reduce allocation). - /// - private ConcurrentQueue requestsRecyclePool = new ConcurrentQueue(); - - /// - /// Sets up a cache with the given characteristics. - /// - /// The absolute path to the root of the cache. - /// The maximum number of entries in the cache. - /// The maximum combined size of all entries in the cache. - public void Setup(string rootPath, int maxEntries, long maxSizeBytes) { - this.rootPath = rootPath; - this.maxEntries = maxEntries; - this.maxSizeBytes = maxSizeBytes; - - // Check that we have a reasonable config: - AssertOrThrow.True(rootPath != null && rootPath.Length != 0, "rootPath can't be null or empty"); - AssertOrThrow.True(Directory.Exists(rootPath), "rootPath must be an existing directory: " + rootPath); - AssertOrThrow.True(maxEntries >= 256, "maxEntries must be >= 256"); - AssertOrThrow.True(maxSizeBytes >= 1048576, "maxSizeBytes must be >= 1MB"); - - md5 = MD5.Create(); - InitializeCache(); - - setupDone = true; - - Thread backgroundThread = new Thread(BackgroundThreadMain); - backgroundThread.IsBackground = true; - backgroundThread.Start(); - } - - /// - /// Requests a read from the cache. - /// - /// The key to read. - /// Maximum age for a cache hit. If the copy we have on cache is older - /// than this, the request will fail. Use -1 to mean "any age". - /// The callback that is to be called (asynchronously) when the read operation - /// finishes. This callback will be called on the MAIN thread. - public void RequestRead(string key, long maxAgeMillis, CacheReadCallback callback) { - string hash = GetHash(key); - CacheRequest request; - if (!requestsRecyclePool.Dequeue(out request)) { - request = new CacheRequest(); - } - request.type = RequestType.READ; - request.key = key; - request.hash = hash; - request.readCallback = callback; - request.maxAgeMillis = maxAgeMillis; - requestsPendingWork.Enqueue(request); - } - - /// - /// Requests a write to the cache. The data will be written asynchronously. - /// - /// The key to write. - /// The data to write. - public void RequestWrite(string key, byte[] data) { - string hash = GetHash(key); - CacheRequest request; - if (!requestsRecyclePool.Dequeue(out request)) { - request = new CacheRequest(); - } - request.type = RequestType.WRITE; - request.key = key; - request.hash = hash; - request.data = data; - requestsPendingWork.Enqueue(request); - } - - /// - /// Requests that the cache be cleared. The cache will be cleared asynchronously. - /// - public void RequestClear() { - CacheRequest request; - if (!requestsRecyclePool.Dequeue(out request)) { - request = new CacheRequest(); - } - request.type = RequestType.CLEAR; - requestsPendingWork.Enqueue(request); - } - +namespace com.google.apps.peltzer.client.model.util +{ /// - /// Checks for pending deliveries and delivers them. - /// - private void Update() { - if (!setupDone) return; - - // To avoid locking the queue on every frame, exit early if the volatile count is 0. - if (requestsPendingDelivery.VolatileCount == 0) return; - - // Check for a pending delivery. - // Note that for performance reasons, we limit ourselves to delivering one result per frame. - CacheRequest delivery; - if (!requestsPendingDelivery.Dequeue(out delivery)) { - return; - } - - // Deliver the results to the callback. - delivery.readCallback(delivery.success, delivery.data); - - // Recycle the request for reuse. - delivery.Reset(); - requestsRecyclePool.Enqueue(delivery); - } - - /// - /// Initializes the cache (reads the cache state from disk). + /// A persistent disk-based LRU cache that can store associations of strings to arbitrary data. + /// + /// This can be used, for example, to implement a download cache for remote assets. This + /// class is agnostic to the actual meaning of the keys and values. To this class, keys are just + /// unique strings and values are just opaque byte arrays. + /// + /// This cache automatically offloads heavy work (I/O, decoding, etc) to a background thread to avoid + /// blocking the main thread. + /// + /// NOTE: We're not currently handling I/O errors -- the cache assumes that the file system works + /// perfectly, which is a pretty fair assumption since we're using a hidden directory under + /// AppData\Local\.... that normal users don't normally access (or even know about). + /// Unless the user goes and messes around with the permissions of the directory, everything should + /// work correctly. If we do get an I/O error (which would be rare), then we will just crash. /// - private void InitializeCache() { - foreach (string file in Directory.GetFiles(rootPath)) { - if (file.EndsWith(BLOB_FILE_EXT)) { - FileInfo finfo = new FileInfo(file); - string hash = Path.GetFileNameWithoutExtension(file).ToLowerInvariant(); - cacheEntries[hash] = new CacheEntry(hash, finfo.Length, TicksToMillis(finfo.LastWriteTimeUtc.Ticks)); + [ExecuteInEditMode] + public class PersistentBlobCache : MonoBehaviour + { + private const string BLOB_FILE_EXT = ".blob"; + + public delegate void CacheReadCallback(bool success, byte[] data); + + /// + /// Indicates whether Setup() was completed. + /// + private bool setupDone = false; + + /// + /// Maximum number of entries allowed in the cache. + /// + private int maxEntries; + + /// + /// Maximum total bytes allowed in the cache. + /// + private long maxSizeBytes; + + /// + /// Root path to the cache. + /// + private string rootPath; + + /// + /// MD5 hash computing function. + /// + private MD5 md5; + + /// + /// Maps key hash to cache entry. + /// This is owned by the BACKGROUND thread. + /// + private Dictionary cacheEntries = new Dictionary(); + + /// + /// Requests that are pending background work. + /// + private ConcurrentQueue requestsPendingWork = new ConcurrentQueue(); + + /// + /// Requests for which the background work is done, and which are pending delivery of callback in the + /// main thread. + /// + private ConcurrentQueue requestsPendingDelivery = new ConcurrentQueue(); + + /// + /// Recycle pool of requests (to avoid reduce allocation). + /// + private ConcurrentQueue requestsRecyclePool = new ConcurrentQueue(); + + /// + /// Sets up a cache with the given characteristics. + /// + /// The absolute path to the root of the cache. + /// The maximum number of entries in the cache. + /// The maximum combined size of all entries in the cache. + public void Setup(string rootPath, int maxEntries, long maxSizeBytes) + { + this.rootPath = rootPath; + this.maxEntries = maxEntries; + this.maxSizeBytes = maxSizeBytes; + + // Check that we have a reasonable config: + AssertOrThrow.True(rootPath != null && rootPath.Length != 0, "rootPath can't be null or empty"); + AssertOrThrow.True(Directory.Exists(rootPath), "rootPath must be an existing directory: " + rootPath); + AssertOrThrow.True(maxEntries >= 256, "maxEntries must be >= 256"); + AssertOrThrow.True(maxSizeBytes >= 1048576, "maxSizeBytes must be >= 1MB"); + + md5 = MD5.Create(); + InitializeCache(); + + setupDone = true; + + Thread backgroundThread = new Thread(BackgroundThreadMain); + backgroundThread.IsBackground = true; + backgroundThread.Start(); } - } - } - /// - /// Returns the hash of the given key. - /// - /// The input string. - /// The hash of the input string, in lowercase hexadecimal format. - private string GetHash(string key) { - byte[] hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(key)); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < hashBytes.Length; i++) { - // x2 is 2-digit hexadecimal format (like "d8") - sb.Append(hashBytes[i].ToString("x2")); - } - // x4 is 4-digit hexadecimal format (like "a1b2"). - return sb.ToString() + key.Length.ToString("x4"); - } - - /// - /// Returns the full path corresponding to a hash value. - /// - /// The hash value - /// The full path to the file that stores the asset with the indicated hash. - private string HashToFullPath(string hash) { - return Path.Combine(rootPath, hash + BLOB_FILE_EXT); - } - - /// - /// (Background thread). Main function that constantly checks the queues for pending requests and executes - /// them as they arrive. - /// - private void BackgroundThreadMain() { - try { - while (true) { - CacheRequest request; - - // Wait until the next request comes in. - if (!requestsPendingWork.WaitAndDequeue(/* waitTime */ 5000, out request)) { - continue; - } - - // Process it. - switch (request.type) { - case RequestType.READ: - BackgroundHandleReadRequest(request); - break; - case RequestType.WRITE: - BackgroundHandleWriteRequest(request); - break; - case RequestType.CLEAR: - BackgroundHandleClearRequest(request); - break; - default: - throw new Exception("Invalid cache request type, should be READ, WRITE or CLEAR."); - } + /// + /// Requests a read from the cache. + /// + /// The key to read. + /// Maximum age for a cache hit. If the copy we have on cache is older + /// than this, the request will fail. Use -1 to mean "any age". + /// The callback that is to be called (asynchronously) when the read operation + /// finishes. This callback will be called on the MAIN thread. + public void RequestRead(string key, long maxAgeMillis, CacheReadCallback callback) + { + string hash = GetHash(key); + CacheRequest request; + if (!requestsRecyclePool.Dequeue(out request)) + { + request = new CacheRequest(); + } + request.type = RequestType.READ; + request.key = key; + request.hash = hash; + request.readCallback = callback; + request.maxAgeMillis = maxAgeMillis; + requestsPendingWork.Enqueue(request); } - } catch (ThreadAbortException) { - // That's ok (happens on project shutdown). - } catch (Exception ex) { - Debug.LogErrorFormat("Cache background thread crashed: " + ex); - } - } - - /// - /// (Background thread). Handles a read request, reading it from disk and scheduling the delivery - /// of the results to the caller. - /// - /// The read request to execute. - private void BackgroundHandleReadRequest(CacheRequest readRequest) { - string fullPath = HashToFullPath(readRequest.hash); - - CacheEntry entry; - if (!cacheEntries.TryGetValue(readRequest.hash, out entry)) { - // Not in the cache - readRequest.data = null; - readRequest.success = false; - } else if (readRequest.maxAgeMillis > 0 && entry.AgeMillis > readRequest.maxAgeMillis) { - // Too old. - readRequest.data = null; - readRequest.success = false; - } else if (!File.Exists(fullPath)) { - // Too old. - readRequest.data = null; - readRequest.success = false; - } else { - // Found it. - readRequest.data = File.ReadAllBytes(fullPath); - readRequest.success = true; - // Update the read timestamp. - entry.readTimestampMillis = TicksToMillis(DateTime.UtcNow.Ticks); - } - // Schedule the result for delivery to the caller. - requestsPendingDelivery.Enqueue(readRequest); - } + /// + /// Requests a write to the cache. The data will be written asynchronously. + /// + /// The key to write. + /// The data to write. + public void RequestWrite(string key, byte[] data) + { + string hash = GetHash(key); + CacheRequest request; + if (!requestsRecyclePool.Dequeue(out request)) + { + request = new CacheRequest(); + } + request.type = RequestType.WRITE; + request.key = key; + request.hash = hash; + request.data = data; + requestsPendingWork.Enqueue(request); + } - /// - /// (Background thread). Handles a write request. Writes the data to disk. - /// - /// The write request to execute. - private void BackgroundHandleWriteRequest(CacheRequest writeRequest) { - string fullPath = HashToFullPath(writeRequest.hash); - string tempPath = Path.Combine(Path.GetDirectoryName(fullPath), "temp.dat"); - - // In the event of a crash or hardware issues -- e.g., user trips on the power cord, our write - // to disk might be interrupted in an inconsistent state. So instead of writing directly to - // the destination file, we write to a temporary file and then move. - File.WriteAllBytes(tempPath, writeRequest.data); - if (File.Exists(fullPath)) File.Delete(fullPath); - File.Move(tempPath, fullPath); - - // Update the file size and last used time information in the cache. - CacheEntry entry; - if (!cacheEntries.TryGetValue(writeRequest.hash, out entry)) { - entry = cacheEntries[writeRequest.hash] = new CacheEntry(writeRequest.hash); - } - entry.fileSize = writeRequest.data.Length; - entry.writeTimestampMillis = TicksToMillis(DateTime.UtcNow.Ticks); + /// + /// Requests that the cache be cleared. The cache will be cleared asynchronously. + /// + public void RequestClear() + { + CacheRequest request; + if (!requestsRecyclePool.Dequeue(out request)) + { + request = new CacheRequest(); + } + request.type = RequestType.CLEAR; + requestsPendingWork.Enqueue(request); + } - // We are done with writeRequest, so we can recycle it. - writeRequest.Reset(); - requestsRecyclePool.Enqueue(writeRequest); + /// + /// Checks for pending deliveries and delivers them. + /// + private void Update() + { + if (!setupDone) return; + + // To avoid locking the queue on every frame, exit early if the volatile count is 0. + if (requestsPendingDelivery.VolatileCount == 0) return; + + // Check for a pending delivery. + // Note that for performance reasons, we limit ourselves to delivering one result per frame. + CacheRequest delivery; + if (!requestsPendingDelivery.Dequeue(out delivery)) + { + return; + } + + // Deliver the results to the callback. + delivery.readCallback(delivery.success, delivery.data); + + // Recycle the request for reuse. + delivery.Reset(); + requestsRecyclePool.Enqueue(delivery); + } - // Check if the cache needs trimming. - TrimCache(); - } + /// + /// Initializes the cache (reads the cache state from disk). + /// + private void InitializeCache() + { + foreach (string file in Directory.GetFiles(rootPath)) + { + if (file.EndsWith(BLOB_FILE_EXT)) + { + FileInfo finfo = new FileInfo(file); + string hash = Path.GetFileNameWithoutExtension(file).ToLowerInvariant(); + cacheEntries[hash] = new CacheEntry(hash, finfo.Length, TicksToMillis(finfo.LastWriteTimeUtc.Ticks)); + } + } + } - /// - /// (Background thread). Clears the entire cache. - /// - private void BackgroundHandleClearRequest(CacheRequest clearRequest) { - foreach (string file in Directory.GetFiles(rootPath, "*" + BLOB_FILE_EXT)) { - File.Delete(file); - } - cacheEntries.Clear(); - clearRequest.Reset(); - requestsRecyclePool.Enqueue(clearRequest); - } + /// + /// Returns the hash of the given key. + /// + /// The input string. + /// The hash of the input string, in lowercase hexadecimal format. + private string GetHash(string key) + { + byte[] hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(key)); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < hashBytes.Length; i++) + { + // x2 is 2-digit hexadecimal format (like "d8") + sb.Append(hashBytes[i].ToString("x2")); + } + // x4 is 4-digit hexadecimal format (like "a1b2"). + return sb.ToString() + key.Length.ToString("x4"); + } - private void TrimCache() { - long totalSize = 0; - foreach (CacheEntry cacheEntry in cacheEntries.Values) { - totalSize += cacheEntry.fileSize; - } + /// + /// Returns the full path corresponding to a hash value. + /// + /// The hash value + /// The full path to the file that stores the asset with the indicated hash. + private string HashToFullPath(string hash) + { + return Path.Combine(rootPath, hash + BLOB_FILE_EXT); + } - if (totalSize <= maxSizeBytes && cacheEntries.Count <= maxEntries) { - // We're within budget, no need to trim the cache. - return; - } + /// + /// (Background thread). Main function that constantly checks the queues for pending requests and executes + /// them as they arrive. + /// + private void BackgroundThreadMain() + { + try + { + while (true) + { + CacheRequest request; + + // Wait until the next request comes in. + if (!requestsPendingWork.WaitAndDequeue(/* waitTime */ 5000, out request)) + { + continue; + } + + // Process it. + switch (request.type) + { + case RequestType.READ: + BackgroundHandleReadRequest(request); + break; + case RequestType.WRITE: + BackgroundHandleWriteRequest(request); + break; + case RequestType.CLEAR: + BackgroundHandleClearRequest(request); + break; + default: + throw new Exception("Invalid cache request type, should be READ, WRITE or CLEAR."); + } + } + } + catch (ThreadAbortException) + { + // That's ok (happens on project shutdown). + } + catch (Exception ex) + { + Debug.LogErrorFormat("Cache background thread crashed: " + ex); + } + } - // Sort the entries from oldest to newest. This is the order in which we will evict them. - Queue entriesOldestToNewest = - new Queue(cacheEntries.Values.OrderBy(entry => entry.writeTimestampMillis)); + /// + /// (Background thread). Handles a read request, reading it from disk and scheduling the delivery + /// of the results to the caller. + /// + /// The read request to execute. + private void BackgroundHandleReadRequest(CacheRequest readRequest) + { + string fullPath = HashToFullPath(readRequest.hash); + + CacheEntry entry; + if (!cacheEntries.TryGetValue(readRequest.hash, out entry)) + { + // Not in the cache + readRequest.data = null; + readRequest.success = false; + } + else if (readRequest.maxAgeMillis > 0 && entry.AgeMillis > readRequest.maxAgeMillis) + { + // Too old. + readRequest.data = null; + readRequest.success = false; + } + else if (!File.Exists(fullPath)) + { + // Too old. + readRequest.data = null; + readRequest.success = false; + } + else + { + // Found it. + readRequest.data = File.ReadAllBytes(fullPath); + readRequest.success = true; + // Update the read timestamp. + entry.readTimestampMillis = TicksToMillis(DateTime.UtcNow.Ticks); + } + + // Schedule the result for delivery to the caller. + requestsPendingDelivery.Enqueue(readRequest); + } - // Each iteration evicts the oldest item, until we're back under budget. - while (totalSize > maxSizeBytes || cacheEntries.Count > maxEntries) { - // What's the oldest file? - if (entriesOldestToNewest.Count == 0) break; - CacheEntry oldest = entriesOldestToNewest.Dequeue(); + /// + /// (Background thread). Handles a write request. Writes the data to disk. + /// + /// The write request to execute. + private void BackgroundHandleWriteRequest(CacheRequest writeRequest) + { + string fullPath = HashToFullPath(writeRequest.hash); + string tempPath = Path.Combine(Path.GetDirectoryName(fullPath), "temp.dat"); + + // In the event of a crash or hardware issues -- e.g., user trips on the power cord, our write + // to disk might be interrupted in an inconsistent state. So instead of writing directly to + // the destination file, we write to a temporary file and then move. + File.WriteAllBytes(tempPath, writeRequest.data); + if (File.Exists(fullPath)) File.Delete(fullPath); + File.Move(tempPath, fullPath); + + // Update the file size and last used time information in the cache. + CacheEntry entry; + if (!cacheEntries.TryGetValue(writeRequest.hash, out entry)) + { + entry = cacheEntries[writeRequest.hash] = new CacheEntry(writeRequest.hash); + } + entry.fileSize = writeRequest.data.Length; + entry.writeTimestampMillis = TicksToMillis(DateTime.UtcNow.Ticks); + + // We are done with writeRequest, so we can recycle it. + writeRequest.Reset(); + requestsRecyclePool.Enqueue(writeRequest); + + // Check if the cache needs trimming. + TrimCache(); + } - // Delete this file. - string filePath = HashToFullPath(oldest.hash); - if (File.Exists(filePath)) { - File.Delete(filePath); + /// + /// (Background thread). Clears the entire cache. + /// + private void BackgroundHandleClearRequest(CacheRequest clearRequest) + { + foreach (string file in Directory.GetFiles(rootPath, "*" + BLOB_FILE_EXT)) + { + File.Delete(file); + } + cacheEntries.Clear(); + clearRequest.Reset(); + requestsRecyclePool.Enqueue(clearRequest); } - cacheEntries.Remove(oldest.hash); - // Update our accounting - totalSize -= oldest.fileSize; - } - } + private void TrimCache() + { + long totalSize = 0; + foreach (CacheEntry cacheEntry in cacheEntries.Values) + { + totalSize += cacheEntry.fileSize; + } + + if (totalSize <= maxSizeBytes && cacheEntries.Count <= maxEntries) + { + // We're within budget, no need to trim the cache. + return; + } + + // Sort the entries from oldest to newest. This is the order in which we will evict them. + Queue entriesOldestToNewest = + new Queue(cacheEntries.Values.OrderBy(entry => entry.writeTimestampMillis)); + + // Each iteration evicts the oldest item, until we're back under budget. + while (totalSize > maxSizeBytes || cacheEntries.Count > maxEntries) + { + // What's the oldest file? + if (entriesOldestToNewest.Count == 0) break; + CacheEntry oldest = entriesOldestToNewest.Dequeue(); + + // Delete this file. + string filePath = HashToFullPath(oldest.hash); + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + cacheEntries.Remove(oldest.hash); + + // Update our accounting + totalSize -= oldest.fileSize; + } + } - private static long TicksToMillis(long ticks) { - // According to the docs: "A single tick represents one hundred nanoseconds or one ten-millionth of a - // second. There are 10,000 ticks in a millisecond, or 10 million ticks in a second." - // https://msdn.microsoft.com/en-us/library/system.datetime.ticks(v=vs.110).aspx - return ticks / 10000L; - } + private static long TicksToMillis(long ticks) + { + // According to the docs: "A single tick represents one hundred nanoseconds or one ten-millionth of a + // second. There are 10,000 ticks in a millisecond, or 10 million ticks in a second." + // https://msdn.microsoft.com/en-us/library/system.datetime.ticks(v=vs.110).aspx + return ticks / 10000L; + } - private void OnEnable() { + private void OnEnable() + { #if UNITY_EDITOR if (!Application.isPlaying) { // In the Unity Editor, we need to install a delegate to get Update() frequently @@ -396,9 +441,10 @@ private void OnEnable() { UnityEditor.EditorApplication.update += Update; } #endif - } + } - private void OnDisable() { + private void OnDisable() + { #if UNITY_EDITOR if (!Application.isPlaying) { // In the Unity Editor, we need to install a delegate to get Update() frequently @@ -406,105 +452,111 @@ private void OnDisable() { UnityEditor.EditorApplication.update -= Update; } #endif - } - - /// - /// Represents each entry in the cache. - /// - private class CacheEntry { - /// - /// Hash of the entry's key. - /// - public string hash; - - /// - /// Size of the file, in bytes. - /// - public long fileSize = 0; - - /// - /// Time in millis when the file was last written to. - /// - public long writeTimestampMillis = 0; - - /// - /// Time in millis when the file was last read. - /// - public long readTimestampMillis = 0; - - /// - /// Age of the file (millis since it was last written). - /// - public long AgeMillis { get { return TicksToMillis(DateTime.UtcNow.Ticks) - writeTimestampMillis; } } - - public CacheEntry(string hash) { this.hash = hash; } - public CacheEntry(string hash, long fileSize, long writeTimestampMillis) { - this.hash = hash; - this.fileSize = fileSize; - this.writeTimestampMillis = writeTimestampMillis; - } - } + } - public enum RequestType { - // Read a file from the cache. - READ, - // Write a file to the cache. - WRITE, - // Clear the entire cache. - CLEAR - } + /// + /// Represents each entry in the cache. + /// + private class CacheEntry + { + /// + /// Hash of the entry's key. + /// + public string hash; + + /// + /// Size of the file, in bytes. + /// + public long fileSize = 0; + + /// + /// Time in millis when the file was last written to. + /// + public long writeTimestampMillis = 0; + + /// + /// Time in millis when the file was last read. + /// + public long readTimestampMillis = 0; + + /// + /// Age of the file (millis since it was last written). + /// + public long AgeMillis { get { return TicksToMillis(DateTime.UtcNow.Ticks) - writeTimestampMillis; } } + + public CacheEntry(string hash) { this.hash = hash; } + public CacheEntry(string hash, long fileSize, long writeTimestampMillis) + { + this.hash = hash; + this.fileSize = fileSize; + this.writeTimestampMillis = writeTimestampMillis; + } + } - /// - /// Represents a cache operation request. - /// - /// We reuse the same class for read and write requests (even though it might be a bit confusing) - /// because we pool these objects to avoid allocation. - /// - private class CacheRequest { - /// - /// Type of request (see RequestType for details). - /// - public RequestType type; - /// - /// The key to read or write. - /// - public string key; - /// - /// The hash of the key. - /// - public string hash; - /// - /// The callback to call when the request is complete (for READ requests only). - /// - public CacheReadCallback readCallback; - /// - /// The request data. For READ requests, this is an out parameter that points to the - /// data at the end of the operation. For WRITE requests, this is an in parameter that - /// points to the data to write. - /// - public byte[] data; - /// - /// For READ requests, this is the maximum accepted age of the cached copy, in millis. - /// Ignored for WRITE requests. - /// - public long maxAgeMillis; - /// - /// Indicates whether or not the request was successful. Only used for READ requests. - /// - public bool success; - - public CacheRequest() { - Reset(); - } + public enum RequestType + { + // Read a file from the cache. + READ, + // Write a file to the cache. + WRITE, + // Clear the entire cache. + CLEAR + } - public void Reset() { - type = RequestType.READ; - hash = null; - readCallback = null; - success = false; - data = null; - maxAgeMillis = -1; - } + /// + /// Represents a cache operation request. + /// + /// We reuse the same class for read and write requests (even though it might be a bit confusing) + /// because we pool these objects to avoid allocation. + /// + private class CacheRequest + { + /// + /// Type of request (see RequestType for details). + /// + public RequestType type; + /// + /// The key to read or write. + /// + public string key; + /// + /// The hash of the key. + /// + public string hash; + /// + /// The callback to call when the request is complete (for READ requests only). + /// + public CacheReadCallback readCallback; + /// + /// The request data. For READ requests, this is an out parameter that points to the + /// data at the end of the operation. For WRITE requests, this is an in parameter that + /// points to the data to write. + /// + public byte[] data; + /// + /// For READ requests, this is the maximum accepted age of the cached copy, in millis. + /// Ignored for WRITE requests. + /// + public long maxAgeMillis; + /// + /// Indicates whether or not the request was successful. Only used for READ requests. + /// + public bool success; + + public CacheRequest() + { + Reset(); + } + + public void Reset() + { + type = RequestType.READ; + hash = null; + readCallback = null; + success = false; + data = null; + maxAgeMillis = -1; + } + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/util/Slerpee.cs b/Assets/Scripts/model/util/Slerpee.cs index b8ca7ef1..782dd2fe 100644 --- a/Assets/Scripts/model/util/Slerpee.cs +++ b/Assets/Scripts/model/util/Slerpee.cs @@ -18,130 +18,142 @@ using System.Text; using UnityEngine; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// A slerp (spherical linear interpolation). An instance of this class represents a slerp for a single - /// rotational transform - and maintains the state of that slerp (start orientation, target orientation, - /// start time, duration). - /// The users of this class are responsible for keeping track of Slerpee/transform correspondance, as well - /// as assigning the output of Slerpee to the corresponding transform. - /// It's a glorified calculator, essentially. - /// - class Slerpee { - // The default duration of a slerp, in seconds. - private const float DEFAULT_SLERP_DURATION_SECONDS = .05f; - // Minimum angle difference for targetOrientation to start a new slerp. - private const float MIN_SLERP_ANGLE_DEGREES = .1f; - private Quaternion baseOrientation; - private Quaternion targetOrientation; - private Quaternion currentSlerpedOrientation; +namespace com.google.apps.peltzer.client.tools.utils +{ + /// + /// A slerp (spherical linear interpolation). An instance of this class represents a slerp for a single + /// rotational transform - and maintains the state of that slerp (start orientation, target orientation, + /// start time, duration). + /// The users of this class are responsible for keeping track of Slerpee/transform correspondance, as well + /// as assigning the output of Slerpee to the corresponding transform. + /// It's a glorified calculator, essentially. + /// + class Slerpee + { + // The default duration of a slerp, in seconds. + private const float DEFAULT_SLERP_DURATION_SECONDS = .05f; + // Minimum angle difference for targetOrientation to start a new slerp. + private const float MIN_SLERP_ANGLE_DEGREES = .1f; + private Quaternion baseOrientation; + private Quaternion targetOrientation; + private Quaternion currentSlerpedOrientation; - private float slerpStartTime = 0f; + private float slerpStartTime = 0f; - // This is to aid debugging and logging, and should not be used for any other purpose. - private int meshTargetId; + // This is to aid debugging and logging, and should not be used for any other purpose. + private int meshTargetId; - private bool isSlerping = false; + private bool isSlerping = false; - /// - /// Do initial setup for the slerp, passing in the current base orientation to slerp from. - /// Passing an id does not change behavior in any way, but is helpful for debugging/logging so should be - /// done whenever possible. - /// - public Slerpee(Quaternion baseOrientation, int id = -1) { - this.baseOrientation = baseOrientation; - this.currentSlerpedOrientation = baseOrientation; - this.targetOrientation = baseOrientation; - this.meshTargetId = id; - } + /// + /// Do initial setup for the slerp, passing in the current base orientation to slerp from. + /// Passing an id does not change behavior in any way, but is helpful for debugging/logging so should be + /// done whenever possible. + /// + public Slerpee(Quaternion baseOrientation, int id = -1) + { + this.baseOrientation = baseOrientation; + this.currentSlerpedOrientation = baseOrientation; + this.targetOrientation = baseOrientation; + this.meshTargetId = id; + } - /// - /// Starts a slerp from the current orientation to the targetOrientation. - /// Setting instant to true will apply the orientation instantly. - /// If a slerp is already in progress and startSlerp is called with a different targetOrientation, - /// that slerp will be cancelled, and a new one will be started using the currentSlerpedOrientation as the - /// new baseOrientation. - /// - private Quaternion StartOrUpdateSlerpInternal(Quaternion targetOrientation, - bool instant = false) { - if (instant) { - currentSlerpedOrientation = targetOrientation; - this.targetOrientation = targetOrientation; - this.baseOrientation = targetOrientation; - isSlerping = false; - return currentSlerpedOrientation; - } + /// + /// Starts a slerp from the current orientation to the targetOrientation. + /// Setting instant to true will apply the orientation instantly. + /// If a slerp is already in progress and startSlerp is called with a different targetOrientation, + /// that slerp will be cancelled, and a new one will be started using the currentSlerpedOrientation as the + /// new baseOrientation. + /// + private Quaternion StartOrUpdateSlerpInternal(Quaternion targetOrientation, + bool instant = false) + { + if (instant) + { + currentSlerpedOrientation = targetOrientation; + this.targetOrientation = targetOrientation; + this.baseOrientation = targetOrientation; + isSlerping = false; + return currentSlerpedOrientation; + } - // If the new target is essentially the same as our current target, don't start a new slerp and just update. - if (Quaternion.Angle(this.targetOrientation, targetOrientation) <= MIN_SLERP_ANGLE_DEGREES) { - // Early return if a slerp isn't active. - if (!isSlerping) { - return currentSlerpedOrientation; - } - return UpdateAndGetCurrentOrientation(); - } + // If the new target is essentially the same as our current target, don't start a new slerp and just update. + if (Quaternion.Angle(this.targetOrientation, targetOrientation) <= MIN_SLERP_ANGLE_DEGREES) + { + // Early return if a slerp isn't active. + if (!isSlerping) + { + return currentSlerpedOrientation; + } + return UpdateAndGetCurrentOrientation(); + } - // Reset slerp parameters for a new slerp. - slerpStartTime = Time.time; - this.targetOrientation = targetOrientation; - // If a slerp is in progress, use the in progress orientation as the base for the new slerp. Otherwise, - // us the final orientation of the last completed slerp. Either way, currentSlerpedOrientation holds this value. - baseOrientation = currentSlerpedOrientation; - isSlerping = true; + // Reset slerp parameters for a new slerp. + slerpStartTime = Time.time; + this.targetOrientation = targetOrientation; + // If a slerp is in progress, use the in progress orientation as the base for the new slerp. Otherwise, + // us the final orientation of the last completed slerp. Either way, currentSlerpedOrientation holds this value. + baseOrientation = currentSlerpedOrientation; + isSlerping = true; - return currentSlerpedOrientation; - } + return currentSlerpedOrientation; + } - /// - /// Starts a slerp from the current orientation to the targetOrientation. - /// If a slerp is already in progress and startSlerp is called with a different targetOrientation, - /// that slerp will be cancelled, and a new one will be started using the currentSlerpedOrientation as the - /// new baseOrientation. - /// - public Quaternion StartOrUpdateSlerp(Quaternion targetOrientation) { - return StartOrUpdateSlerpInternal(targetOrientation); - } + /// + /// Starts a slerp from the current orientation to the targetOrientation. + /// If a slerp is already in progress and startSlerp is called with a different targetOrientation, + /// that slerp will be cancelled, and a new one will be started using the currentSlerpedOrientation as the + /// new baseOrientation. + /// + public Quaternion StartOrUpdateSlerp(Quaternion targetOrientation) + { + return StartOrUpdateSlerpInternal(targetOrientation); + } - /// - /// Updates the slerp to the supplied orientation instantly without interpolation. - /// - /// The orientation to update to. - /// The targetOrientation that was input. - public Quaternion UpdateOrientationInstantly(Quaternion targetOrientation) { - return StartOrUpdateSlerpInternal(targetOrientation, true); - } + /// + /// Updates the slerp to the supplied orientation instantly without interpolation. + /// + /// The orientation to update to. + /// The targetOrientation that was input. + public Quaternion UpdateOrientationInstantly(Quaternion targetOrientation) + { + return StartOrUpdateSlerpInternal(targetOrientation, true); + } - /// - /// Updates the current state of the slerp, and returns the new interpolated orientation. - /// - public Quaternion UpdateAndGetCurrentOrientation() { - // Calculate what percentage of the duration has elapsed. - float elapsedTime = Time.time - slerpStartTime; - if (elapsedTime > DEFAULT_SLERP_DURATION_SECONDS) { - isSlerping = false; - // Unity doesn't provide an inbuilt Quaternion normalization function, and slerping normalizes. - currentSlerpedOrientation = Quaternion.Slerp(baseOrientation, targetOrientation, 1.0f); - return currentSlerpedOrientation; - } + /// + /// Updates the current state of the slerp, and returns the new interpolated orientation. + /// + public Quaternion UpdateAndGetCurrentOrientation() + { + // Calculate what percentage of the duration has elapsed. + float elapsedTime = Time.time - slerpStartTime; + if (elapsedTime > DEFAULT_SLERP_DURATION_SECONDS) + { + isSlerping = false; + // Unity doesn't provide an inbuilt Quaternion normalization function, and slerping normalizes. + currentSlerpedOrientation = Quaternion.Slerp(baseOrientation, targetOrientation, 1.0f); + return currentSlerpedOrientation; + } - float pctDone = elapsedTime / DEFAULT_SLERP_DURATION_SECONDS; - currentSlerpedOrientation = Quaternion.Slerp(baseOrientation, targetOrientation, pctDone); - return currentSlerpedOrientation; - } + float pctDone = elapsedTime / DEFAULT_SLERP_DURATION_SECONDS; + currentSlerpedOrientation = Quaternion.Slerp(baseOrientation, targetOrientation, pctDone); + return currentSlerpedOrientation; + } - public override String ToString() { - StringBuilder builder = new StringBuilder(); - builder.Append("Current Slerp: inSlerp " + isSlerping + "\n"); - builder.Append(" " + "meshId: " + meshTargetId + "\n"); - builder.Append(" " + "baseOrientation: " + baseOrientation + "\n"); - builder.Append(" " + "targetOrientation: " + targetOrientation + "\n"); - builder.Append(" " + "currentSlerpedOrientation: " + currentSlerpedOrientation + "\n"); - builder.Append(" " + "slerpStartTime: " + slerpStartTime + "\n"); - float elapsedTime = Time.time - slerpStartTime; - builder.Append(" " + "elapsedTime: " + elapsedTime + "\n"); - float pctDone = Math.Max(0f, Math.Min(1f, elapsedTime / DEFAULT_SLERP_DURATION_SECONDS)); - builder.Append(" " + "pctDone: " + pctDone.ToString("0.00%") + "\n"); - return builder.ToString(); + public override String ToString() + { + StringBuilder builder = new StringBuilder(); + builder.Append("Current Slerp: inSlerp " + isSlerping + "\n"); + builder.Append(" " + "meshId: " + meshTargetId + "\n"); + builder.Append(" " + "baseOrientation: " + baseOrientation + "\n"); + builder.Append(" " + "targetOrientation: " + targetOrientation + "\n"); + builder.Append(" " + "currentSlerpedOrientation: " + currentSlerpedOrientation + "\n"); + builder.Append(" " + "slerpStartTime: " + slerpStartTime + "\n"); + float elapsedTime = Time.time - slerpStartTime; + builder.Append(" " + "elapsedTime: " + elapsedTime + "\n"); + float pctDone = Math.Max(0f, Math.Min(1f, elapsedTime / DEFAULT_SLERP_DURATION_SECONDS)); + builder.Append(" " + "pctDone: " + pctDone.ToString("0.00%") + "\n"); + return builder.ToString(); + } } - } } diff --git a/Assets/Scripts/model/util/TopologyUtil.cs b/Assets/Scripts/model/util/TopologyUtil.cs index a0fde402..8a531dab 100644 --- a/Assets/Scripts/model/util/TopologyUtil.cs +++ b/Assets/Scripts/model/util/TopologyUtil.cs @@ -19,66 +19,85 @@ using com.google.apps.peltzer.client.model.core; -namespace com.google.apps.peltzer.client.model.util { - public class TopologyUtil { +namespace com.google.apps.peltzer.client.model.util +{ + public class TopologyUtil + { - /// - /// Checks that a given mesh has a valid topology. Basically that it is a closed mesh without "manifold" edges. - /// - public static bool HasValidTopology(MMesh mesh, bool logInfo = false) { - bool valid = true; + /// + /// Checks that a given mesh has a valid topology. Basically that it is a closed mesh without "manifold" edges. + /// + public static bool HasValidTopology(MMesh mesh, bool logInfo = false) + { + bool valid = true; - //------ Check that each vertex is used at least twice. - MultiDict facesUsingVertex = new MultiDict(); + //------ Check that each vertex is used at least twice. + MultiDict facesUsingVertex = new MultiDict(); - foreach(Face face in mesh.GetFaces()) { - foreach(int vertexId in face.vertexIds) { - facesUsingVertex.Add(vertexId, face.id); - } - } + foreach (Face face in mesh.GetFaces()) + { + foreach (int vertexId in face.vertexIds) + { + facesUsingVertex.Add(vertexId, face.id); + } + } - foreach(int vertexId in mesh.GetVertexIds()) { - List faces; - if (facesUsingVertex.TryGetValues(vertexId, out faces)) { - if (faces.Count < 2) { - valid = false; - Console.WriteLine("Vertex " + vertexId + " is only used by " + faces.Count + " faces."); - } - } else { - valid = false; - Console.WriteLine("Vertex " + vertexId + " is unused."); - } - } + foreach (int vertexId in mesh.GetVertexIds()) + { + List faces; + if (facesUsingVertex.TryGetValues(vertexId, out faces)) + { + if (faces.Count < 2) + { + valid = false; + Console.WriteLine("Vertex " + vertexId + " is only used by " + faces.Count + " faces."); + } + } + else + { + valid = false; + Console.WriteLine("Vertex " + vertexId + " is unused."); + } + } - //------ Check that each edges is used exactly twice, once in each direction. - Dictionary edges = new Dictionary(); + //------ Check that each edges is used exactly twice, once in each direction. + Dictionary edges = new Dictionary(); - // Load edges, look for dupes along the way. - foreach (Face face in mesh.GetFaces()) { - for(int i = 0; i < face.vertexIds.Count; i++) { - Edge edge = new Edge(face.vertexIds[i], face.vertexIds[(i + 1) % face.vertexIds.Count]); - if (edges.ContainsKey(edge)) { - valid = false; - if (logInfo) { - Console.WriteLine("Non-manifold edge " + edge + " between faces " + face.id + " and " + edges[edge]); + // Load edges, look for dupes along the way. + foreach (Face face in mesh.GetFaces()) + { + for (int i = 0; i < face.vertexIds.Count; i++) + { + Edge edge = new Edge(face.vertexIds[i], face.vertexIds[(i + 1) % face.vertexIds.Count]); + if (edges.ContainsKey(edge)) + { + valid = false; + if (logInfo) + { + Console.WriteLine("Non-manifold edge " + edge + " between faces " + face.id + " and " + edges[edge]); + } + } + else + { + edges[edge] = face.id; + } + } } - } else { - edges[edge] = face.id; - } - } - } - // Now ensure that each edge also has it's mirror in the set. - foreach(Edge edge in edges.Keys) { - if (!edges.ContainsKey(edge.Reverse())) { - valid = false; - if (logInfo) { - Console.WriteLine("Edge " + edge + " in face " + edges[edge] + " is not joined to another face."); - } - } - } + // Now ensure that each edge also has it's mirror in the set. + foreach (Edge edge in edges.Keys) + { + if (!edges.ContainsKey(edge.Reverse())) + { + valid = false; + if (logInfo) + { + Console.WriteLine("Edge " + edge + " in face " + edges[edge] + " is not joined to another face."); + } + } + } - return valid; + return valid; + } } - } } diff --git a/Assets/Scripts/model/util/WebRequestManager.cs b/Assets/Scripts/model/util/WebRequestManager.cs index 18b4bc0f..3a8464fe 100644 --- a/Assets/Scripts/model/util/WebRequestManager.cs +++ b/Assets/Scripts/model/util/WebRequestManager.cs @@ -19,436 +19,471 @@ using System.Collections; using System.IO; -namespace com.google.apps.peltzer.client.model.util { - /// - /// Manages web requests, limiting how many can happen simultaneously at any given time and re-using - /// buffers as much as possible to avoid reallocation and garbage collection. - /// - /// Not *all* web requests must be routed through this class. Small, infrequent web requests can be made directly - /// via UnityWebRequest without using this class. However, larger or frequent requests should use this, since this - /// will avoid the expensive allocation of numerous download buffers (a typical UnityWebRequest allocates many - /// small buffers for temporary transfer and a larger buffer to contain the download, and they all become garbage - /// that the GC has to clean up). - /// - [ExecuteInEditMode] - public class WebRequestManager : MonoBehaviour { +namespace com.google.apps.peltzer.client.model.util +{ /// - /// Constant passed to EnqueueRequest to mean that the request should not be retrieved from cache. + /// Manages web requests, limiting how many can happen simultaneously at any given time and re-using + /// buffers as much as possible to avoid reallocation and garbage collection. + /// + /// Not *all* web requests must be routed through this class. Small, infrequent web requests can be made directly + /// via UnityWebRequest without using this class. However, larger or frequent requests should use this, since this + /// will avoid the expensive allocation of numerous download buffers (a typical UnityWebRequest allocates many + /// small buffers for temporary transfer and a larger buffer to contain the download, and they all become garbage + /// that the GC has to clean up). /// - public const long CACHE_NONE = 0; - - /// - /// Constant passed to EnqueueRequest to mean that a cached copy is acceptable regardless of its age. - /// - public const long CACHE_ANY_AGE = -1; - - /// - /// Maximum number of concurrent downloads to allow. This indicates how many download buffers we should keep. - /// - private const int MAX_CONCURRENT_DOWNLOADS = 8; - - /// - /// Initial size of the pre-allocated data buffer. - /// The data buffer is re-allocated when needed, but we want to avoid doing that because it's - /// expensive and will happen in the UI thread, so we need to start out with a reasonably large - /// size to handle the data we will download. - /// - private const int DATA_BUFFER_INIT_SIZE = 128 * 1024 * 1024; // 128MB - - /// - /// Size of the temporary buffer used to receive data. This is for a temporary buffer used - /// by Unity to transfer data to us. - /// - private const int TEMP_BUFFER_SIZE = 2 * 1024 * 1024; // 2MB - - /// - /// Delegate that creates a UnityWebRequest. This is used by client code to set up a UnityWebRequest - /// with the desired parameters. - /// - /// The UnityWebRequest. - public delegate UnityWebRequest CreationCallback(); - - /// - /// Delegate that processes the completion of a web request. We call this delegate to inform the client that - /// a web request has completed. - /// - /// The UnityWebRequest that was just completed. - public delegate void CompletionCallback(bool success, int responseCode, byte[] responseBytes); - - /// - /// Represents the desired configuration parameters for the WebRequestManager. - /// - public class WebRequestManagerConfig { - /// - /// The API key to use (mandatory). - /// - public string apiKey; - - /// - /// Whether or not to use caching for web requests (recommended). - /// - public bool cacheEnabled = true; - - /// - /// Maximum cache size, in megabytes. - /// - public int maxCacheSizeMb = 1024; - - /// - /// Maximum number of cache entries. - /// - public int maxCacheEntries = 4096; - - /// - /// If not null, this is the path that will be used to store the cache. - /// If null, the default path will be used. - /// - public string cachePathOverride = null; - - public WebRequestManagerConfig(string apiKey) { - this.apiKey = apiKey; - } - } - - /// - /// Represents a pending request that we have in the queue. - /// - private class PendingRequest { - /// - /// The creation callback. When this request's turn arrives, we will call this to create the UnityWebRequest. - /// - public CreationCallback creationCallback; - /// - /// Completion callback. We will call this when the web request completes. - /// - public CompletionCallback completionCallback; - /// - /// Maximum age of the cached copy, in milliseconds. - /// NO_CACHE means we will not use the cache. - /// ANY_AGE means any age is OK. - /// - public long maxAgeMillis; - - public PendingRequest(CreationCallback creationCallback, CompletionCallback completionCallback, - long maxAgeMillis) { - this.creationCallback = creationCallback; - this.completionCallback = completionCallback; - this.maxAgeMillis = maxAgeMillis; - } - } - - /// - /// Holds buffers for an active web request. Each concurrent active web request must own its own BufferHolder, - /// which is where it stores data. Web requests are implemented as coroutines, so this is the same as saying - /// that each of our active coroutines owns one BufferHolder. - /// - private class BufferHolder { - // Temporary buffer used by Unity to transfer data to us. - public byte[] tempBuffer = new byte[TEMP_BUFFER_SIZE]; - // Permanent buffer in which we accumulate data as we receive. - public byte[] dataBuffer = new byte[DATA_BUFFER_INIT_SIZE]; - } - - /// - /// Requests that are pending execution. This is a concurrent queue because requests may come in from any - /// thread. Requests are serviced on the main thread. - /// - private ConcurrentQueue pendingRequests = new ConcurrentQueue(); - - /// - /// List of BufferHolders that are idle (not being used by any download coroutine). - /// BufferHolders are returned to this list when coroutines finish. - /// - private List idleBuffers = new List(); - - /// - /// Cache for web responses. - /// - private PersistentBlobCache cache; - - public void Setup(WebRequestManagerConfig config) { - // Create all the buffer holders. They are all initially idle. - for (int i = 0; i < MAX_CONCURRENT_DOWNLOADS; i++) { - idleBuffers.Add(new BufferHolder()); - } - - if (config.cacheEnabled) { - string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - string defaultCachePath = Path.Combine(Path.Combine(Path.Combine( - appDataPath, Application.companyName), Application.productName), "WebRequestCache"); - - string cachePath = config.cachePathOverride ?? defaultCachePath; - // Note: Directory.CreateDirectory creates all directories in the path. - Directory.CreateDirectory(cachePath); - - cache = gameObject.AddComponent(); - cache.Setup(cachePath, config.maxCacheEntries, config.maxCacheSizeMb * 1024 * 1024); - } - } - - /// - /// Enqueues a request. Can be called from any thread. - /// - /// The callback that creates the UnityWebRequest. This callback will - /// be called when the request is ready to be serviced. - /// The callback to call when the request is complete. Will be called - /// when the request completes. - /// Indicates the cache strategy. If this is NO_CACHE, the cache will - /// not be used, if it's a positive value, it indicates what is the maximum age of the cached - /// copy that is considered acceptable. If it's ANY_AGE, any cached copy regardless of age - /// will be considered acceptable. - public void EnqueueRequest(CreationCallback creationCallback, CompletionCallback completionCallback, - long maxAgeMillis = CACHE_ANY_AGE) { - // Your call is very important to us. - // Please stay on the line and your request will be handled by the next available operator. - pendingRequests.Enqueue(new PendingRequest(creationCallback, completionCallback, maxAgeMillis)); - - // If we are running in the editor, we don't have an update loop, so we have to manually - // start pending requests here. - if (!Application.isPlaying) { - StartPendingRequests(); - } - } - - /// - /// Clears the local web cache. This is asynchronous (the cache will be cleared in the background). - /// - public void ClearCache() { - if (cache != null) { - cache.RequestClear(); - } - } - - private void Update() { - StartPendingRequests(); - } - - private void StartPendingRequests() { - // Start pending web requests if we have idle buffers. - PendingRequest pendingRequest; - while (idleBuffers.Count > 0 && pendingRequests.Dequeue(out pendingRequest)) { - // Service the request. - // Fetch an idle BufferHolder. We will own that BufferHolder for the duration of the coroutine. - BufferHolder bufferHolder = idleBuffers[idleBuffers.Count - 1]; - // Remove it from the idle list because it's now in use. It will be returned to the pool - // by HandleWebRequest, when it's done with it. - idleBuffers.RemoveAt(idleBuffers.Count - 1); - // Start the coroutine that will handle this web request. When the coroutine is done, - // it will return the buffer to the pool. - StartCoroutine(HandleWebRequest(pendingRequest, bufferHolder)); - } - } + [ExecuteInEditMode] + public class WebRequestManager : MonoBehaviour + { + /// + /// Constant passed to EnqueueRequest to mean that the request should not be retrieved from cache. + /// + public const long CACHE_NONE = 0; + + /// + /// Constant passed to EnqueueRequest to mean that a cached copy is acceptable regardless of its age. + /// + public const long CACHE_ANY_AGE = -1; + + /// + /// Maximum number of concurrent downloads to allow. This indicates how many download buffers we should keep. + /// + private const int MAX_CONCURRENT_DOWNLOADS = 8; + + /// + /// Initial size of the pre-allocated data buffer. + /// The data buffer is re-allocated when needed, but we want to avoid doing that because it's + /// expensive and will happen in the UI thread, so we need to start out with a reasonably large + /// size to handle the data we will download. + /// + private const int DATA_BUFFER_INIT_SIZE = 128 * 1024 * 1024; // 128MB + + /// + /// Size of the temporary buffer used to receive data. This is for a temporary buffer used + /// by Unity to transfer data to us. + /// + private const int TEMP_BUFFER_SIZE = 2 * 1024 * 1024; // 2MB + + /// + /// Delegate that creates a UnityWebRequest. This is used by client code to set up a UnityWebRequest + /// with the desired parameters. + /// + /// The UnityWebRequest. + public delegate UnityWebRequest CreationCallback(); + + /// + /// Delegate that processes the completion of a web request. We call this delegate to inform the client that + /// a web request has completed. + /// + /// The UnityWebRequest that was just completed. + public delegate void CompletionCallback(bool success, int responseCode, byte[] responseBytes); + + /// + /// Represents the desired configuration parameters for the WebRequestManager. + /// + public class WebRequestManagerConfig + { + /// + /// The API key to use (mandatory). + /// + public string apiKey; + + /// + /// Whether or not to use caching for web requests (recommended). + /// + public bool cacheEnabled = true; + + /// + /// Maximum cache size, in megabytes. + /// + public int maxCacheSizeMb = 1024; + + /// + /// Maximum number of cache entries. + /// + public int maxCacheEntries = 4096; + + /// + /// If not null, this is the path that will be used to store the cache. + /// If null, the default path will be used. + /// + public string cachePathOverride = null; + + public WebRequestManagerConfig(string apiKey) + { + this.apiKey = apiKey; + } + } - /// - /// Co-routine that services one PendingRequest. This method must be called with StartCoroutine. - /// - /// The request to service. - private IEnumerator HandleWebRequest(PendingRequest request, BufferHolder bufferHolder) { - // NOTE: This method runs on the main thread, but never blocks -- the blocking part of the work is - // done by yielding the UnityWebRequest, which releases the main thread for other tasks while we - // are waiting for the web request to complete (by the miracle of coroutines). - - // Let the caller create the UnityWebRequest, configuring it as they want. The caller can set the URL, - // method, headers, anything they want. The only thing they can't do is call Send(), as we're in charge - // of doing that. - UnityWebRequest webRequest = request.creationCallback(); - - bool cacheAllowed = cache != null && webRequest.method == "GET" && request.maxAgeMillis != CACHE_NONE; - - // Check the cache (if it's a GET request and cache is enabled). - if (cacheAllowed) { - bool cacheHit = false; - byte[] cacheData = null; - bool cacheReadDone = false; - cache.RequestRead(webRequest.url, request.maxAgeMillis, (bool success, byte[] data) => { - cacheHit = success; - cacheData = data; - cacheReadDone = true; - }); - while (!cacheReadDone) { - yield return null; + /// + /// Represents a pending request that we have in the queue. + /// + private class PendingRequest + { + /// + /// The creation callback. When this request's turn arrives, we will call this to create the UnityWebRequest. + /// + public CreationCallback creationCallback; + /// + /// Completion callback. We will call this when the web request completes. + /// + public CompletionCallback completionCallback; + /// + /// Maximum age of the cached copy, in milliseconds. + /// NO_CACHE means we will not use the cache. + /// ANY_AGE means any age is OK. + /// + public long maxAgeMillis; + + public PendingRequest(CreationCallback creationCallback, CompletionCallback completionCallback, + long maxAgeMillis) + { + this.creationCallback = creationCallback; + this.completionCallback = completionCallback; + this.maxAgeMillis = maxAgeMillis; + } } - if (cacheHit) { - request.completionCallback(/* success */ true, /* responseCode */ 200, cacheData); - // Return the buffer to the pool for reuse. - CleanUpAfterWebRequest(bufferHolder); + /// + /// Holds buffers for an active web request. Each concurrent active web request must own its own BufferHolder, + /// which is where it stores data. Web requests are implemented as coroutines, so this is the same as saying + /// that each of our active coroutines owns one BufferHolder. + /// + private class BufferHolder + { + // Temporary buffer used by Unity to transfer data to us. + public byte[] tempBuffer = new byte[TEMP_BUFFER_SIZE]; + // Permanent buffer in which we accumulate data as we receive. + public byte[] dataBuffer = new byte[DATA_BUFFER_INIT_SIZE]; + } - yield break; + /// + /// Requests that are pending execution. This is a concurrent queue because requests may come in from any + /// thread. Requests are serviced on the main thread. + /// + private ConcurrentQueue pendingRequests = new ConcurrentQueue(); + + /// + /// List of BufferHolders that are idle (not being used by any download coroutine). + /// BufferHolders are returned to this list when coroutines finish. + /// + private List idleBuffers = new List(); + + /// + /// Cache for web responses. + /// + private PersistentBlobCache cache; + + public void Setup(WebRequestManagerConfig config) + { + // Create all the buffer holders. They are all initially idle. + for (int i = 0; i < MAX_CONCURRENT_DOWNLOADS; i++) + { + idleBuffers.Add(new BufferHolder()); + } + + if (config.cacheEnabled) + { + string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + string defaultCachePath = Path.Combine(Path.Combine(Path.Combine( + appDataPath, Application.companyName), Application.productName), "WebRequestCache"); + + string cachePath = config.cachePathOverride ?? defaultCachePath; + // Note: Directory.CreateDirectory creates all directories in the path. + Directory.CreateDirectory(cachePath); + + cache = gameObject.AddComponent(); + cache.Setup(cachePath, config.maxCacheEntries, config.maxCacheSizeMb * 1024 * 1024); + } } - } - - // Use a download handler with our preallocated buffers for the request (this is what avoids the - // allocation of numerous tiny buffers). - // Note that we can't re-use the CustomDownloadHandler objects, as Unity doesn't like it when we do, - // DownloadHandlers must be fresh for every new request. But that's ok because the bulk of the garbage - // is the buffers, and we're not re-allocating those. - CustomDownloadHandler handler = new CustomDownloadHandler(bufferHolder); - webRequest.downloadHandler = handler; - - // We need to asset that we actually succeeded in setting the download handler, because this can fail - // if, for example, the creation callback mistakenly called Send(), or if we (because of an unthinkable - // programming error -- the horror!) are trying to use a disposed CustomDownloadHandler. - AssertOrThrow.True(webRequest.downloadHandler == handler, - "Couldn't set download handler. It's either disposed of, or the creation callback mistakenly called Send()."); - - // Start the web request. This will suspend this coroutine until the request is done. - yield return webRequest.Send(); - - // Request is finished. Call user-supplied callback. - request.completionCallback(!webRequest.isNetworkError, (int)webRequest.responseCode, webRequest.downloadHandler.data); - - // Cache the result, if applicable. - if (!webRequest.isNetworkError && cacheAllowed) { - byte[] data = webRequest.downloadHandler.data; - if (data != null && data.Length > 0) { - byte[] copy = new byte[data.Length]; - Buffer.BlockCopy(data, 0, copy, 0, data.Length); - cache.RequestWrite(webRequest.url, copy); + + /// + /// Enqueues a request. Can be called from any thread. + /// + /// The callback that creates the UnityWebRequest. This callback will + /// be called when the request is ready to be serviced. + /// The callback to call when the request is complete. Will be called + /// when the request completes. + /// Indicates the cache strategy. If this is NO_CACHE, the cache will + /// not be used, if it's a positive value, it indicates what is the maximum age of the cached + /// copy that is considered acceptable. If it's ANY_AGE, any cached copy regardless of age + /// will be considered acceptable. + public void EnqueueRequest(CreationCallback creationCallback, CompletionCallback completionCallback, + long maxAgeMillis = CACHE_ANY_AGE) + { + // Your call is very important to us. + // Please stay on the line and your request will be handled by the next available operator. + pendingRequests.Enqueue(new PendingRequest(creationCallback, completionCallback, maxAgeMillis)); + + // If we are running in the editor, we don't have an update loop, so we have to manually + // start pending requests here. + if (!Application.isPlaying) + { + StartPendingRequests(); + } } - } - // Clean up. - webRequest.Dispose(); - handler.CleanUpAndDispose(); - CleanUpAfterWebRequest(bufferHolder); - } + /// + /// Clears the local web cache. This is asynchronous (the cache will be cleared in the background). + /// + public void ClearCache() + { + if (cache != null) + { + cache.RequestClear(); + } + } - private void CleanUpAfterWebRequest(BufferHolder bufferHolder) { - // Return the buffer to the pool for reuse. - idleBuffers.Add(bufferHolder); + private void Update() + { + StartPendingRequests(); + } - // If we are running in the editor, we don't have an update loop, so we have to manually - // start pending requests here. - if (!Application.isPlaying) { - StartPendingRequests(); - } - } + private void StartPendingRequests() + { + // Start pending web requests if we have idle buffers. + PendingRequest pendingRequest; + while (idleBuffers.Count > 0 && pendingRequests.Dequeue(out pendingRequest)) + { + // Service the request. + // Fetch an idle BufferHolder. We will own that BufferHolder for the duration of the coroutine. + BufferHolder bufferHolder = idleBuffers[idleBuffers.Count - 1]; + // Remove it from the idle list because it's now in use. It will be returned to the pool + // by HandleWebRequest, when it's done with it. + idleBuffers.RemoveAt(idleBuffers.Count - 1); + // Start the coroutine that will handle this web request. When the coroutine is done, + // it will return the buffer to the pool. + StartCoroutine(HandleWebRequest(pendingRequest, bufferHolder)); + } + } - /// - /// Our custom download handler that uses pre-allocated buffers to reduce garbage collection. - /// As recommended in: - /// https://docs.unity3d.com/540/Documentation/Manual/UnityWebRequest.html - /// - private class CustomDownloadHandler : DownloadHandlerScript { - /// - /// Indicates if the download is complete. - /// - private bool downloadComplete; - - /// - /// Data buffer, where we store the data we received so far. Not all of this buffer is valid - /// data. It might be larger than the data contained in it. The dataLength variable indicates - /// what part of it is valid. - /// - private BufferHolder bufferHolder; - - /// - /// Indicates the length of valid data in the dataBuffer buffer. - /// - private int dataLength; - - /// - /// The data in the buffer, decoded as UTF8 text. - /// This is a lazy cache -- we fill it in when we convert the data to text and keep it around for further - /// requests. - /// IMPORTANT: we can't use the variable name "text" here because it hides the "text" property of the - /// base class (which in turn calls GetText()) and leads to subtle bugs. - /// - private string cachedText; - - /// - /// True if this instance was disposed of and can no longer be used. - /// - private bool isDisposed; - - /// - /// Create a new PreallocatedDownloadBuffer that will own and use the given BufferHolder. - /// - /// The BufferHolder to own and use. - public CustomDownloadHandler(BufferHolder bufferHolder) : base(bufferHolder.tempBuffer) { - this.bufferHolder = bufferHolder; - } - - /// - /// Returns a copy of the downloaded data. - /// Can only be called when the download is complete. - /// - /// The downloaded data. The caller is the owner of the buffer, as it's a copy of this - /// instance's internal state. - protected override byte[] GetData() { - // NOTE: we have to make a copy of the buffer because the caller assumes that they own the buffer and can - // do whatever they want with it. In particular, the caller can (and does, in our code) ship the buffer to - // a background thread and then calls Reset() on this object to use it to download something else. - AssertOrThrow.True(!isDisposed, "This object is disposed."); - AssertOrThrow.True(downloadComplete != null, "Data not ready to be read. Download needs to be completed first."); - byte[] result = new byte[dataLength]; - Buffer.BlockCopy(/* src */ bufferHolder.dataBuffer, /* srcOffset */ 0, - /* dst */ result, /* dstOffset */ 0, dataLength); - return result; - } - - /// - /// Returns the downloaded data interpreted as UTF-8 text. This can only be called after the download - /// is complete, otherwise an exception will be thrown. - /// - /// The downloaded data as UTF-8 text. - protected override string GetText() { - AssertOrThrow.True(!isDisposed, "This object is disposed."); - AssertOrThrow.True(downloadComplete != null, "Text not ready to be read. Download needs to be completed first."); - if (cachedText == null) { - // Note that we are careful about only using dataBuffer[0..dataLength-1], since the buffer might be - // larger than the content (particularly if we are reusing the buffer from a previous operation). - cachedText = System.Text.Encoding.UTF8.GetString(bufferHolder.dataBuffer, 0, dataLength); + /// + /// Co-routine that services one PendingRequest. This method must be called with StartCoroutine. + /// + /// The request to service. + private IEnumerator HandleWebRequest(PendingRequest request, BufferHolder bufferHolder) + { + // NOTE: This method runs on the main thread, but never blocks -- the blocking part of the work is + // done by yielding the UnityWebRequest, which releases the main thread for other tasks while we + // are waiting for the web request to complete (by the miracle of coroutines). + + // Let the caller create the UnityWebRequest, configuring it as they want. The caller can set the URL, + // method, headers, anything they want. The only thing they can't do is call Send(), as we're in charge + // of doing that. + UnityWebRequest webRequest = request.creationCallback(); + + bool cacheAllowed = cache != null && webRequest.method == "GET" && request.maxAgeMillis != CACHE_NONE; + + // Check the cache (if it's a GET request and cache is enabled). + if (cacheAllowed) + { + bool cacheHit = false; + byte[] cacheData = null; + bool cacheReadDone = false; + cache.RequestRead(webRequest.url, request.maxAgeMillis, (bool success, byte[] data) => + { + cacheHit = success; + cacheData = data; + cacheReadDone = true; + }); + while (!cacheReadDone) + { + yield return null; + } + if (cacheHit) + { + request.completionCallback(/* success */ true, /* responseCode */ 200, cacheData); + + // Return the buffer to the pool for reuse. + CleanUpAfterWebRequest(bufferHolder); + + yield break; + } + } + + // Use a download handler with our preallocated buffers for the request (this is what avoids the + // allocation of numerous tiny buffers). + // Note that we can't re-use the CustomDownloadHandler objects, as Unity doesn't like it when we do, + // DownloadHandlers must be fresh for every new request. But that's ok because the bulk of the garbage + // is the buffers, and we're not re-allocating those. + CustomDownloadHandler handler = new CustomDownloadHandler(bufferHolder); + webRequest.downloadHandler = handler; + + // We need to asset that we actually succeeded in setting the download handler, because this can fail + // if, for example, the creation callback mistakenly called Send(), or if we (because of an unthinkable + // programming error -- the horror!) are trying to use a disposed CustomDownloadHandler. + AssertOrThrow.True(webRequest.downloadHandler == handler, + "Couldn't set download handler. It's either disposed of, or the creation callback mistakenly called Send()."); + + // Start the web request. This will suspend this coroutine until the request is done. + yield return webRequest.Send(); + + // Request is finished. Call user-supplied callback. + request.completionCallback(!webRequest.isNetworkError, (int)webRequest.responseCode, webRequest.downloadHandler.data); + + // Cache the result, if applicable. + if (!webRequest.isNetworkError && cacheAllowed) + { + byte[] data = webRequest.downloadHandler.data; + if (data != null && data.Length > 0) + { + byte[] copy = new byte[data.Length]; + Buffer.BlockCopy(data, 0, copy, 0, data.Length); + cache.RequestWrite(webRequest.url, copy); + } + } + + // Clean up. + webRequest.Dispose(); + handler.CleanUpAndDispose(); + CleanUpAfterWebRequest(bufferHolder); } - return cachedText; - } - - /// - /// Called by Unity when a new chunk of data is received. - /// - /// The new data that was received. - /// The length of the new data received. - /// - protected override bool ReceiveData(byte[] newData, int newDataLength) { - AssertOrThrow.True(!isDisposed, "This object is disposed."); - int capacityNeeded = dataLength + newDataLength; - - // If our buffer capacity will be exceeded, reallocate to fit. - if (capacityNeeded > bufferHolder.dataBuffer.Length) { - // Reallocate buffer. Pre-allocate twice the needed capacity in order to cover future needs. - // This is a standard buffer resizing strategy which ensures that for N inserts, at most O(log N) - // buffer resizes will be done. - byte[] newBuffer = new byte[capacityNeeded * 2]; - Buffer.BlockCopy( - /* src */ bufferHolder.dataBuffer, /* srcOffset */ 0, - /* dst */ newBuffer, /* dstOffset */ 0, - /* count */ dataLength); - bufferHolder.dataBuffer = newBuffer; + + private void CleanUpAfterWebRequest(BufferHolder bufferHolder) + { + // Return the buffer to the pool for reuse. + idleBuffers.Add(bufferHolder); + + // If we are running in the editor, we don't have an update loop, so we have to manually + // start pending requests here. + if (!Application.isPlaying) + { + StartPendingRequests(); + } } - // Append the new data to our current data. - Buffer.BlockCopy( - /* src */ newData, /* srcOffset */ 0, - /* dst */ bufferHolder.dataBuffer, /* dstOffset */ dataLength, - /* count */ newDataLength); - dataLength += newDataLength; - return true; - } - - /// - /// Called by Unity when the download is complete. - /// - protected override void CompleteContent() { - AssertOrThrow.True(!isDisposed, "This object is disposed."); - downloadComplete = true; - } - - public void CleanUpAndDispose() { - Dispose(); - isDisposed = true; - bufferHolder = null; - } + /// + /// Our custom download handler that uses pre-allocated buffers to reduce garbage collection. + /// As recommended in: + /// https://docs.unity3d.com/540/Documentation/Manual/UnityWebRequest.html + /// + private class CustomDownloadHandler : DownloadHandlerScript + { + /// + /// Indicates if the download is complete. + /// + private bool downloadComplete; + + /// + /// Data buffer, where we store the data we received so far. Not all of this buffer is valid + /// data. It might be larger than the data contained in it. The dataLength variable indicates + /// what part of it is valid. + /// + private BufferHolder bufferHolder; + + /// + /// Indicates the length of valid data in the dataBuffer buffer. + /// + private int dataLength; + + /// + /// The data in the buffer, decoded as UTF8 text. + /// This is a lazy cache -- we fill it in when we convert the data to text and keep it around for further + /// requests. + /// IMPORTANT: we can't use the variable name "text" here because it hides the "text" property of the + /// base class (which in turn calls GetText()) and leads to subtle bugs. + /// + private string cachedText; + + /// + /// True if this instance was disposed of and can no longer be used. + /// + private bool isDisposed; + + /// + /// Create a new PreallocatedDownloadBuffer that will own and use the given BufferHolder. + /// + /// The BufferHolder to own and use. + public CustomDownloadHandler(BufferHolder bufferHolder) : base(bufferHolder.tempBuffer) + { + this.bufferHolder = bufferHolder; + } + + /// + /// Returns a copy of the downloaded data. + /// Can only be called when the download is complete. + /// + /// The downloaded data. The caller is the owner of the buffer, as it's a copy of this + /// instance's internal state. + protected override byte[] GetData() + { + // NOTE: we have to make a copy of the buffer because the caller assumes that they own the buffer and can + // do whatever they want with it. In particular, the caller can (and does, in our code) ship the buffer to + // a background thread and then calls Reset() on this object to use it to download something else. + AssertOrThrow.True(!isDisposed, "This object is disposed."); + AssertOrThrow.True(downloadComplete != null, "Data not ready to be read. Download needs to be completed first."); + byte[] result = new byte[dataLength]; + Buffer.BlockCopy(/* src */ bufferHolder.dataBuffer, /* srcOffset */ 0, + /* dst */ result, /* dstOffset */ 0, dataLength); + return result; + } + + /// + /// Returns the downloaded data interpreted as UTF-8 text. This can only be called after the download + /// is complete, otherwise an exception will be thrown. + /// + /// The downloaded data as UTF-8 text. + protected override string GetText() + { + AssertOrThrow.True(!isDisposed, "This object is disposed."); + AssertOrThrow.True(downloadComplete != null, "Text not ready to be read. Download needs to be completed first."); + if (cachedText == null) + { + // Note that we are careful about only using dataBuffer[0..dataLength-1], since the buffer might be + // larger than the content (particularly if we are reusing the buffer from a previous operation). + cachedText = System.Text.Encoding.UTF8.GetString(bufferHolder.dataBuffer, 0, dataLength); + } + return cachedText; + } + + /// + /// Called by Unity when a new chunk of data is received. + /// + /// The new data that was received. + /// The length of the new data received. + /// + protected override bool ReceiveData(byte[] newData, int newDataLength) + { + AssertOrThrow.True(!isDisposed, "This object is disposed."); + int capacityNeeded = dataLength + newDataLength; + + // If our buffer capacity will be exceeded, reallocate to fit. + if (capacityNeeded > bufferHolder.dataBuffer.Length) + { + // Reallocate buffer. Pre-allocate twice the needed capacity in order to cover future needs. + // This is a standard buffer resizing strategy which ensures that for N inserts, at most O(log N) + // buffer resizes will be done. + byte[] newBuffer = new byte[capacityNeeded * 2]; + Buffer.BlockCopy( + /* src */ bufferHolder.dataBuffer, /* srcOffset */ 0, + /* dst */ newBuffer, /* dstOffset */ 0, + /* count */ dataLength); + bufferHolder.dataBuffer = newBuffer; + } + + // Append the new data to our current data. + Buffer.BlockCopy( + /* src */ newData, /* srcOffset */ 0, + /* dst */ bufferHolder.dataBuffer, /* dstOffset */ dataLength, + /* count */ newDataLength); + dataLength += newDataLength; + return true; + } + + /// + /// Called by Unity when the download is complete. + /// + protected override void CompleteContent() + { + AssertOrThrow.True(!isDisposed, "This object is disposed."); + downloadComplete = true; + } + + public void CleanUpAndDispose() + { + Dispose(); + isDisposed = true; + bufferHolder = null; + } + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/model/util/WorldSpaceAdjuster.cs b/Assets/Scripts/model/util/WorldSpaceAdjuster.cs index 42be3af3..894696b2 100644 --- a/Assets/Scripts/model/util/WorldSpaceAdjuster.cs +++ b/Assets/Scripts/model/util/WorldSpaceAdjuster.cs @@ -15,30 +15,34 @@ using com.google.apps.peltzer.client.model.main; using UnityEngine; -namespace com.google.apps.peltzer.client.model.util { - /// - /// A utility class to adjust the world space to the current model, to help place it at a - /// comfortable and useful position relative to the user. - /// - public static class WorldSpaceAdjuster { +namespace com.google.apps.peltzer.client.model.util +{ /// - /// Adjusts the world space (for example, after opening a new model), setting the world to model space transform - /// such that the model appears at a convenient position. + /// A utility class to adjust the world space to the current model, to help place it at a + /// comfortable and useful position relative to the user. /// - public static void AdjustWorldSpace() { - // NOTE: for now, the adjustment we make is minimal: we just translate things so that no geometry spawns under - // the floor (where it would be invisible). In the future, we can tweak this method to also adjust the world - // transform to make the loaded model appear at a more natural position/orientation. - Bounds boundsModelSpace = PeltzerMain.Instance.model.FindBoundsOfAllMeshes(); + public static class WorldSpaceAdjuster + { + /// + /// Adjusts the world space (for example, after opening a new model), setting the world to model space transform + /// such that the model appears at a convenient position. + /// + public static void AdjustWorldSpace() + { + // NOTE: for now, the adjustment we make is minimal: we just translate things so that no geometry spawns under + // the floor (where it would be invisible). In the future, we can tweak this method to also adjust the world + // transform to make the loaded model appear at a more natural position/orientation. + Bounds boundsModelSpace = PeltzerMain.Instance.model.FindBoundsOfAllMeshes(); - // We want to prevent parts of the geometry from being under the floor (y = 0), so we want to set up the world - // transform such that minModelY maps to 0 or above in world space. - float minWorldY = PeltzerMain.Instance.worldSpace.ModelToWorld(boundsModelSpace.min).y; - if (minWorldY < 0) { - // Move the offset up by -minWorldY to cancel out the negative coordinate, making minModelY map - // to y=0 in world space. - PeltzerMain.Instance.worldSpace.offset += Vector3.up * -minWorldY; - } + // We want to prevent parts of the geometry from being under the floor (y = 0), so we want to set up the world + // transform such that minModelY maps to 0 or above in world space. + float minWorldY = PeltzerMain.Instance.worldSpace.ModelToWorld(boundsModelSpace.min).y; + if (minWorldY < 0) + { + // Move the offset up by -minWorldY to cancel out the negative coordinate, making minModelY map + // to y=0 in world space. + PeltzerMain.Instance.worldSpace.offset += Vector3.up * -minWorldY; + } + } } - } } diff --git a/Assets/Scripts/serialization/PolySerializationUtils.cs b/Assets/Scripts/serialization/PolySerializationUtils.cs index 9e969782..45a585e7 100644 --- a/Assets/Scripts/serialization/PolySerializationUtils.cs +++ b/Assets/Scripts/serialization/PolySerializationUtils.cs @@ -15,112 +15,130 @@ using System.Collections.Generic; using UnityEngine; -namespace com.google.apps.peltzer.client.serialization { - /// - /// Utility methods for serializing higher level structures (vectors, quaternions, lists, etc). - /// - public static class PolySerializationUtils { - public static void WriteVector3(PolySerializer serializer, Vector3 v) { - serializer.WriteFloat(v.x); - serializer.WriteFloat(v.y); - serializer.WriteFloat(v.z); - } +namespace com.google.apps.peltzer.client.serialization +{ + /// + /// Utility methods for serializing higher level structures (vectors, quaternions, lists, etc). + /// + public static class PolySerializationUtils + { + public static void WriteVector3(PolySerializer serializer, Vector3 v) + { + serializer.WriteFloat(v.x); + serializer.WriteFloat(v.y); + serializer.WriteFloat(v.z); + } - public static void WriteQuaternion(PolySerializer serializer, Quaternion q) { - serializer.WriteFloat(q.x); - serializer.WriteFloat(q.y); - serializer.WriteFloat(q.z); - serializer.WriteFloat(q.w); - } + public static void WriteQuaternion(PolySerializer serializer, Quaternion q) + { + serializer.WriteFloat(q.x); + serializer.WriteFloat(q.y); + serializer.WriteFloat(q.z); + serializer.WriteFloat(q.w); + } - public static void WriteIntList(PolySerializer serializer, IList list) { - serializer.WriteCount(list.Count); - for (int i = 0; i < list.Count; i++) { - serializer.WriteInt(list[i]); - } - } + public static void WriteIntList(PolySerializer serializer, IList list) + { + serializer.WriteCount(list.Count); + for (int i = 0; i < list.Count; i++) + { + serializer.WriteInt(list[i]); + } + } - public static void WriteVector3List(PolySerializer serializer, IList list) { - serializer.WriteCount(list.Count); - for (int i = 0; i < list.Count; i++) { - WriteVector3(serializer, list[i]); - } - } + public static void WriteVector3List(PolySerializer serializer, IList list) + { + serializer.WriteCount(list.Count); + for (int i = 0; i < list.Count; i++) + { + WriteVector3(serializer, list[i]); + } + } - public static void WriteStringSet(PolySerializer serializer, HashSet stringSet) { - serializer.WriteCount(stringSet.Count); - foreach (string s in stringSet) { - serializer.WriteString(s); - } - } + public static void WriteStringSet(PolySerializer serializer, HashSet stringSet) + { + serializer.WriteCount(stringSet.Count); + foreach (string s in stringSet) + { + serializer.WriteString(s); + } + } - public static Vector3 ReadVector3(PolySerializer serializer) { - float x = serializer.ReadFloat(); - float y = serializer.ReadFloat(); - float z = serializer.ReadFloat(); - return new Vector3(x, y, z); - } + public static Vector3 ReadVector3(PolySerializer serializer) + { + float x = serializer.ReadFloat(); + float y = serializer.ReadFloat(); + float z = serializer.ReadFloat(); + return new Vector3(x, y, z); + } - public static Quaternion ReadQuaternion(PolySerializer serializer) { - float x = serializer.ReadFloat(); - float y = serializer.ReadFloat(); - float z = serializer.ReadFloat(); - float w = serializer.ReadFloat(); - return new Quaternion(x, y, z, w); - } + public static Quaternion ReadQuaternion(PolySerializer serializer) + { + float x = serializer.ReadFloat(); + float y = serializer.ReadFloat(); + float z = serializer.ReadFloat(); + float w = serializer.ReadFloat(); + return new Quaternion(x, y, z, w); + } - /// - /// Reads a list of integers. - /// - /// The serializer to read from. - /// Minimum acceptable size of the list. - /// Maximum acceptable size of the list. - /// Name of the list (for debugging purposes, used in exceptions). - /// The list. - public static List ReadIntList(PolySerializer serializer, int min = 0, int max = int.MaxValue, - string listName = "untitled") { - int count = serializer.ReadCount(min, max, listName); - List result = new List(count); - for (int i = 0; i < count; i++) { - result.Add(serializer.ReadInt()); - } - return result; - } + /// + /// Reads a list of integers. + /// + /// The serializer to read from. + /// Minimum acceptable size of the list. + /// Maximum acceptable size of the list. + /// Name of the list (for debugging purposes, used in exceptions). + /// The list. + public static List ReadIntList(PolySerializer serializer, int min = 0, int max = int.MaxValue, + string listName = "untitled") + { + int count = serializer.ReadCount(min, max, listName); + List result = new List(count); + for (int i = 0; i < count; i++) + { + result.Add(serializer.ReadInt()); + } + return result; + } - /// - /// Reads a list of Vector3s. - /// - /// The serializer to read from. - /// Minimum acceptable size of the list. - /// Maximum acceptable size of the list. - /// Name of the list (for debugging purposes, used in exceptions). - /// The list. - public static List ReadVector3List(PolySerializer serializer, int min = 0, int max = int.MaxValue, - string listName = "untitled") { - int count = serializer.ReadCount(min, max, listName); - List result = new List(count); - for (int i = 0; i < count; i++) { - result.Add(ReadVector3(serializer)); - } - return result; - } + /// + /// Reads a list of Vector3s. + /// + /// The serializer to read from. + /// Minimum acceptable size of the list. + /// Maximum acceptable size of the list. + /// Name of the list (for debugging purposes, used in exceptions). + /// The list. + public static List ReadVector3List(PolySerializer serializer, int min = 0, int max = int.MaxValue, + string listName = "untitled") + { + int count = serializer.ReadCount(min, max, listName); + List result = new List(count); + for (int i = 0; i < count; i++) + { + result.Add(ReadVector3(serializer)); + } + return result; + } - /// - /// Reads a set of strings from the PolySerializer. - /// - /// The serializer to read from. - /// Minimum acceptable size of the set. - /// Maximum acceptable size of the set. - /// Name of the set (for debugging purposes, used in exceptions). - /// The set. - public static HashSet ReadStringSet(PolySerializer serializer, int min = 0, int max = int.MaxValue, - string listName = "untitled") { - int count = serializer.ReadCount(min, max, listName); - HashSet result = new HashSet(); - for (int i = 0; i < count; i++) { - result.Add(serializer.ReadString()); - } - return result; + /// + /// Reads a set of strings from the PolySerializer. + /// + /// The serializer to read from. + /// Minimum acceptable size of the set. + /// Maximum acceptable size of the set. + /// Name of the set (for debugging purposes, used in exceptions). + /// The set. + public static HashSet ReadStringSet(PolySerializer serializer, int min = 0, int max = int.MaxValue, + string listName = "untitled") + { + int count = serializer.ReadCount(min, max, listName); + HashSet result = new HashSet(); + for (int i = 0; i < count; i++) + { + result.Add(serializer.ReadString()); + } + return result; + } } - } } diff --git a/Assets/Scripts/serialization/PolySerializer.cs b/Assets/Scripts/serialization/PolySerializer.cs index 1bb8bc0a..2ecc55eb 100644 --- a/Assets/Scripts/serialization/PolySerializer.cs +++ b/Assets/Scripts/serialization/PolySerializer.cs @@ -15,778 +15,842 @@ using System; using System.Text; -namespace com.google.apps.peltzer.client.serialization { - /// - /// Handles serialization to and from the Poly file format. - /// - /// DESIGN GOALS: implementing serialization using objects or protos in C# can lead to a lot of garbage, - /// which was causing performance problems (bug) as it causes the GC to pause the main thread, - /// leading to frame loss. - /// - /// This class is designed to allow serialization and deserialization while minimizing allocation - /// and garbage generation. - /// - /// Also, this format is designed for backwards and forward compatibility: a newer version of the code should - /// be able to read an older file version and an older version of the code should be able to read a newer - /// file version. This is accomplished by grouping data into chunks as described below. - /// - /// CHUNKS - /// A chunk of data is simply an array of bytes. The file consists of several such arrays, each with - /// a label indicating what they are. This is used to ensure backwards/forward compatibility as described - /// later. - /// - /// HOW TO USE - /// To use this class, create an instance using the default constructor. Before using it, you must set it - /// up for reading or writing: - /// - /// serializer = new PolySerializer() - /// serializer.SetupForWriting(); - /// - /// Now you can write chunks: - /// - /// const int MY_CHUNK_LABEL = 1234; - /// serializer.StartWritingChunk(MY_CHUNK_LABEL); - /// serializer.WriteInt(10); - /// serializer.WriteFloat(1.235f); - /// //...etc... - /// serializer.FinishWritingChunk(MY_CHUNK_LABEL); - /// - /// When done, you can convert the result to a byte array: - /// - /// serializer.ToByteArray(); - /// - /// Or, to avoid an extra allocation, just get direct access to the buffer: - /// - /// byte[] buffer; - /// int offset, length; - /// serializer.GetUnderlyingDataBuffer(out buffer, out offset, out length); - /// //...do something with buffer[offset..offset+length-1]... - /// - /// To use for reading: - /// - /// byte[] inputBuffer = //...get the data from somewhere... - /// - /// serializer = new PolySerializer(); - /// serializer.SetupForReading(inputBuffer, 0, inputBuffer.Length); - /// - /// serializer.StartReadingChunk(MY_CHUNK_LABEL); - /// int myInt = serializer.ReadInt(); - /// float myFloat = serializer.ReadFloat(); - /// //..etc.. - /// serializer.FinishReadingChunk(MY_CHUNK_LABEL); - /// - /// DATA FORMAT - /// - /// Data is segmented into CHUNKS. Each chunk consists of a header and a body. The header is 12 bytes long - /// and consists of: - /// * Chunk start mark (4 bytes). The literal integer 0x1337 (defined in the CHUNK_START_MARK constant below). - /// * Chunk label (4 bytes). An arbitrary (user-defined) integer describing what the chunk is. - /// * Chunk size (4 bytes). The size of the chunk in bytes, INCLUDING the header. - /// - /// After the chunk header comes the chunk body. - /// The data in the chunk body is just raw bytes representing ints, floats, booleans, strings, etc. It's not - /// annotated or delimited, so the reader is supposed to know how to parse it. - /// Also, for simplicity, we do not allow nested chunks. - /// - /// |---- chunk header ----|---- chunk body ----|---- chunk header ----|---- chunk body .... - /// - /// +-------+-------+-------+--------------------------+-------+-------+-------+----------------------/ - /// | CSM | label | size | data | CSM | label | size | data \ ... - /// +-------+-------+-------+--------------------------+-------+-------+-------+----------------------/ - /// - /// (CSM = Chunk Start Mark. The literal integer 0x1337). - /// - /// NOTE ABOUT ENDIAN-NESS: Data is always written in little-endian format, regardless of the host architecture. - /// So the integer 0x11223344 is written as 0x44, 0x33, 0x22, 0x11. - /// - /// BACKWARD COMPATIBILITY (NEWER CODE READING OLDER FORMAT) - /// - /// When newer code is reading old data, it can always query to see what the next chunk is before reading it, - /// so it can detect whether or not newly defined chunks are present before attempting to read them. If they - /// are present, it can read them. If they are not present, it can skip them. So, for example: - /// - /// const int BORING_V1_STUFF_CHUNK = 9000; - /// const int ADVANCED_V2_STUFF_CHUNK = 9001; - /// const int EVEN_MORE_ADVANCED_V3_STUFF = 9002; - /// - /// serializer.StartReadingChunk(BORING_V1_STUFF_CHUNK); - /// // ...read basic data that's been there since v1... - /// serializer.FinishReadingChunk(BORING_V1_STUFF_CHUNK); - /// - /// if (serializer.GetNextChunkLabel() == ADVANCED_V2_STUFF_CHUNK) { - /// // Advanced V2 features are present, process them. - /// serializer.StartReadingChunk(ADVANCED_V2_STUFF_CHUNK); - /// // ...read data... - /// serializer.FinishReadingChunk(ADVANCED_V2_STUFF_CHUNK); - /// } - /// if (serializer.GetNextChunkLabel() == EVEN_MORE_ADVANCED_V3_STUFF) { - /// // Super advanced V3 features are present, process them. - /// serializer.StartReadingChunk(EVEN_MORE_ADVANCED_V3_STUFF); - /// // ...read data... - /// serializer.FinishReadingChunk(EVEN_MORE_ADVANCED_V3_STUFF); - /// } - /// - /// FORWARD COMPATIBILITY (OLDER CODE READING NEWER FORMAT) - /// When older code comes across a file written by a newer version, it can still attempt to read the parts of - /// it that it understands. This is simple, because when the code requests to read a chunk, the logic in this - /// class will actually skip over any unidentified chunks that are in the way, so older code will just read - /// the chunks that it can handle: - /// - /// const int BORING_V1_STUFF_CHUNK = 9000; - /// const int SOMETHING_ELSE = 10000; - /// - /// serializer.StartReadingChunk(BORING_V1_STUFF_CHUNK); - /// // ...read data... - /// serializer.FinishReadingChunk(BORING_V1_STUFF_CHUNK); - /// - /// // Go on to something else: - /// serializer.StartReadingChunk(SOMETHING_ELSE); - /// // ...read data... - /// - /// At the point where we call StartReadingChunk(SOMETHING_ELSE), we will actually look at the file and - /// skip over the two unidentified chunks 9001 and 9002 that contain the more advanced data, as if those - /// chunks didn't exist at all. - /// - /// VERSION CUT-OFF - /// This class does NOT implement a mechanism by which the client can tell that it's too out of date to - /// read a given file. This must be implemented by the user of this class (an idea is to make the first - /// chunk in the file contain a "minimum version" number). - /// - /// NOTE ABOUT AssertOrThrow: - /// AssertOrThrow IS NOT used in the critical parts of the code because when the second argument is a complicated - /// concatenation of strings, the concatenation will still have to be computed even if the condition is true, - /// which defeats the purpose of not generating tons of garbage. - /// - public class PolySerializer { - /// - /// Marker used to indicate the start of a chunk. - /// - private const int CHUNK_START_MARK = 0x1337; - - /// - /// Size of the chunk header. - /// - private const int CHUNK_HEADER_SIZE = 12; - - /// - /// Marker used to represent a null string. - /// - private const int NULL_STRING_MARKER = -9999; - - private const int DEFAULT_INITIAL_CAPACITY = 128 * 1024; - - /// - /// Marker that indicates something is a field that contains the count of something. - /// See ReadCount() for more info. - /// - private const int COUNT_FIELD_MARKER = 0xc0c0; - - /// - /// Mode of operation (reading or writing). - /// - private enum Mode { - // Mode not set (uninitialized). - UNSET, - // Open for reading (can read chunks from the buffer). - READING, - // Open for writing (can write chunks to the buffer). - WRITING, - // Finished writing. In this state writing has finished, and nothing else can be written. - // But the buffer can be queried for the results of the write operation. - FINISHED_WRITING, - } - - /// - /// Our current mode. - /// - private Mode mode = Mode.UNSET; - - /// - /// Indicates if we are in the middle of reading/writing a chunk. - /// - private bool isChunkInProgress; - - /// - /// Buffer that contains the data. May be oversized (the correct start and length of the data in the buffer - /// is given by the dataStartOffset and dataLength field). - /// - private byte[] dataBuffer; - - /// - /// The offset in the dataBuffer where the data starts. - /// Only the bytes in dataBuffer[dataStartOffset..dataStart+dataLength-1] are valid data. - /// - private int dataStartOffset; - - /// - /// Length of the data in the buffer. - /// Only the bytes in dataBuffer[dataStartOffset..dataStart+dataLength-1] are valid data. - /// - private int dataLength; - - /// - /// Current read/write offset in the data. - /// - private int curOffset; - - /// - /// Offset where the current chunk started. - /// - private int chunkStartOffset = -1; - - /// - /// Label of the current chunk being written or read. - /// - private int chunkLabel = -1; - - /// - /// Size of the current chunk we are reading. - /// Only valid when READING. We don't keep this up to date during the process of writing a - /// chunk. We compute the size only when we finish the writing the chunk. - /// - private int chunkSizeForReading; - - /// - /// Creates a new PolySerializer. Before using, it must be set up with one of the Setup*() methods. - /// - public PolySerializer() {} - - /// - /// Sets up the serializer for reading from the given byte array. - /// This object will use the buffer directly, so the caller must not modify it while this class - /// is using it. - /// - /// The buffer to use. The implementation uses the buffer directly, not a copy, so the - /// caller MUST NOT modify the buffer (or at least the part of the buffer between dataOffset and - /// dataOffset+dataLength-1) while this class is using it. - /// The offset in the buffer where the data starts. - /// The length of the data in the buffer. - public void SetupForReading(byte[] buffer, int startOffset, int length) { - Setup(Mode.READING, buffer, startOffset, length); - } - - /// - /// Sets up the serializer for writing. It will use an internally allocated byte array which will - /// get resized as needed. The caller can specify an initial capacity for it. - /// - /// Initial capacity of the buffer. The caller should supply - /// an approximate guess to how big the data will be. The more accurate the guess, the fewer - /// re-allocations of the buffer will be made, so less garbage will be produced. - public void SetupForWriting(int minInitialCapacity = DEFAULT_INITIAL_CAPACITY) { - // Reuse our buffer, if it's big enough and we were already in write mode before. - // This allows us to reduce allocation. - byte[] bufferToUse = (mode == Mode.WRITING && dataBuffer != null && dataBuffer.Length >= minInitialCapacity) ? - dataBuffer : new byte[minInitialCapacity]; - // Note: dataLength is 0 for writing because we start with empty data. - Setup(Mode.WRITING, bufferToUse, 0, 0); - } - - private void Setup(Mode mode, byte[] buffer, int startOffset, int length) { - if (startOffset + length > buffer.Length) { - Throw("Data start offset (" + startOffset + ") + data length (" + length + - ") can't be larger than buffer (" + buffer.Length + ")"); - } - this.mode = mode; - dataBuffer = buffer; - dataLength = length; - dataStartOffset = startOffset; - curOffset = startOffset; - isChunkInProgress = false; - chunkSizeForReading = -1; - chunkLabel = -1; - chunkStartOffset = -1; - } - - /// - /// Convenience method for checking that a buffer appears to have a valid header, before trying to process it. - /// This is useful as a quick check to see if a buffer is in the right file format before trying to - /// deserialize it (when multiple possible serialization formats are allowed). - /// - /// The buffer to check. - /// The offset in the buffer where the data starts. - /// The length of the data in the buffer. - /// True if the data has a valid header, false if not. - public static bool HasValidHeader(byte[] buffer, int offset, int length) { - return - // Must be long enough to contain a chunk header. - (length >= CHUNK_HEADER_SIZE) && (offset + CHUNK_HEADER_SIZE <= buffer.Length) && - // ..and the chunk start mark must be present at the beginning. - CHUNK_START_MARK == DecodeInt(buffer, offset); - } - - /// - /// Returns whether or not a chunk is currently open for reading/writing. This will be true between the - /// call to StartWritingChunk or StartReadingChunk, and the corresponding call to FinishWritingChunk - /// or FinishReadingChunk. - /// - public bool IsChunkInProgress { get { return isChunkInProgress; } } - - /// - /// Starts writing a chunk. - /// It is an error to call this if not in write mode, or if a chunk is already being written (there are - /// no nested chunks). - /// - /// The user-defined label of the chunk. - public void StartWritingChunk(int label) { - if (mode != Mode.WRITING) Throw("Can't write chunk. Not set up for writing."); - if (isChunkInProgress) Throw("Can't start writing chunk " + label + ", was already writing one."); - // Make sure we have enough space for the chunk header (12 bytes). - PrepareToWrite(CHUNK_HEADER_SIZE); - // Memorize that this is where the current chunk starts. - chunkStartOffset = curOffset; - chunkLabel = label; - // Leave space for the chunk header (which we will come back and write later, in FinishWritingChunk). - curOffset += CHUNK_HEADER_SIZE; - // Ready to start writing chunk data. - isChunkInProgress = true; - } - - /// - /// Finishes writing the current chunk. - /// - /// The label of the chunk to finish writing. This is for sanity-checking. - public void FinishWritingChunk(int label) { - // Sanity check that we are ending the same chunk that we started. - if (mode != Mode.WRITING) Throw("Can't finish writing chunk. Not set up for writing."); - if (!isChunkInProgress) Throw("Can't finish writing chunk. No write in progress."); - if (chunkLabel != label) { - Throw("Wrong label on FinishWritingChunk, expected " + chunkLabel + ", got " + label); - } - - // Write the chunk header (we left space for it in StartWritingChunk). - // The chunk size is INCLUSIVE of the header, so it's just the difference from the - // current offset to the offset where the chunk started. - EncodeChunkHeader(chunkStartOffset, chunkLabel, curOffset - chunkStartOffset); - - isChunkInProgress = false; - } - - /// - /// Writes an int to the current chunk. - /// Can only be called when currently writing a chunk. - /// - /// The value to write. - public void WriteInt(int value) { - if (mode != Mode.WRITING) Throw("Can't write. Not set up for writing."); - if (!isChunkInProgress) Throw("Can't write. Not currently in a chunk."); - PrepareToWrite(4); - EncodeInt(curOffset, value); - curOffset += 4; - } - - /// - /// Writes a byte to the current chunk. - /// Can only be called when currently writing a chunk. - /// - /// The value to write. - public void WriteByte(byte value) { - if (mode != Mode.WRITING) Throw("Can't write. Not set up for writing."); - if (!isChunkInProgress) Throw("Can't write. Not currently in a chunk."); - PrepareToWrite(1); - dataBuffer[curOffset] = value; - curOffset++; - } +namespace com.google.apps.peltzer.client.serialization +{ + /// + /// Handles serialization to and from the Poly file format. + /// + /// DESIGN GOALS: implementing serialization using objects or protos in C# can lead to a lot of garbage, + /// which was causing performance problems (bug) as it causes the GC to pause the main thread, + /// leading to frame loss. + /// + /// This class is designed to allow serialization and deserialization while minimizing allocation + /// and garbage generation. + /// + /// Also, this format is designed for backwards and forward compatibility: a newer version of the code should + /// be able to read an older file version and an older version of the code should be able to read a newer + /// file version. This is accomplished by grouping data into chunks as described below. + /// + /// CHUNKS + /// A chunk of data is simply an array of bytes. The file consists of several such arrays, each with + /// a label indicating what they are. This is used to ensure backwards/forward compatibility as described + /// later. + /// + /// HOW TO USE + /// To use this class, create an instance using the default constructor. Before using it, you must set it + /// up for reading or writing: + /// + /// serializer = new PolySerializer() + /// serializer.SetupForWriting(); + /// + /// Now you can write chunks: + /// + /// const int MY_CHUNK_LABEL = 1234; + /// serializer.StartWritingChunk(MY_CHUNK_LABEL); + /// serializer.WriteInt(10); + /// serializer.WriteFloat(1.235f); + /// //...etc... + /// serializer.FinishWritingChunk(MY_CHUNK_LABEL); + /// + /// When done, you can convert the result to a byte array: + /// + /// serializer.ToByteArray(); + /// + /// Or, to avoid an extra allocation, just get direct access to the buffer: + /// + /// byte[] buffer; + /// int offset, length; + /// serializer.GetUnderlyingDataBuffer(out buffer, out offset, out length); + /// //...do something with buffer[offset..offset+length-1]... + /// + /// To use for reading: + /// + /// byte[] inputBuffer = //...get the data from somewhere... + /// + /// serializer = new PolySerializer(); + /// serializer.SetupForReading(inputBuffer, 0, inputBuffer.Length); + /// + /// serializer.StartReadingChunk(MY_CHUNK_LABEL); + /// int myInt = serializer.ReadInt(); + /// float myFloat = serializer.ReadFloat(); + /// //..etc.. + /// serializer.FinishReadingChunk(MY_CHUNK_LABEL); + /// + /// DATA FORMAT + /// + /// Data is segmented into CHUNKS. Each chunk consists of a header and a body. The header is 12 bytes long + /// and consists of: + /// * Chunk start mark (4 bytes). The literal integer 0x1337 (defined in the CHUNK_START_MARK constant below). + /// * Chunk label (4 bytes). An arbitrary (user-defined) integer describing what the chunk is. + /// * Chunk size (4 bytes). The size of the chunk in bytes, INCLUDING the header. + /// + /// After the chunk header comes the chunk body. + /// The data in the chunk body is just raw bytes representing ints, floats, booleans, strings, etc. It's not + /// annotated or delimited, so the reader is supposed to know how to parse it. + /// Also, for simplicity, we do not allow nested chunks. + /// + /// |---- chunk header ----|---- chunk body ----|---- chunk header ----|---- chunk body .... + /// + /// +-------+-------+-------+--------------------------+-------+-------+-------+----------------------/ + /// | CSM | label | size | data | CSM | label | size | data \ ... + /// +-------+-------+-------+--------------------------+-------+-------+-------+----------------------/ + /// + /// (CSM = Chunk Start Mark. The literal integer 0x1337). + /// + /// NOTE ABOUT ENDIAN-NESS: Data is always written in little-endian format, regardless of the host architecture. + /// So the integer 0x11223344 is written as 0x44, 0x33, 0x22, 0x11. + /// + /// BACKWARD COMPATIBILITY (NEWER CODE READING OLDER FORMAT) + /// + /// When newer code is reading old data, it can always query to see what the next chunk is before reading it, + /// so it can detect whether or not newly defined chunks are present before attempting to read them. If they + /// are present, it can read them. If they are not present, it can skip them. So, for example: + /// + /// const int BORING_V1_STUFF_CHUNK = 9000; + /// const int ADVANCED_V2_STUFF_CHUNK = 9001; + /// const int EVEN_MORE_ADVANCED_V3_STUFF = 9002; + /// + /// serializer.StartReadingChunk(BORING_V1_STUFF_CHUNK); + /// // ...read basic data that's been there since v1... + /// serializer.FinishReadingChunk(BORING_V1_STUFF_CHUNK); + /// + /// if (serializer.GetNextChunkLabel() == ADVANCED_V2_STUFF_CHUNK) { + /// // Advanced V2 features are present, process them. + /// serializer.StartReadingChunk(ADVANCED_V2_STUFF_CHUNK); + /// // ...read data... + /// serializer.FinishReadingChunk(ADVANCED_V2_STUFF_CHUNK); + /// } + /// if (serializer.GetNextChunkLabel() == EVEN_MORE_ADVANCED_V3_STUFF) { + /// // Super advanced V3 features are present, process them. + /// serializer.StartReadingChunk(EVEN_MORE_ADVANCED_V3_STUFF); + /// // ...read data... + /// serializer.FinishReadingChunk(EVEN_MORE_ADVANCED_V3_STUFF); + /// } + /// + /// FORWARD COMPATIBILITY (OLDER CODE READING NEWER FORMAT) + /// When older code comes across a file written by a newer version, it can still attempt to read the parts of + /// it that it understands. This is simple, because when the code requests to read a chunk, the logic in this + /// class will actually skip over any unidentified chunks that are in the way, so older code will just read + /// the chunks that it can handle: + /// + /// const int BORING_V1_STUFF_CHUNK = 9000; + /// const int SOMETHING_ELSE = 10000; + /// + /// serializer.StartReadingChunk(BORING_V1_STUFF_CHUNK); + /// // ...read data... + /// serializer.FinishReadingChunk(BORING_V1_STUFF_CHUNK); + /// + /// // Go on to something else: + /// serializer.StartReadingChunk(SOMETHING_ELSE); + /// // ...read data... + /// + /// At the point where we call StartReadingChunk(SOMETHING_ELSE), we will actually look at the file and + /// skip over the two unidentified chunks 9001 and 9002 that contain the more advanced data, as if those + /// chunks didn't exist at all. + /// + /// VERSION CUT-OFF + /// This class does NOT implement a mechanism by which the client can tell that it's too out of date to + /// read a given file. This must be implemented by the user of this class (an idea is to make the first + /// chunk in the file contain a "minimum version" number). + /// + /// NOTE ABOUT AssertOrThrow: + /// AssertOrThrow IS NOT used in the critical parts of the code because when the second argument is a complicated + /// concatenation of strings, the concatenation will still have to be computed even if the condition is true, + /// which defeats the purpose of not generating tons of garbage. + /// + public class PolySerializer + { + /// + /// Marker used to indicate the start of a chunk. + /// + private const int CHUNK_START_MARK = 0x1337; + + /// + /// Size of the chunk header. + /// + private const int CHUNK_HEADER_SIZE = 12; + + /// + /// Marker used to represent a null string. + /// + private const int NULL_STRING_MARKER = -9999; + + private const int DEFAULT_INITIAL_CAPACITY = 128 * 1024; + + /// + /// Marker that indicates something is a field that contains the count of something. + /// See ReadCount() for more info. + /// + private const int COUNT_FIELD_MARKER = 0xc0c0; + + /// + /// Mode of operation (reading or writing). + /// + private enum Mode + { + // Mode not set (uninitialized). + UNSET, + // Open for reading (can read chunks from the buffer). + READING, + // Open for writing (can write chunks to the buffer). + WRITING, + // Finished writing. In this state writing has finished, and nothing else can be written. + // But the buffer can be queried for the results of the write operation. + FINISHED_WRITING, + } - /// - /// Writes a boolean to the current chunk. - /// Can only be called when currently writing a chunk. - /// - /// The value to write. - public void WriteBool(bool value) { - WriteByte(value ? (byte)1 : (byte)0); - } + /// + /// Our current mode. + /// + private Mode mode = Mode.UNSET; + + /// + /// Indicates if we are in the middle of reading/writing a chunk. + /// + private bool isChunkInProgress; + + /// + /// Buffer that contains the data. May be oversized (the correct start and length of the data in the buffer + /// is given by the dataStartOffset and dataLength field). + /// + private byte[] dataBuffer; + + /// + /// The offset in the dataBuffer where the data starts. + /// Only the bytes in dataBuffer[dataStartOffset..dataStart+dataLength-1] are valid data. + /// + private int dataStartOffset; + + /// + /// Length of the data in the buffer. + /// Only the bytes in dataBuffer[dataStartOffset..dataStart+dataLength-1] are valid data. + /// + private int dataLength; + + /// + /// Current read/write offset in the data. + /// + private int curOffset; + + /// + /// Offset where the current chunk started. + /// + private int chunkStartOffset = -1; + + /// + /// Label of the current chunk being written or read. + /// + private int chunkLabel = -1; + + /// + /// Size of the current chunk we are reading. + /// Only valid when READING. We don't keep this up to date during the process of writing a + /// chunk. We compute the size only when we finish the writing the chunk. + /// + private int chunkSizeForReading; + + /// + /// Creates a new PolySerializer. Before using, it must be set up with one of the Setup*() methods. + /// + public PolySerializer() { } + + /// + /// Sets up the serializer for reading from the given byte array. + /// This object will use the buffer directly, so the caller must not modify it while this class + /// is using it. + /// + /// The buffer to use. The implementation uses the buffer directly, not a copy, so the + /// caller MUST NOT modify the buffer (or at least the part of the buffer between dataOffset and + /// dataOffset+dataLength-1) while this class is using it. + /// The offset in the buffer where the data starts. + /// The length of the data in the buffer. + public void SetupForReading(byte[] buffer, int startOffset, int length) + { + Setup(Mode.READING, buffer, startOffset, length); + } - /// - /// Writes a floating point value to the current chunk. - /// Can only be called when currently writing a chunk. - /// - /// The value to write. - public void WriteFloat(float value) { - if (mode != Mode.WRITING) Throw("Can't write. Not set up for writing."); - if (!isChunkInProgress) Throw("Can't write. Not currently in a chunk."); - PrepareToWrite(4); - EncodeFloat(curOffset, value); - curOffset += 4; - } + /// + /// Sets up the serializer for writing. It will use an internally allocated byte array which will + /// get resized as needed. The caller can specify an initial capacity for it. + /// + /// Initial capacity of the buffer. The caller should supply + /// an approximate guess to how big the data will be. The more accurate the guess, the fewer + /// re-allocations of the buffer will be made, so less garbage will be produced. + public void SetupForWriting(int minInitialCapacity = DEFAULT_INITIAL_CAPACITY) + { + // Reuse our buffer, if it's big enough and we were already in write mode before. + // This allows us to reduce allocation. + byte[] bufferToUse = (mode == Mode.WRITING && dataBuffer != null && dataBuffer.Length >= minInitialCapacity) ? + dataBuffer : new byte[minInitialCapacity]; + // Note: dataLength is 0 for writing because we start with empty data. + Setup(Mode.WRITING, bufferToUse, 0, 0); + } - /// - /// Writes a string to the current chunk. - /// Can only be called when currently writing a chunk. - /// - /// The value to write. A null strings are handled correctly and will be read back - /// as a null string. - public void WriteString(string value) { - if (mode != Mode.WRITING) Throw("Can't write. Not set up for writing."); - if (!isChunkInProgress) Throw("Can't write. Not currently in a chunk."); - - // Special case for writing NULL: write a null marker instead of the length. - if (value == null) { - WriteInt(NULL_STRING_MARKER); - return; - } - - int byteCount = Encoding.UTF8.GetByteCount(value); - // We will write 4 bytes for the buffer length and then the bytes. - WriteInt(byteCount); - PrepareToWrite(byteCount); - int actualByteCount = Encoding.UTF8.GetBytes(value, 0, value.Length, dataBuffer, curOffset); - // The actual bytes written should be the same as our pre-calculated amount. This is guaranteed - // to be true by the UTF8 contract, but just as a sanity check, let's verify: - if (byteCount != actualByteCount) { - Throw("UTF8 encoding error, expected " + byteCount + " bytes, got " + actualByteCount + " bytes"); - } - - curOffset += byteCount; - } + private void Setup(Mode mode, byte[] buffer, int startOffset, int length) + { + if (startOffset + length > buffer.Length) + { + Throw("Data start offset (" + startOffset + ") + data length (" + length + + ") can't be larger than buffer (" + buffer.Length + ")"); + } + this.mode = mode; + dataBuffer = buffer; + dataLength = length; + dataStartOffset = startOffset; + curOffset = startOffset; + isChunkInProgress = false; + chunkSizeForReading = -1; + chunkLabel = -1; + chunkStartOffset = -1; + } - /// - /// Writes a count. A count is just an int with a special marker indicating that it's a count. - /// - /// - public void WriteCount(int count) { - WriteInt(COUNT_FIELD_MARKER); - WriteInt(count); - } + /// + /// Convenience method for checking that a buffer appears to have a valid header, before trying to process it. + /// This is useful as a quick check to see if a buffer is in the right file format before trying to + /// deserialize it (when multiple possible serialization formats are allowed). + /// + /// The buffer to check. + /// The offset in the buffer where the data starts. + /// The length of the data in the buffer. + /// True if the data has a valid header, false if not. + public static bool HasValidHeader(byte[] buffer, int offset, int length) + { + return + // Must be long enough to contain a chunk header. + (length >= CHUNK_HEADER_SIZE) && (offset + CHUNK_HEADER_SIZE <= buffer.Length) && + // ..and the chunk start mark must be present at the beginning. + CHUNK_START_MARK == DecodeInt(buffer, offset); + } - /// - /// Indicates that writing has finished. After calling this, you can query for the buffer with - /// ToByteArray() or GetUnderlyingDataBuffer(). This can only be called in write mode, and when - /// a chunk is not currently in progress. - /// - public void FinishWriting() { - if (mode != Mode.WRITING) Throw("Can't finish writing. Not currently writing."); - if (isChunkInProgress) Throw("Can't finish writing. A chunk is still in progress."); - mode = Mode.FINISHED_WRITING; - } + /// + /// Returns whether or not a chunk is currently open for reading/writing. This will be true between the + /// call to StartWritingChunk or StartReadingChunk, and the corresponding call to FinishWritingChunk + /// or FinishReadingChunk. + /// + public bool IsChunkInProgress { get { return isChunkInProgress; } } + + /// + /// Starts writing a chunk. + /// It is an error to call this if not in write mode, or if a chunk is already being written (there are + /// no nested chunks). + /// + /// The user-defined label of the chunk. + public void StartWritingChunk(int label) + { + if (mode != Mode.WRITING) Throw("Can't write chunk. Not set up for writing."); + if (isChunkInProgress) Throw("Can't start writing chunk " + label + ", was already writing one."); + // Make sure we have enough space for the chunk header (12 bytes). + PrepareToWrite(CHUNK_HEADER_SIZE); + // Memorize that this is where the current chunk starts. + chunkStartOffset = curOffset; + chunkLabel = label; + // Leave space for the chunk header (which we will come back and write later, in FinishWritingChunk). + curOffset += CHUNK_HEADER_SIZE; + // Ready to start writing chunk data. + isChunkInProgress = true; + } - /// - /// Obtains a reference the underlying buffer, data offset and data length. - /// This can only be called after writing is finished (after a call to FinishWriting()). - /// The returned buffer IS OWNED BY THIS INSTANCE and should not be modified by the caller. - /// - public void GetUnderlyingDataBuffer(out byte[] buffer, out int offset, out int length) { - if (mode != Mode.FINISHED_WRITING) Throw("Can't get byte array. Not in write finished mode."); - buffer = dataBuffer; - offset = dataStartOffset; - length = dataLength; - } + /// + /// Finishes writing the current chunk. + /// + /// The label of the chunk to finish writing. This is for sanity-checking. + public void FinishWritingChunk(int label) + { + // Sanity check that we are ending the same chunk that we started. + if (mode != Mode.WRITING) Throw("Can't finish writing chunk. Not set up for writing."); + if (!isChunkInProgress) Throw("Can't finish writing chunk. No write in progress."); + if (chunkLabel != label) + { + Throw("Wrong label on FinishWritingChunk, expected " + chunkLabel + ", got " + label); + } + + // Write the chunk header (we left space for it in StartWritingChunk). + // The chunk size is INCLUSIVE of the header, so it's just the difference from the + // current offset to the offset where the chunk started. + EncodeChunkHeader(chunkStartOffset, chunkLabel, curOffset - chunkStartOffset); + + isChunkInProgress = false; + } - /// - /// Converts the resulting buffer into a byte array. The returned byte array will have exactly the - /// contents of the data. - /// This can only be called when the serializer is in write mode, and when not writing a chunk. - /// For performance reasons, it is preferrable to use GetUnderlyingDataBuffer() instead, as that avoids - /// the allocation of a new buffer. - /// - /// - public byte[] ToByteArray() { - if (mode != Mode.FINISHED_WRITING) Throw("Can't get byte array. Not in write finished mode."); - byte[] result = new byte[dataLength]; - Buffer.BlockCopy( - /* src */ dataBuffer, /* srcOffset */ dataStartOffset, - /* dst */ result, /* dstOffset */ 0, - /* count */ dataLength); - return result; - } + /// + /// Writes an int to the current chunk. + /// Can only be called when currently writing a chunk. + /// + /// The value to write. + public void WriteInt(int value) + { + if (mode != Mode.WRITING) Throw("Can't write. Not set up for writing."); + if (!isChunkInProgress) Throw("Can't write. Not currently in a chunk."); + PrepareToWrite(4); + EncodeInt(curOffset, value); + curOffset += 4; + } - /// - /// Returns the label of the next chunk. - /// - /// The label of the next chunk in the data, or -1 if the data ends. - public int GetNextChunkLabel() { - if (mode != Mode.READING) Throw("Can't read next chunk label. Not set up for reading."); - if (isChunkInProgress) Throw("Can't read next chunk label. Chunk currently in progress."); - - if (!HasEnoughDataForChunkHeader(curOffset)) { - // End of data, no more chunks. - return -1; - } - int label; - int unused; - DecodeChunkHeader(curOffset, out label, out unused); - return label; - } + /// + /// Writes a byte to the current chunk. + /// Can only be called when currently writing a chunk. + /// + /// The value to write. + public void WriteByte(byte value) + { + if (mode != Mode.WRITING) Throw("Can't write. Not set up for writing."); + if (!isChunkInProgress) Throw("Can't write. Not currently in a chunk."); + PrepareToWrite(1); + dataBuffer[curOffset] = value; + curOffset++; + } - /// - /// Seeks ahead until we find the beginning of the chunk with the given label. - /// Ignores other chunks that are found before that. - /// - /// The label to advance to. + /// Writes a boolean to the current chunk. + /// Can only be called when currently writing a chunk. + /// + /// The value to write. + public void WriteBool(bool value) + { + WriteByte(value ? (byte)1 : (byte)0); + } - int thisLabel, thisSize; + /// + /// Writes a floating point value to the current chunk. + /// Can only be called when currently writing a chunk. + /// + /// The value to write. + public void WriteFloat(float value) + { + if (mode != Mode.WRITING) Throw("Can't write. Not set up for writing."); + if (!isChunkInProgress) Throw("Can't write. Not currently in a chunk."); + PrepareToWrite(4); + EncodeFloat(curOffset, value); + curOffset += 4; + } - // Skip chunks until we find the one we want. - // This allows files generated with newer versions of the serialization code to include chunks for - // new functionality, which older versions will just ignore. - while (true) { - // If we got to the end and didn't find the chunk, abort. - if (!HasEnoughDataForChunkHeader(curOffset)) Throw("Chunk not found: " + chunkLabel); + /// + /// Writes a string to the current chunk. + /// Can only be called when currently writing a chunk. + /// + /// The value to write. A null strings are handled correctly and will be read back + /// as a null string. + public void WriteString(string value) + { + if (mode != Mode.WRITING) Throw("Can't write. Not set up for writing."); + if (!isChunkInProgress) Throw("Can't write. Not currently in a chunk."); + + // Special case for writing NULL: write a null marker instead of the length. + if (value == null) + { + WriteInt(NULL_STRING_MARKER); + return; + } + + int byteCount = Encoding.UTF8.GetByteCount(value); + // We will write 4 bytes for the buffer length and then the bytes. + WriteInt(byteCount); + PrepareToWrite(byteCount); + int actualByteCount = Encoding.UTF8.GetBytes(value, 0, value.Length, dataBuffer, curOffset); + // The actual bytes written should be the same as our pre-calculated amount. This is guaranteed + // to be true by the UTF8 contract, but just as a sanity check, let's verify: + if (byteCount != actualByteCount) + { + Throw("UTF8 encoding error, expected " + byteCount + " bytes, got " + actualByteCount + " bytes"); + } + + curOffset += byteCount; + } - // Check if this chunk is the one we're looking for. - DecodeChunkHeader(curOffset, out thisLabel, out thisSize); + /// + /// Writes a count. A count is just an int with a special marker indicating that it's a count. + /// + /// + public void WriteCount(int count) + { + WriteInt(COUNT_FIELD_MARKER); + WriteInt(count); + } - // If it's the right label, stop here. This is the chunk we want. - if (thisLabel == chunkLabelToRead) break; + /// + /// Indicates that writing has finished. After calling this, you can query for the buffer with + /// ToByteArray() or GetUnderlyingDataBuffer(). This can only be called in write mode, and when + /// a chunk is not currently in progress. + /// + public void FinishWriting() + { + if (mode != Mode.WRITING) Throw("Can't finish writing. Not currently writing."); + if (isChunkInProgress) Throw("Can't finish writing. A chunk is still in progress."); + mode = Mode.FINISHED_WRITING; + } - // Not the right label, so skip to the next chunk. - curOffset += thisSize; - } + /// + /// Obtains a reference the underlying buffer, data offset and data length. + /// This can only be called after writing is finished (after a call to FinishWriting()). + /// The returned buffer IS OWNED BY THIS INSTANCE and should not be modified by the caller. + /// + public void GetUnderlyingDataBuffer(out byte[] buffer, out int offset, out int length) + { + if (mode != Mode.FINISHED_WRITING) Throw("Can't get byte array. Not in write finished mode."); + buffer = dataBuffer; + offset = dataStartOffset; + length = dataLength; + } - // At this point, we are now positioned at the start of the correct chunk. - isChunkInProgress = true; + /// + /// Converts the resulting buffer into a byte array. The returned byte array will have exactly the + /// contents of the data. + /// This can only be called when the serializer is in write mode, and when not writing a chunk. + /// For performance reasons, it is preferrable to use GetUnderlyingDataBuffer() instead, as that avoids + /// the allocation of a new buffer. + /// + /// + public byte[] ToByteArray() + { + if (mode != Mode.FINISHED_WRITING) Throw("Can't get byte array. Not in write finished mode."); + byte[] result = new byte[dataLength]; + Buffer.BlockCopy( + /* src */ dataBuffer, /* srcOffset */ dataStartOffset, + /* dst */ result, /* dstOffset */ 0, + /* count */ dataLength); + return result; + } - chunkLabel = thisLabel; - chunkStartOffset = curOffset; - chunkSizeForReading = thisSize; + /// + /// Returns the label of the next chunk. + /// + /// The label of the next chunk in the data, or -1 if the data ends. + public int GetNextChunkLabel() + { + if (mode != Mode.READING) Throw("Can't read next chunk label. Not set up for reading."); + if (isChunkInProgress) Throw("Can't read next chunk label. Chunk currently in progress."); + + if (!HasEnoughDataForChunkHeader(curOffset)) + { + // End of data, no more chunks. + return -1; + } + int label; + int unused; + DecodeChunkHeader(curOffset, out label, out unused); + return label; + } - // Skip the header, start reading from body of chunk. - curOffset += CHUNK_HEADER_SIZE; - } + /// + /// Seeks ahead until we find the beginning of the chunk with the given label. + /// Ignores other chunks that are found before that. + /// + /// The label to advance to. - /// Finishes reading a chunk. - /// - /// The chunk label of the chunk to finish. For sanity checking. - public void FinishReadingChunk(int chunkLabelToFinish) { - if (mode != Mode.READING) Throw("Can't finish reading chunk. Not set up for reading."); - if (!isChunkInProgress) Throw("Can't finish reading chunk " + chunkLabelToFinish + ". Was not reading one."); - if (chunkLabel != chunkLabelToFinish) { - Throw("Wrong chunk label when finishing reading, expected " + chunkLabel + " got " + chunkLabelToFinish); - } - - // Advance to the end of the chunk, skipping any portion of the chunk that wasn't read. - curOffset = chunkStartOffset + chunkSizeForReading; - isChunkInProgress = false; - } + /// + /// Finishes reading a chunk. + /// + /// The chunk label of the chunk to finish. For sanity checking. + public void FinishReadingChunk(int chunkLabelToFinish) + { + if (mode != Mode.READING) Throw("Can't finish reading chunk. Not set up for reading."); + if (!isChunkInProgress) Throw("Can't finish reading chunk " + chunkLabelToFinish + ". Was not reading one."); + if (chunkLabel != chunkLabelToFinish) + { + Throw("Wrong chunk label when finishing reading, expected " + chunkLabel + " got " + chunkLabelToFinish); + } + + // Advance to the end of the chunk, skipping any portion of the chunk that wasn't read. + curOffset = chunkStartOffset + chunkSizeForReading; + isChunkInProgress = false; + } - /// - /// Reads an int from the current chunk. - /// Can only be called in read mode, and when reading a chunk. - /// - /// The value. - public int ReadInt() { - AssertCanRead(4); - int value = DecodeInt(curOffset); - curOffset += 4; - return value; - } + /// + /// Reads an int from the current chunk. + /// Can only be called in read mode, and when reading a chunk. + /// + /// The value. + public int ReadInt() + { + AssertCanRead(4); + int value = DecodeInt(curOffset); + curOffset += 4; + return value; + } - /// - /// Reads a count, optionally with range checking. A count is just an integer, but it's annotated in a special - /// way for sanity checking because reading the wrong thing as a count can lead to bizarre results (for example, - /// erroneously believing that a mesh has 1,000,000,000 faces will likely exhaust memory during load). - /// - /// The minimum acceptable value for the count. Defaults to 0. - /// The maximum acceptable value for the count. Defaults to INT_MAX. - /// Name of the count, for debug purposes (if an exception is thrown). - /// The count. - public int ReadCount(int min = 0, int max = int.MaxValue, string name = "untitled") { - int marker = ReadInt(); - if (marker != COUNT_FIELD_MARKER) Throw("Expected count field marker, saw instead " + marker); - int count = ReadInt(); - if (count < min || count > max) { - Throw("Count (" + name + ") out of acceptable range [" + min + ", " + max + "]"); - } - return count; - } + /// + /// Reads a count, optionally with range checking. A count is just an integer, but it's annotated in a special + /// way for sanity checking because reading the wrong thing as a count can lead to bizarre results (for example, + /// erroneously believing that a mesh has 1,000,000,000 faces will likely exhaust memory during load). + /// + /// The minimum acceptable value for the count. Defaults to 0. + /// The maximum acceptable value for the count. Defaults to INT_MAX. + /// Name of the count, for debug purposes (if an exception is thrown). + /// The count. + public int ReadCount(int min = 0, int max = int.MaxValue, string name = "untitled") + { + int marker = ReadInt(); + if (marker != COUNT_FIELD_MARKER) Throw("Expected count field marker, saw instead " + marker); + int count = ReadInt(); + if (count < min || count > max) + { + Throw("Count (" + name + ") out of acceptable range [" + min + ", " + max + "]"); + } + return count; + } - /// - /// Reads a byte from the current chunk. - /// Can only be called in read mode, and when reading a chunk. - /// - /// The value. - public byte ReadByte() { - AssertCanRead(1); - byte value = dataBuffer[curOffset]; - curOffset++; - return value; - } + /// + /// Reads a byte from the current chunk. + /// Can only be called in read mode, and when reading a chunk. + /// + /// The value. + public byte ReadByte() + { + AssertCanRead(1); + byte value = dataBuffer[curOffset]; + curOffset++; + return value; + } - /// - /// Reads a boolean from the current chunk. - /// Can only be called in read mode, and when reading a chunk. - /// - /// The value. - public bool ReadBool() { - return ReadByte() != 0; - } + /// + /// Reads a boolean from the current chunk. + /// Can only be called in read mode, and when reading a chunk. + /// + /// The value. + public bool ReadBool() + { + return ReadByte() != 0; + } - /// - /// Reads a floating point value from the current chunk. - /// Can only be called in read mode, and when reading a chunk. - /// - /// The value. - public float ReadFloat() { - AssertCanRead(4); - float value = DecodeFloat(curOffset); - curOffset += 4; - return value; - } + /// + /// Reads a floating point value from the current chunk. + /// Can only be called in read mode, and when reading a chunk. + /// + /// The value. + public float ReadFloat() + { + AssertCanRead(4); + float value = DecodeFloat(curOffset); + curOffset += 4; + return value; + } - /// - /// Reads a string value from the current chunk. - /// Can only be called in read mode, and when reading a chunk. - /// - /// The string. Returns null to represent that a null string was actually present - /// in the file (null is a valid value that can be written and read back). - public string ReadString() { - AssertCanRead(4); - // First read the length of the byte array. - int byteCount = ReadInt(); - if (byteCount == NULL_STRING_MARKER) { - // Special case for the null string. - return null; - } else if (byteCount < 0) { - Throw("Invalid byte count for string: " + byteCount); - } - // Now read the byte array. - AssertCanRead(byteCount); - string value = Encoding.UTF8.GetString(dataBuffer, curOffset, byteCount); - curOffset += byteCount; - return value; - } + /// + /// Reads a string value from the current chunk. + /// Can only be called in read mode, and when reading a chunk. + /// + /// The string. Returns null to represent that a null string was actually present + /// in the file (null is a valid value that can be written and read back). + public string ReadString() + { + AssertCanRead(4); + // First read the length of the byte array. + int byteCount = ReadInt(); + if (byteCount == NULL_STRING_MARKER) + { + // Special case for the null string. + return null; + } + else if (byteCount < 0) + { + Throw("Invalid byte count for string: " + byteCount); + } + // Now read the byte array. + AssertCanRead(byteCount); + string value = Encoding.UTF8.GetString(dataBuffer, curOffset, byteCount); + curOffset += byteCount; + return value; + } - private void PrepareToWrite(int amount) { - int required = curOffset + amount; - if (required > dataBuffer.Length) { - byte[] newBuffer = new byte[required * 2]; - Buffer.BlockCopy( - /* src */ dataBuffer, /* srcOffset */ dataStartOffset, - /* dst */ newBuffer, /* dstOffset */ dataStartOffset, - /* count */ dataLength); - dataBuffer = newBuffer; - } - dataLength = Math.Max(dataLength, curOffset + amount - dataStartOffset); - } + private void PrepareToWrite(int amount) + { + int required = curOffset + amount; + if (required > dataBuffer.Length) + { + byte[] newBuffer = new byte[required * 2]; + Buffer.BlockCopy( + /* src */ dataBuffer, /* srcOffset */ dataStartOffset, + /* dst */ newBuffer, /* dstOffset */ dataStartOffset, + /* count */ dataLength); + dataBuffer = newBuffer; + } + dataLength = Math.Max(dataLength, curOffset + amount - dataStartOffset); + } - private bool HasEnoughDataForChunkHeader(int offset) { - return offset + CHUNK_HEADER_SIZE <= dataStartOffset + dataLength; - } + private bool HasEnoughDataForChunkHeader(int offset) + { + return offset + CHUNK_HEADER_SIZE <= dataStartOffset + dataLength; + } - private void AssertCanRead(int bytes) { - if (mode != Mode.READING) Throw("Can't read. Not set up for reading."); - if (!isChunkInProgress) Throw("Can't read. Chunk not open for reading."); - if (curOffset + bytes > chunkStartOffset + chunkSizeForReading) { - Throw("Unexpected end of chunk. Can't read " + bytes + " bytes."); - } - // These should not normally happen on well-formed chunks, but let's check anyway: - if (curOffset + bytes > dataStartOffset + dataLength) { - Throw("Unexpected end of data. Can't read " + bytes + " bytes."); - } - if (curOffset + bytes > dataBuffer.Length) { - Throw("Unexpected end of buffer. Can't read " + bytes + " bytes."); - } - } + private void AssertCanRead(int bytes) + { + if (mode != Mode.READING) Throw("Can't read. Not set up for reading."); + if (!isChunkInProgress) Throw("Can't read. Chunk not open for reading."); + if (curOffset + bytes > chunkStartOffset + chunkSizeForReading) + { + Throw("Unexpected end of chunk. Can't read " + bytes + " bytes."); + } + // These should not normally happen on well-formed chunks, but let's check anyway: + if (curOffset + bytes > dataStartOffset + dataLength) + { + Throw("Unexpected end of data. Can't read " + bytes + " bytes."); + } + if (curOffset + bytes > dataBuffer.Length) + { + Throw("Unexpected end of buffer. Can't read " + bytes + " bytes."); + } + } - private void EncodeInt(int offset, int value) { - uint uvalue = (uint)value; - // Little-endian (least significant byte first). - dataBuffer[offset] = (byte)uvalue; - dataBuffer[offset + 1] = (byte)(uvalue >> 8); - dataBuffer[offset + 2] = (byte)(uvalue >> 16); - dataBuffer[offset + 3] = (byte)(uvalue >> 24); - } + private void EncodeInt(int offset, int value) + { + uint uvalue = (uint)value; + // Little-endian (least significant byte first). + dataBuffer[offset] = (byte)uvalue; + dataBuffer[offset + 1] = (byte)(uvalue >> 8); + dataBuffer[offset + 2] = (byte)(uvalue >> 16); + dataBuffer[offset + 3] = (byte)(uvalue >> 24); + } - private int DecodeInt(int offset) { - return DecodeInt(dataBuffer, offset); - } + private int DecodeInt(int offset) + { + return DecodeInt(dataBuffer, offset); + } - private static int DecodeInt(byte[] buffer, int offset) { - // Little-endian (least significant byte first). - uint uvalue = - ((uint)buffer[offset]) | - ((uint)buffer[offset + 1] << 8) | - ((uint)buffer[offset + 2] << 16) | - ((uint)buffer[offset + 3] << 24); - return (int)uvalue; - } + private static int DecodeInt(byte[] buffer, int offset) + { + // Little-endian (least significant byte first). + uint uvalue = + ((uint)buffer[offset]) | + ((uint)buffer[offset + 1] << 8) | + ((uint)buffer[offset + 2] << 16) | + ((uint)buffer[offset + 3] << 24); + return (int)uvalue; + } - private void EncodeFloat(int offset, float value) { - // While we COULD in theory do complicated math to encode a float without relying on platform endianness, - // it's easy to just look in memory using an unsafe{} block. - unsafe { - byte* ptr = (byte*)&value; - if (isPlatformBigEndian) { - dataBuffer[offset] = ptr[3]; - dataBuffer[offset + 1] = ptr[2]; - dataBuffer[offset + 2] = ptr[1]; - dataBuffer[offset + 3] = ptr[0]; - } else { - dataBuffer[offset] = ptr[0]; - dataBuffer[offset + 1] = ptr[1]; - dataBuffer[offset + 2] = ptr[2]; - dataBuffer[offset + 3] = ptr[3]; - } - } - } + private void EncodeFloat(int offset, float value) + { + // While we COULD in theory do complicated math to encode a float without relying on platform endianness, + // it's easy to just look in memory using an unsafe{} block. + unsafe + { + byte* ptr = (byte*)&value; + if (isPlatformBigEndian) + { + dataBuffer[offset] = ptr[3]; + dataBuffer[offset + 1] = ptr[2]; + dataBuffer[offset + 2] = ptr[1]; + dataBuffer[offset + 3] = ptr[0]; + } + else + { + dataBuffer[offset] = ptr[0]; + dataBuffer[offset + 1] = ptr[1]; + dataBuffer[offset + 2] = ptr[2]; + dataBuffer[offset + 3] = ptr[3]; + } + } + } - private float DecodeFloat(int offset) { - float value = 0; - unsafe { - byte* ptr = (byte*)&value; - if (isPlatformBigEndian) { - ptr[3] = dataBuffer[offset]; - ptr[2] = dataBuffer[offset + 1]; - ptr[1] = dataBuffer[offset + 2]; - ptr[0] = dataBuffer[offset + 3]; - } else { - ptr[0] = dataBuffer[offset]; - ptr[1] = dataBuffer[offset + 1]; - ptr[2] = dataBuffer[offset + 2]; - ptr[3] = dataBuffer[offset + 3]; - } - } - return value; - } + private float DecodeFloat(int offset) + { + float value = 0; + unsafe + { + byte* ptr = (byte*)&value; + if (isPlatformBigEndian) + { + ptr[3] = dataBuffer[offset]; + ptr[2] = dataBuffer[offset + 1]; + ptr[1] = dataBuffer[offset + 2]; + ptr[0] = dataBuffer[offset + 3]; + } + else + { + ptr[0] = dataBuffer[offset]; + ptr[1] = dataBuffer[offset + 1]; + ptr[2] = dataBuffer[offset + 2]; + ptr[3] = dataBuffer[offset + 3]; + } + } + return value; + } - private void DecodeChunkHeader(int offset, out int label, out int size) { - int mark = DecodeInt(offset); - if (mark != CHUNK_START_MARK) { - Throw("Chunk start mark not found at " + offset + ", expected " + CHUNK_START_MARK + - ", found instead " + mark); - } - label = DecodeInt(offset + 4); - size = DecodeInt(offset + 8); - } + private void DecodeChunkHeader(int offset, out int label, out int size) + { + int mark = DecodeInt(offset); + if (mark != CHUNK_START_MARK) + { + Throw("Chunk start mark not found at " + offset + ", expected " + CHUNK_START_MARK + + ", found instead " + mark); + } + label = DecodeInt(offset + 4); + size = DecodeInt(offset + 8); + } - private void EncodeChunkHeader(int offset, int label, int size) { - EncodeInt(offset, CHUNK_START_MARK); - EncodeInt(offset + 4, label); - EncodeInt(offset + 8, size); - } + private void EncodeChunkHeader(int offset, int label, int size) + { + EncodeInt(offset, CHUNK_START_MARK); + EncodeInt(offset + 4, label); + EncodeInt(offset + 8, size); + } - private void Throw(string error) { - string message = new StringBuilder() - .Append("PolySerializer error: ") - .Append(error) - .Append(". ") - .Append("dataBuffer length: ").Append(dataBuffer != null ? dataBuffer.Length.ToString() : "(null)") - .Append("dataStartOffset: ").Append(dataStartOffset) - .Append("dataLength: ").Append(dataLength) - .Append("curOffset: ").Append(curOffset) - .Append("mode: ").Append(mode) - .Append("isChunkInProgress: ").Append(isChunkInProgress) - .Append("chunkStartOffset: ").Append(chunkStartOffset) - .Append("chunkLabel: ").Append(chunkLabel) - .Append("chunkSizeForReading: ").Append(chunkSizeForReading) - .ToString(); - throw new Exception(message); - } + private void Throw(string error) + { + string message = new StringBuilder() + .Append("PolySerializer error: ") + .Append(error) + .Append(". ") + .Append("dataBuffer length: ").Append(dataBuffer != null ? dataBuffer.Length.ToString() : "(null)") + .Append("dataStartOffset: ").Append(dataStartOffset) + .Append("dataLength: ").Append(dataLength) + .Append("curOffset: ").Append(curOffset) + .Append("mode: ").Append(mode) + .Append("isChunkInProgress: ").Append(isChunkInProgress) + .Append("chunkStartOffset: ").Append(chunkStartOffset) + .Append("chunkLabel: ").Append(chunkLabel) + .Append("chunkSizeForReading: ").Append(chunkSizeForReading) + .ToString(); + throw new Exception(message); + } - private static bool isPlatformBigEndian; - static PolySerializer() { - ushort testValue = 0xBEEF; - byte firstByte; - unsafe { // totally safe, though. - // Get the first byte of the representation. - firstByte = *((byte*)&testValue); - } - if (firstByte != 0xBE && firstByte != 0xEF) throw new Exception("Can't determine platform endian-ness."); - // Big-endian platforms put the most significant byte (0xBE in our case) first. - isPlatformBigEndian = (0xBE == firstByte); + private static bool isPlatformBigEndian; + static PolySerializer() + { + ushort testValue = 0xBEEF; + byte firstByte; + unsafe + { // totally safe, though. + // Get the first byte of the representation. + firstByte = *((byte*)&testValue); + } + if (firstByte != 0xBE && firstByte != 0xEF) throw new Exception("Can't determine platform endian-ness."); + // Big-endian platforms put the most significant byte (0xBE in our case) first. + isPlatformBigEndian = (0xBE == firstByte); + } } - } } diff --git a/Assets/Scripts/serialization/SerializationConsts.cs b/Assets/Scripts/serialization/SerializationConsts.cs index 385f81bd..272c4b15 100644 --- a/Assets/Scripts/serialization/SerializationConsts.cs +++ b/Assets/Scripts/serialization/SerializationConsts.cs @@ -17,27 +17,29 @@ using System.Linq; using System.Text; -namespace com.google.apps.peltzer.client.serialization { - public static class SerializationConsts { - // Chunk labels: - public const int CHUNK_PELTZER = 100; - // Basic mesh data. - public const int CHUNK_MMESH = 101; - // Remix IDs (optional chunk). - public const int CHUNK_MMESH_EXT_REMIX_IDS = 102; - // Recommended rotation of model on the Poly Menu (optional chunk). - public const int CHUNK_PELTZER_EXT_MODEL_ROTATION = 103; - // Note: when adding additional mesh chunks, name them CHUNK_MMESH_EXT_* and - // describe what new fields they contain. +namespace com.google.apps.peltzer.client.serialization +{ + public static class SerializationConsts + { + // Chunk labels: + public const int CHUNK_PELTZER = 100; + // Basic mesh data. + public const int CHUNK_MMESH = 101; + // Remix IDs (optional chunk). + public const int CHUNK_MMESH_EXT_REMIX_IDS = 102; + // Recommended rotation of model on the Poly Menu (optional chunk). + public const int CHUNK_PELTZER_EXT_MODEL_ROTATION = 103; + // Note: when adding additional mesh chunks, name them CHUNK_MMESH_EXT_* and + // describe what new fields they contain. - // Maximum allowed counts for repeated fields (for sanity checking). - public const int MAX_MESHES_PER_FILE = 100000; - public const int MAX_MATERIALS_PER_FILE = 1024; - public const int MAX_VERTICES_PER_MESH = 500000; - public const int MAX_FACES_PER_MESH = 100000; - public const int MAX_VERTICES_PER_FACE = 256; - public const int MAX_HOLES_PER_FACE = 256; - public const int MAX_VERTICES_PER_HOLE = 256; - public const int MAX_REMIX_IDS_PER_MMESH = 256; - } + // Maximum allowed counts for repeated fields (for sanity checking). + public const int MAX_MESHES_PER_FILE = 100000; + public const int MAX_MATERIALS_PER_FILE = 1024; + public const int MAX_VERTICES_PER_MESH = 500000; + public const int MAX_FACES_PER_MESH = 100000; + public const int MAX_VERTICES_PER_FACE = 256; + public const int MAX_HOLES_PER_FACE = 256; + public const int MAX_VERTICES_PER_HOLE = 256; + public const int MAX_REMIX_IDS_PER_MMESH = 256; + } } diff --git a/Assets/Scripts/testing/OTPerfTest.cs b/Assets/Scripts/testing/OTPerfTest.cs index 3edb961f..109acd14 100644 --- a/Assets/Scripts/testing/OTPerfTest.cs +++ b/Assets/Scripts/testing/OTPerfTest.cs @@ -17,149 +17,169 @@ using UnityEngine; using UnityEngine.Profiling; -public class OTPerfTest : MonoBehaviour { - - // Use this for initialization - void Start () { - Debug.Log("OTPerfTest Start"); - RunNativeTest(100000, 1000); - } - - void BasicTest(int numItems, int numQueries, float itemSize = 1f) { - - NativeSpatial nativeSpatial = new NativeSpatial(); - Random.InitState(12); - - // Add Test - Bounds itemBounds = new Bounds(Vector3.one, 0.5f * Vector3.one); - - nativeSpatial.Add(1, itemBounds); - HashSet nativeOutset = new HashSet(); - nativeSpatial.IntersectedBy(itemBounds, out nativeOutset); - foreach (int id in nativeOutset) { - Debug.Log("Collided with " + id); - }; - Debug.Log("Finished running intersection tests"); - } - - void RunValidationTest(int numItems, int numQueries, float itemSize = 1f) { - Bounds octreeBounds = new Bounds(Vector3.zero, 20f * Vector3.one); - CollisionSystem testOctree = new OctreeImpl(octreeBounds); - CollisionSystem nativeSpatial = new NativeSpatial(); - Random.InitState(12); - - // Add Test - - for (int i = 0; i < numItems; i++) { - Vector3 pos = new Vector3(Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f)); - Bounds itemBounds = new Bounds(pos, itemSize * Vector3.one); - Profiler.BeginSample("OTAdd"); - testOctree.Add(i, itemBounds); - nativeSpatial.Add(i, itemBounds); - Profiler.EndSample(); +public class OTPerfTest : MonoBehaviour +{ + + // Use this for initialization + void Start() + { + Debug.Log("OTPerfTest Start"); + RunNativeTest(100000, 1000); + } + + void BasicTest(int numItems, int numQueries, float itemSize = 1f) + { + + NativeSpatial nativeSpatial = new NativeSpatial(); + Random.InitState(12); + + // Add Test + Bounds itemBounds = new Bounds(Vector3.one, 0.5f * Vector3.one); + + nativeSpatial.Add(1, itemBounds); + HashSet nativeOutset = new HashSet(); + nativeSpatial.IntersectedBy(itemBounds, out nativeOutset); + foreach (int id in nativeOutset) + { + Debug.Log("Collided with " + id); + }; + Debug.Log("Finished running intersection tests"); } - - Debug.Log("About to run intersection tests"); - for (int i = 0; i < numQueries; i++) { - Vector3 pos = new Vector3(Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f)); - Bounds itemBounds = new Bounds(pos, 1f * Vector3.one); - HashSet outSet = new HashSet(); - HashSet nativeOutset = new HashSet(); - Profiler.BeginSample("OTIntersect"); //Debug.Log("Octree returned set of " + outSet.Count + " items"); - testOctree.IntersectedBy(itemBounds, out outSet); - nativeSpatial.IntersectedBy(itemBounds, out nativeOutset); - if (outSet != null) { - //Debug.Log("Comparing nonzero results " + outSet.Count + " and " + nativeOutset.Count); - int origSize = outSet.Count; - outSet.IntersectWith(nativeOutset); - if (outSet.Count != origSize) { - Debug.Log("Results didn't match! Octree set size: " + origSize + " native size " + nativeOutset.Count); + + void RunValidationTest(int numItems, int numQueries, float itemSize = 1f) + { + Bounds octreeBounds = new Bounds(Vector3.zero, 20f * Vector3.one); + CollisionSystem testOctree = new OctreeImpl(octreeBounds); + CollisionSystem nativeSpatial = new NativeSpatial(); + Random.InitState(12); + + // Add Test + + for (int i = 0; i < numItems; i++) + { + Vector3 pos = new Vector3(Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f)); + Bounds itemBounds = new Bounds(pos, itemSize * Vector3.one); + Profiler.BeginSample("OTAdd"); + testOctree.Add(i, itemBounds); + nativeSpatial.Add(i, itemBounds); + Profiler.EndSample(); } - } - else { - if (nativeOutset.Count != 0) { - Debug.Log("Native code returned more results than original"); + + Debug.Log("About to run intersection tests"); + for (int i = 0; i < numQueries; i++) + { + Vector3 pos = new Vector3(Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f)); + Bounds itemBounds = new Bounds(pos, 1f * Vector3.one); + HashSet outSet = new HashSet(); + HashSet nativeOutset = new HashSet(); + Profiler.BeginSample("OTIntersect"); //Debug.Log("Octree returned set of " + outSet.Count + " items"); + testOctree.IntersectedBy(itemBounds, out outSet); + nativeSpatial.IntersectedBy(itemBounds, out nativeOutset); + if (outSet != null) + { + //Debug.Log("Comparing nonzero results " + outSet.Count + " and " + nativeOutset.Count); + int origSize = outSet.Count; + outSet.IntersectWith(nativeOutset); + if (outSet.Count != origSize) + { + Debug.Log("Results didn't match! Octree set size: " + origSize + " native size " + nativeOutset.Count); + } + } + else + { + if (nativeOutset.Count != 0) + { + Debug.Log("Native code returned more results than original"); + } + } + Profiler.EndSample(); } - } - Profiler.EndSample(); + Debug.Log("Finished running intersection tests"); } - Debug.Log("Finished running intersection tests"); - } - - void RunOctreeTest(int numItems, int numQueries, float itemSize = 1f) { - Bounds octreeBounds = new Bounds(Vector3.zero, 20f * Vector3.one); - CollisionSystem testOctree = new OctreeImpl(octreeBounds); - - Random.InitState(12); - - // Add Test - - for (int i = 0; i < numItems; i++) { - Vector3 pos = new Vector3(Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f)); - Bounds itemBounds = new Bounds(pos, itemSize * Vector3.one); - Profiler.BeginSample("OTAdd"); - testOctree.Add(i, itemBounds); - Profiler.EndSample(); - } - - for (int i = 0; i < numItems; i++) { - Vector3 pos = new Vector3(Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f)); - Bounds itemBounds = new Bounds(pos, itemSize * Vector3.one); - Profiler.BeginSample("OTModify"); - testOctree.UpdateItemBounds(i, itemBounds); - Profiler.EndSample(); - } - - for (int i = 0; i < numQueries; i++) { - Vector3 pos = new Vector3(Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f)); - Bounds itemBounds = new Bounds(pos, 0.001f * Vector3.one); - HashSet outSet = new HashSet(); - Profiler.BeginSample("OTIntersect"); - testOctree.IntersectedBy(itemBounds, out outSet); - Profiler.EndSample(); - } - - - } - - void RunNativeTest(int numItems, int numQueries, float itemSize = 1f) { - - NativeSpatial nativeSpatial = new NativeSpatial(); - Random.InitState(12); - - - // Add Test - - for (int i = 0; i < numItems; i++) { - Vector3 pos = new Vector3(Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f)); - Bounds itemBounds = new Bounds(pos, itemSize * Vector3.one); - Profiler.BeginSample("OTAdd"); - nativeSpatial.Add(i, itemBounds); - Profiler.EndSample(); + + void RunOctreeTest(int numItems, int numQueries, float itemSize = 1f) + { + Bounds octreeBounds = new Bounds(Vector3.zero, 20f * Vector3.one); + CollisionSystem testOctree = new OctreeImpl(octreeBounds); + + Random.InitState(12); + + // Add Test + + for (int i = 0; i < numItems; i++) + { + Vector3 pos = new Vector3(Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f)); + Bounds itemBounds = new Bounds(pos, itemSize * Vector3.one); + Profiler.BeginSample("OTAdd"); + testOctree.Add(i, itemBounds); + Profiler.EndSample(); + } + + for (int i = 0; i < numItems; i++) + { + Vector3 pos = new Vector3(Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f)); + Bounds itemBounds = new Bounds(pos, itemSize * Vector3.one); + Profiler.BeginSample("OTModify"); + testOctree.UpdateItemBounds(i, itemBounds); + Profiler.EndSample(); + } + + for (int i = 0; i < numQueries; i++) + { + Vector3 pos = new Vector3(Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f)); + Bounds itemBounds = new Bounds(pos, 0.001f * Vector3.one); + HashSet outSet = new HashSet(); + Profiler.BeginSample("OTIntersect"); + testOctree.IntersectedBy(itemBounds, out outSet); + Profiler.EndSample(); + } + + } - - for (int i = 0; i < numItems; i++) { - Vector3 pos = new Vector3(Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f)); - Bounds itemBounds = new Bounds(pos, itemSize * Vector3.one); - Profiler.BeginSample("OTModify"); - nativeSpatial.UpdateItemBounds(i, itemBounds); - Profiler.EndSample(); + + void RunNativeTest(int numItems, int numQueries, float itemSize = 1f) + { + + NativeSpatial nativeSpatial = new NativeSpatial(); + Random.InitState(12); + + + // Add Test + + for (int i = 0; i < numItems; i++) + { + Vector3 pos = new Vector3(Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f)); + Bounds itemBounds = new Bounds(pos, itemSize * Vector3.one); + Profiler.BeginSample("OTAdd"); + nativeSpatial.Add(i, itemBounds); + Profiler.EndSample(); + } + + for (int i = 0; i < numItems; i++) + { + Vector3 pos = new Vector3(Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f)); + Bounds itemBounds = new Bounds(pos, itemSize * Vector3.one); + Profiler.BeginSample("OTModify"); + nativeSpatial.UpdateItemBounds(i, itemBounds); + Profiler.EndSample(); + } + + for (int i = 0; i < numQueries; i++) + { + Vector3 pos = new Vector3(Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f)); + Bounds itemBounds = new Bounds(pos, 0.001f * Vector3.one); + HashSet outSet = new HashSet(); + Profiler.BeginSample("OTIntersect"); + nativeSpatial.IntersectedBy(itemBounds, out outSet); + Profiler.EndSample(); + } + + } - - for (int i = 0; i < numQueries; i++) { - Vector3 pos = new Vector3(Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f), Random.Range(-9.5f, 9.5f)); - Bounds itemBounds = new Bounds(pos, 0.001f * Vector3.one); - HashSet outSet = new HashSet(); - Profiler.BeginSample("OTIntersect"); - nativeSpatial.IntersectedBy(itemBounds, out outSet); - Profiler.EndSample(); + + // Update is called once per frame + void Update() + { + } - - - } - - // Update is called once per frame - void Update () { - - } } diff --git a/Assets/Scripts/testing/PerformanceTesting.cs b/Assets/Scripts/testing/PerformanceTesting.cs index 1da3ad14..d84cec60 100644 --- a/Assets/Scripts/testing/PerformanceTesting.cs +++ b/Assets/Scripts/testing/PerformanceTesting.cs @@ -18,160 +18,187 @@ using com.google.apps.peltzer.client.tools.utils; using UnityEngine; -namespace com.google.apps.peltzer.client.testing { - /// - /// Utility to stress-test Poly. - /// - class PerformanceTesting : MonoBehaviour { - // Whether we're creating one complex mesh (if not, we're creating many small ones): - private static readonly bool ONE_COMPLEX_MESH = false; - - // How many instances per axis, the total number of meshes will be the cube of this number (ish). - // http://www.miniwebtool.com/cube-numbers-list/?to=1000 is useful. - private static readonly float ENTRIES_PER_AXIS = 32; - - // What primitive to insert. Cubes have 6 faces/12 tris, spheres have 80 faces/240 tris. - private static readonly Primitives.Shape primitive = Primitives.Shape.CUBE; - - // A counter of available memory. I haven't gotten this working yet, so I'm just trusting Task Manager - // to give decent estimates right now. Unity has a memory profiler but it seems to only be profiling - // objects managed by Unity: it tends to under-report memory usage by 3x compared to Task Manager and - // so I don't trust it right now. - System.Diagnostics.PerformanceCounter ramCounter; - - // To calculate a rolling FPS: http://wiki.unity3d.com/index.php?title=FramesPerSecond - float deltaTime = 0.0f; - - // We'll create one mesh in Start() and then keep cloning it for our instancing. - MMesh mesh; - - // The negative limit of any given axis, given our bounds. - float minDimension; - // The distance between centers of meshes on any given axis. - float increment; - - // Keeping track of our instancing, I'm sure there's a prettier way but this works. We set the original - // position of the mesh to the negative limit of the bounds. We then move along the xAxis by 'increment' - // until hitting the positive X limit, then reset the X position and raise the Y position by 'increment'. - // When we hit the positive Y limit, we reset the X and Y positions and raise the Z position by 'increment' - int xChanges = 0; - int yChanges = 0; - int zChanges = 0; - - // We use ~10 different materials just to keep our ReMesher busy, as it groups by material. This doesn't - // seem to have much of a noticable effect on anything right now. - int materialId = 1; - - PeltzerMain peltzerMain; - - void Start() { - // Instantiate the RAM profiler. - System.Diagnostics.PerformanceCounterCategory.Exists("PerformanceCounter"); - ramCounter = new System.Diagnostics.PerformanceCounter("Memory", "Available MBytes"); - - peltzerMain = FindObjectOfType(); - - if (ONE_COMPLEX_MESH) { - mesh = Primitives.AxisAlignedIcosphere(/* id */ 0, - /* Just in front of user*/ new Vector3(0f, 1f, 0.5f), - /* Big */ Vector3.one * (GridUtils.GRID_SIZE / 2.0f) * 15, - /* materialID */ 1, - /* recursionLevel */ 4); - Debug.Log("Adding one complex mesh with " + mesh.vertexCount + " verts and " + mesh.faceCount + " faces"); - } else { - // Determine the negative limit and the distance between centers given the bounds. - minDimension = -PeltzerMain.DEFAULT_BOUNDS.extents.x + GridUtils.GRID_SIZE; - increment = PeltzerMain.DEFAULT_BOUNDS.extents.x / (ENTRIES_PER_AXIS + 1) * 2; - - // Create a mesh and position it at the negative limits. - if (primitive == Primitives.Shape.CUBE) { - mesh = Primitives.AxisAlignedBox(0, Vector3.one * minDimension, - Vector3.one * (GridUtils.GRID_SIZE / 2.0f), 1); - } else if (primitive == Primitives.Shape.SPHERE) { - mesh = Primitives.AxisAlignedIcosphere(0, Vector3.one * minDimension, - Vector3.one * (GridUtils.GRID_SIZE / 2.0f), 1); +namespace com.google.apps.peltzer.client.testing +{ + /// + /// Utility to stress-test Poly. + /// + class PerformanceTesting : MonoBehaviour + { + // Whether we're creating one complex mesh (if not, we're creating many small ones): + private static readonly bool ONE_COMPLEX_MESH = false; + + // How many instances per axis, the total number of meshes will be the cube of this number (ish). + // http://www.miniwebtool.com/cube-numbers-list/?to=1000 is useful. + private static readonly float ENTRIES_PER_AXIS = 32; + + // What primitive to insert. Cubes have 6 faces/12 tris, spheres have 80 faces/240 tris. + private static readonly Primitives.Shape primitive = Primitives.Shape.CUBE; + + // A counter of available memory. I haven't gotten this working yet, so I'm just trusting Task Manager + // to give decent estimates right now. Unity has a memory profiler but it seems to only be profiling + // objects managed by Unity: it tends to under-report memory usage by 3x compared to Task Manager and + // so I don't trust it right now. + System.Diagnostics.PerformanceCounter ramCounter; + + // To calculate a rolling FPS: http://wiki.unity3d.com/index.php?title=FramesPerSecond + float deltaTime = 0.0f; + + // We'll create one mesh in Start() and then keep cloning it for our instancing. + MMesh mesh; + + // The negative limit of any given axis, given our bounds. + float minDimension; + // The distance between centers of meshes on any given axis. + float increment; + + // Keeping track of our instancing, I'm sure there's a prettier way but this works. We set the original + // position of the mesh to the negative limit of the bounds. We then move along the xAxis by 'increment' + // until hitting the positive X limit, then reset the X position and raise the Y position by 'increment'. + // When we hit the positive Y limit, we reset the X and Y positions and raise the Z position by 'increment' + int xChanges = 0; + int yChanges = 0; + int zChanges = 0; + + // We use ~10 different materials just to keep our ReMesher busy, as it groups by material. This doesn't + // seem to have much of a noticable effect on anything right now. + int materialId = 1; + + PeltzerMain peltzerMain; + + void Start() + { + // Instantiate the RAM profiler. + System.Diagnostics.PerformanceCounterCategory.Exists("PerformanceCounter"); + ramCounter = new System.Diagnostics.PerformanceCounter("Memory", "Available MBytes"); + + peltzerMain = FindObjectOfType(); + + if (ONE_COMPLEX_MESH) + { + mesh = Primitives.AxisAlignedIcosphere(/* id */ 0, + /* Just in front of user*/ new Vector3(0f, 1f, 0.5f), + /* Big */ Vector3.one * (GridUtils.GRID_SIZE / 2.0f) * 15, + /* materialID */ 1, + /* recursionLevel */ 4); + Debug.Log("Adding one complex mesh with " + mesh.vertexCount + " verts and " + mesh.faceCount + " faces"); + } + else + { + // Determine the negative limit and the distance between centers given the bounds. + minDimension = -PeltzerMain.DEFAULT_BOUNDS.extents.x + GridUtils.GRID_SIZE; + increment = PeltzerMain.DEFAULT_BOUNDS.extents.x / (ENTRIES_PER_AXIS + 1) * 2; + + // Create a mesh and position it at the negative limits. + if (primitive == Primitives.Shape.CUBE) + { + mesh = Primitives.AxisAlignedBox(0, Vector3.one * minDimension, + Vector3.one * (GridUtils.GRID_SIZE / 2.0f), 1); + } + else if (primitive == Primitives.Shape.SPHERE) + { + mesh = Primitives.AxisAlignedIcosphere(0, Vector3.one * minDimension, + Vector3.one * (GridUtils.GRID_SIZE / 2.0f), 1); + } + } } - } - } - // Adds 1000 meshes per frame, unless we're just dealing with a single complex mesh. - void Update() { - // Ugly but means I don't have to think about startup ordering or make PeltzerMain aware of this class. - Model model = peltzerMain.GetModel(); - if (model == null) { - return; - } - - if (ONE_COMPLEX_MESH) { - if (model.GetNumberOfMeshes() >= 1) { - return; - } else { - model.AddMesh(mesh); - return; - } - } - - // Stop inserting once we've filled all 3 axes. - if (zChanges >= ENTRIES_PER_AXIS) { - return; - } - - // Keep track of FPS. - deltaTime += (Time.deltaTime - deltaTime) * 0.1f; - - // Limiting to 1000 inserts per frame seems to keep Unity from crashing. - for (int i = 0; i < 1000; i++) { - // Abort the loop when done and print to console. - if (zChanges >= ENTRIES_PER_AXIS) { - Debug.Log("Added " + model.GetNumberOfMeshes()); - return; - } + // Adds 1000 meshes per frame, unless we're just dealing with a single complex mesh. + void Update() + { + // Ugly but means I don't have to think about startup ordering or make PeltzerMain aware of this class. + Model model = peltzerMain.GetModel(); + if (model == null) + { + return; + } - if (xChanges < ENTRIES_PER_AXIS) { - // Move on the X axis. - xChanges++; - mesh.offset += new Vector3(increment, 0f, 0f); - } else if (xChanges == ENTRIES_PER_AXIS) { - // If at the positive limit of the X axis, reset X position and bump up Y position. - if (yChanges == ENTRIES_PER_AXIS) { - // If at the positive limit of the Y axis, reset X and Y position and bump up Z position. - xChanges = 0; - yChanges = 0; - zChanges++; - mesh.offset = new Vector3(minDimension, minDimension, mesh.offset.z + increment); - - // Also switch up the material ID. - materialId++; - materialId %= MaterialRegistry.GetNumMaterials(); - materialId++; - foreach (Face face in mesh.GetFaces()) { - face.SetProperties(new FaceProperties(materialId)); + if (ONE_COMPLEX_MESH) + { + if (model.GetNumberOfMeshes() >= 1) + { + return; + } + else + { + model.AddMesh(mesh); + return; + } } - } - yChanges++; - mesh.offset = new Vector3(minDimension, mesh.offset.y + increment, mesh.offset.z); - xChanges = 0; - } - // Insert the new mesh. - MMesh clone = mesh.CloneWithNewId(model.GenerateMeshId()); - if (!model.AddMesh(clone)) { - // Should never happen, doesn't seem to, but why not. - // Debug.Log(clone.offset.x + ", " + clone.offset.y + ", " + clone.offset.z); - } else { - int count = model.GetNumberOfMeshes(); - if (count % 200 == 0) { - // Some printing code, I've just been using Unity profiler for FPS though, and can't get the memory - // profiling working. - float msec = deltaTime * 1000.0f; - float fps = 1.0f / deltaTime; - //StringBuilder stringBuilder = new StringBuilder(string.Format("({0:0.} fps)", fps)); - //stringBuilder.Append(ramCounter.NextValue()).Append("MB,"); - //stringBuilder.Append(count).Append(" meshes"); - //Debug.Log(stringBuilder.ToString()); - } + // Stop inserting once we've filled all 3 axes. + if (zChanges >= ENTRIES_PER_AXIS) + { + return; + } + + // Keep track of FPS. + deltaTime += (Time.deltaTime - deltaTime) * 0.1f; + + // Limiting to 1000 inserts per frame seems to keep Unity from crashing. + for (int i = 0; i < 1000; i++) + { + // Abort the loop when done and print to console. + if (zChanges >= ENTRIES_PER_AXIS) + { + Debug.Log("Added " + model.GetNumberOfMeshes()); + return; + } + + if (xChanges < ENTRIES_PER_AXIS) + { + // Move on the X axis. + xChanges++; + mesh.offset += new Vector3(increment, 0f, 0f); + } + else if (xChanges == ENTRIES_PER_AXIS) + { + // If at the positive limit of the X axis, reset X position and bump up Y position. + if (yChanges == ENTRIES_PER_AXIS) + { + // If at the positive limit of the Y axis, reset X and Y position and bump up Z position. + xChanges = 0; + yChanges = 0; + zChanges++; + mesh.offset = new Vector3(minDimension, minDimension, mesh.offset.z + increment); + + // Also switch up the material ID. + materialId++; + materialId %= MaterialRegistry.GetNumMaterials(); + materialId++; + foreach (Face face in mesh.GetFaces()) + { + face.SetProperties(new FaceProperties(materialId)); + } + } + yChanges++; + mesh.offset = new Vector3(minDimension, mesh.offset.y + increment, mesh.offset.z); + xChanges = 0; + } + + // Insert the new mesh. + MMesh clone = mesh.CloneWithNewId(model.GenerateMeshId()); + if (!model.AddMesh(clone)) + { + // Should never happen, doesn't seem to, but why not. + // Debug.Log(clone.offset.x + ", " + clone.offset.y + ", " + clone.offset.z); + } + else + { + int count = model.GetNumberOfMeshes(); + if (count % 200 == 0) + { + // Some printing code, I've just been using Unity profiler for FPS though, and can't get the memory + // profiling working. + float msec = deltaTime * 1000.0f; + float fps = 1.0f / deltaTime; + //StringBuilder stringBuilder = new StringBuilder(string.Format("({0:0.} fps)", fps)); + //stringBuilder.Append(ramCounter.NextValue()).Append("MB,"); + //stringBuilder.Append(count).Append(" meshes"); + //Debug.Log(stringBuilder.ToString()); + } + } + } } - } } - } } diff --git a/Assets/Scripts/tools/BackgroundMeshValidator.cs b/Assets/Scripts/tools/BackgroundMeshValidator.cs index ee00cb31..6c93da73 100644 --- a/Assets/Scripts/tools/BackgroundMeshValidator.cs +++ b/Assets/Scripts/tools/BackgroundMeshValidator.cs @@ -21,287 +21,323 @@ using UnityEngine; using System; -namespace com.google.apps.peltzer.client.tools { - /// - /// Helper object that continually validates meshes in a background thread, indicating when they are invalid and - /// what was the last valid state. - /// - /// To use this object, create an instance and call StartValidating() when you want to start validating. Then call - /// UpdateMeshes() every frame from the UI thread to give the validator the current state of the meshes. - /// Call StopValidating() when you want to stop validating. - /// - /// Between StartValidating() and StopValidating(), you can read the ValidityState property at any time to know if the validator - /// currently thinks the meshes are valid or not, and you can call GetLastValidState to get the latest - /// snapshot of the meshes when they were last considered to be valid. - /// - public class BackgroundMeshValidator { +namespace com.google.apps.peltzer.client.tools +{ /// - /// Worker thread that runs in the background doing validation. If this is not null, then the background thread - /// is currently running. + /// Helper object that continually validates meshes in a background thread, indicating when they are invalid and + /// what was the last valid state. + /// + /// To use this object, create an instance and call StartValidating() when you want to start validating. Then call + /// UpdateMeshes() every frame from the UI thread to give the validator the current state of the meshes. + /// Call StopValidating() when you want to stop validating. + /// + /// Between StartValidating() and StopValidating(), you can read the ValidityState property at any time to know if the validator + /// currently thinks the meshes are valid or not, and you can call GetLastValidState to get the latest + /// snapshot of the meshes when they were last considered to be valid. /// - private Thread workerThread; + public class BackgroundMeshValidator + { + /// + /// Worker thread that runs in the background doing validation. If this is not null, then the background thread + /// is currently running. + /// + private Thread workerThread; - private readonly Model model; + private readonly Model model; - /// - /// Lock that guards the data in this class. Lock must be held to access most member variables - /// (as noted below). - /// - private object lockObject = new object(); + /// + /// Lock that guards the data in this class. Lock must be held to access most member variables + /// (as noted below). + /// + private object lockObject = new object(); - /// - /// Represents each of the possible states that we can be in. - /// - private enum State { - // Not running. - NOT_RUNNING, - // Starting up. - STARTING, - // The background thread is hungry, waiting for juicy new data to process. - WAITING_FOR_DATA, - // The background thread is validating the last provided data. - VALIDATING, - // We want to stop. Background thread being kindly asked to quit. - QUITTING, - }; + /// + /// Represents each of the possible states that we can be in. + /// + private enum State + { + // Not running. + NOT_RUNNING, + // Starting up. + STARTING, + // The background thread is hungry, waiting for juicy new data to process. + WAITING_FOR_DATA, + // The background thread is validating the last provided data. + VALIDATING, + // We want to stop. Background thread being kindly asked to quit. + QUITTING, + }; - /// - /// Represents the validity of the mesh. - /// - public enum Validity { - // Means we haven't analyzed a snapshot yet, so we don't know. - NOT_YET_KNOWN, - // The last snapshot we analyzed was invalid. - INVALID, - // The last snapshot we analyzed was valid. - VALID, - }; + /// + /// Represents the validity of the mesh. + /// + public enum Validity + { + // Means we haven't analyzed a snapshot yet, so we don't know. + NOT_YET_KNOWN, + // The last snapshot we analyzed was invalid. + INVALID, + // The last snapshot we analyzed was valid. + VALID, + }; - /// - /// The state we are currently in. - /// GUARDED_BY(lockObject) - /// - private State state = State.NOT_RUNNING; + /// + /// The state we are currently in. + /// GUARDED_BY(lockObject) + /// + private State state = State.NOT_RUNNING; - /// - /// Validation copies. These are guarded by the lock. - /// GUARDED_BY(lockObject) - /// - private List validationCopies; + /// + /// Validation copies. These are guarded by the lock. + /// GUARDED_BY(lockObject) + /// + private List validationCopies; - /// - /// List of all vertices that are bring manipulated (in all meshes). - /// GUARDED_BY(lockObject) - /// - private HashSet updatedVerts; + /// + /// List of all vertices that are bring manipulated (in all meshes). + /// GUARDED_BY(lockObject) + /// + private HashSet updatedVerts; - /// - /// Last valid state of the meshes (dictionary from mesh ID to MMesh). - /// IMPORTANT: this is returned to the caller, so it's IMMUTABLE once returned. - /// When we make a new one, we just replace this entirely by a new Dictionary. - /// GUARDED_BY(lockObject) - private Dictionary lastValidState; + /// + /// Last valid state of the meshes (dictionary from mesh ID to MMesh). + /// IMPORTANT: this is returned to the caller, so it's IMMUTABLE once returned. + /// When we make a new one, we just replace this entirely by a new Dictionary. + /// GUARDED_BY(lockObject) + private Dictionary lastValidState; - /// - /// Current state of the meshes (the validity of the last snapshot we analyzed). - /// This is volatile for efficiency (for lockless read/write). It can only be written by - /// the WORKER thread, and can be read from any thread. - /// - private volatile Validity validity; + /// + /// Current state of the meshes (the validity of the last snapshot we analyzed). + /// This is volatile for efficiency (for lockless read/write). It can only be written by + /// the WORKER thread, and can be read from any thread. + /// + private volatile Validity validity; - /// - /// Creates a BackgroundMeshValidator. This will not automatically START it (you have to do that explicitly - /// by calling StartValidating() when you're ready). - /// - public BackgroundMeshValidator(Model model) { - this.model = model; - } + /// + /// Creates a BackgroundMeshValidator. This will not automatically START it (you have to do that explicitly + /// by calling StartValidating() when you're ready). + /// + public BackgroundMeshValidator(Model model) + { + this.model = model; + } - /// - /// Returns whether the validator is currently active. The validator is active between the calls to - /// StartValidating() and StopValidating(). - /// - public bool IsActive { - get { - return workerThread != null; - } - } + /// + /// Returns whether the validator is currently active. The validator is active between the calls to + /// StartValidating() and StopValidating(). + /// + public bool IsActive + { + get + { + return workerThread != null; + } + } - /// - /// Returns the current validity state of the meshes (as of the last snapshot the background thread has a - /// chance to analyze). This may change suddenly, as it's based on a best effort by the background thread, - /// which is processing and validating meshes asynchronously. Note that if this is called when the - /// validator is not active, this will return the last known state. - /// - public Validity ValidityState { - get { - // validity is volatile so it can be read from any thread. - return validity; - } - } + /// + /// Returns the current validity state of the meshes (as of the last snapshot the background thread has a + /// chance to analyze). This may change suddenly, as it's based on a best effort by the background thread, + /// which is processing and validating meshes asynchronously. Note that if this is called when the + /// validator is not active, this will return the last known state. + /// + public Validity ValidityState + { + get + { + // validity is volatile so it can be read from any thread. + return validity; + } + } - /// - /// Starts the validator. This will start the background thread that will do the actual work. After calling - /// StartValidating(), you must call OfferPreviewMeshes() on every frame in order to feed data to the background thread. - /// - public void StartValidating() { - lock (lockObject) { - AssertOrThrow.True(state == State.NOT_RUNNING, "State should be State.NOT_RUNNING"); - state = State.STARTING; - } - validity = Validity.NOT_YET_KNOWN; - lastValidState = new Dictionary(); - validationCopies = null; - updatedVerts = null; + /// + /// Starts the validator. This will start the background thread that will do the actual work. After calling + /// StartValidating(), you must call OfferPreviewMeshes() on every frame in order to feed data to the background thread. + /// + public void StartValidating() + { + lock (lockObject) + { + AssertOrThrow.True(state == State.NOT_RUNNING, "State should be State.NOT_RUNNING"); + state = State.STARTING; + } + validity = Validity.NOT_YET_KNOWN; + lastValidState = new Dictionary(); + validationCopies = null; + updatedVerts = null; - workerThread = new Thread(new ThreadStart(WorkerThreadMain)); - workerThread.IsBackground = true; - workerThread.Start(); - } + workerThread = new Thread(new ThreadStart(WorkerThreadMain)); + workerThread.IsBackground = true; + workerThread.Start(); + } - /// - /// Stops the validator. Call this from the UI thread when you want to stop validating meshes. - /// IMPORTANT: after this method returns, ValidityState and GetLastValidState() will return the state of the - /// last snapshot examined in the background, which will NOT NECESSARILY be the last state fed through - /// UpdateMeshes(). It will be the last state that the background thread had a chance to analyze. It will - /// not "catch up" to the latest state when stopping. If this turns out to be problematic, maybe we - /// should instead implement a queue of capacity 1 and ensure that StopValidating() will get the chance - /// to validate the last state fed through UpdateMeshes(). - /// - public void StopValidating() { - lock (lockObject) { - // If we are not running, there's nothing to do. - if (state == State.NOT_RUNNING) return; - // Indicate to the worker thread that we're calling it a day. - state = State.QUITTING; - // Wake up background thread to make it notice the state change. - Monitor.Pulse(lockObject); - } - // Block and wait for worker thread to finish. - workerThread.Join(); - workerThread = null; - // We can update state without locking because we know the worker thread is dead (we just killed it!). - state = State.NOT_RUNNING; - } + /// + /// Stops the validator. Call this from the UI thread when you want to stop validating meshes. + /// IMPORTANT: after this method returns, ValidityState and GetLastValidState() will return the state of the + /// last snapshot examined in the background, which will NOT NECESSARILY be the last state fed through + /// UpdateMeshes(). It will be the last state that the background thread had a chance to analyze. It will + /// not "catch up" to the latest state when stopping. If this turns out to be problematic, maybe we + /// should instead implement a queue of capacity 1 and ensure that StopValidating() will get the chance + /// to validate the last state fed through UpdateMeshes(). + /// + public void StopValidating() + { + lock (lockObject) + { + // If we are not running, there's nothing to do. + if (state == State.NOT_RUNNING) return; + // Indicate to the worker thread that we're calling it a day. + state = State.QUITTING; + // Wake up background thread to make it notice the state change. + Monitor.Pulse(lockObject); + } + // Block and wait for worker thread to finish. + workerThread.Join(); + workerThread = null; + // We can update state without locking because we know the worker thread is dead (we just killed it!). + state = State.NOT_RUNNING; + } - /// - /// Call this once per frame from the UI thread to offer updated preview meshes to the validator. - /// The validator may or may not want them, depending on its current state. Even if it doesn't - /// want them right now, it will appreciate your politeness in offering, and do nothing. - /// It eventually will accept them, though, when it's ready. That's why you have to call on - /// every frame. - /// - /// The current state of the meshes. - /// All the vertices (in all meshes) that are being manipulated. - public void UpdateMeshes(Dictionary meshes, HashSet updatedVerts) { - // It's an error to call this while not running. - AssertOrThrow.True(workerThread != null, - "Can't call UpdateMeshes when BackgroundMeshValidator is not running."); + /// + /// Call this once per frame from the UI thread to offer updated preview meshes to the validator. + /// The validator may or may not want them, depending on its current state. Even if it doesn't + /// want them right now, it will appreciate your politeness in offering, and do nothing. + /// It eventually will accept them, though, when it's ready. That's why you have to call on + /// every frame. + /// + /// The current state of the meshes. + /// All the vertices (in all meshes) that are being manipulated. + public void UpdateMeshes(Dictionary meshes, HashSet updatedVerts) + { + // It's an error to call this while not running. + AssertOrThrow.True(workerThread != null, + "Can't call UpdateMeshes when BackgroundMeshValidator is not running."); - lock (lockObject) { - if (state != State.WAITING_FOR_DATA) { - // Worker thread is busy and doesn't have an appetite for new data right now. - // Caller should try again later (as documented above). - return; + lock (lockObject) + { + if (state != State.WAITING_FOR_DATA) + { + // Worker thread is busy and doesn't have an appetite for new data right now. + // Caller should try again later (as documented above). + return; + } + // Take a snapshot of the preview meshes so we can work offline. + validationCopies = new List(meshes.Count()); + foreach (int meshId in meshes.Keys) + { + validationCopies.Add(meshes[meshId].Clone()); + } + this.updatedVerts = new HashSet(updatedVerts); + // Ok, now that we have data, we transition into the VALIDATING state. + state = State.VALIDATING; + // Poke the worker thread to tell it to wake up, because there's work to do. + Monitor.Pulse(lockObject); + } } - // Take a snapshot of the preview meshes so we can work offline. - validationCopies = new List(meshes.Count()); - foreach (int meshId in meshes.Keys) { - validationCopies.Add(meshes[meshId].Clone()); + + /// + /// Returns the last good state, that is the last state that passed mesh validation. + /// Since validation is asynchronous, this will always be a few frames behind the current state, + /// but shouldn't lag that far behind. + /// + /// The last valid state. + public Dictionary GetLastValidState() + { + lock (lockObject) + { + // IMPLEMENTATION WARNING: since we are returning this to the caller, we must guarantee that this + // is immutable. This is guaranteed because when we modify this, we replace it with a new Dictionary + // rather than modify it in-place. + return lastValidState; + } } - this.updatedVerts = new HashSet(updatedVerts); - // Ok, now that we have data, we transition into the VALIDATING state. - state = State.VALIDATING; - // Poke the worker thread to tell it to wake up, because there's work to do. - Monitor.Pulse(lockObject); - } - } - /// - /// Returns the last good state, that is the last state that passed mesh validation. - /// Since validation is asynchronous, this will always be a few frames behind the current state, - /// but shouldn't lag that far behind. - /// - /// The last valid state. - public Dictionary GetLastValidState() { - lock (lockObject) { - // IMPLEMENTATION WARNING: since we are returning this to the caller, we must guarantee that this - // is immutable. This is guaranteed because when we modify this, we replace it with a new Dictionary - // rather than modify it in-place. - return lastValidState; - } - } + /// + /// Worker thread main function. + /// + private void WorkerThreadMain() + { + // Note: yes, there is a performance penalty using a try/catch, but this runs on a background thread, + // and it's extremely useful for us to be able to get stack traces of things that went wrong here. + try + { + while (true) + { + // Try to get the next set of validation copies, waiting as necessary. + lock (lockObject) + { + // We must always check the state right after locking, because the main thread might be + // signaling us to quit. + if (state == State.QUITTING) return; - /// - /// Worker thread main function. - /// - private void WorkerThreadMain() { - // Note: yes, there is a performance penalty using a try/catch, but this runs on a background thread, - // and it's extremely useful for us to be able to get stack traces of things that went wrong here. - try { - while (true) { - // Try to get the next set of validation copies, waiting as necessary. - lock (lockObject) { - // We must always check the state right after locking, because the main thread might be - // signaling us to quit. - if (state == State.QUITTING) return; + // Indicate that we are sitting around waiting for new data to appear. + state = State.WAITING_FOR_DATA; + // Must use a while loop because Wait() does not guarantee to only unblock on a valid + // Pulse(). It can unblock spuriously at any time. + while (state == State.WAITING_FOR_DATA) + { + // Wait until the state changes. Monitor.Wait() temporarily releases the lock, then locks + // again once Monitor.Pulse() has been called. + Monitor.Wait(lockObject); + } + // If the main thread told us to quit, stop here. + if (state == State.QUITTING) return; + // Indicate that we are now working on validating the meshes. + state = State.VALIDATING; + } - // Indicate that we are sitting around waiting for new data to appear. - state = State.WAITING_FOR_DATA; - // Must use a while loop because Wait() does not guarantee to only unblock on a valid - // Pulse(). It can unblock spuriously at any time. - while (state == State.WAITING_FOR_DATA) { - // Wait until the state changes. Monitor.Wait() temporarily releases the lock, then locks - // again once Monitor.Pulse() has been called. - Monitor.Wait(lockObject); - } - // If the main thread told us to quit, stop here. - if (state == State.QUITTING) return; - // Indicate that we are now working on validating the meshes. - state = State.VALIDATING; - } + // Validate the copies. + // When done, update last valid state, etc. + Dictionary currentState = new Dictionary(); + bool allMeshesValid = true; + foreach (MMesh mesh in validationCopies) + { + // Get the original mesh as it is in the model (unmodified, and presumably valid). + MMesh originalMesh = model.GetMesh(mesh.id); + List updatedVertsForThisMesh = new List(); + foreach (VertexKey v in updatedVerts) + { + if (v.meshId == mesh.id) + { + updatedVertsForThisMesh.Add(mesh.GetVertex(v.vertexId)); + } + } + // Figure out which vertices are being updated + HashSet updatedVertIds = new HashSet(updatedVertsForThisMesh.Select(v => v.id)); + // Fix the mutated mesh, keeping track of duplicated vertices that were not fixed. + DisjointSet dupVerts; + MeshFixer.FixMutatedMesh(originalMesh, mesh, updatedVertIds, /* splitNonCoplanarFaces */ true, + /* mergeAdjacentCoplanarFaces */ true); - // Validate the copies. - // When done, update last valid state, etc. - Dictionary currentState = new Dictionary(); - bool allMeshesValid = true; - foreach (MMesh mesh in validationCopies) { - // Get the original mesh as it is in the model (unmodified, and presumably valid). - MMesh originalMesh = model.GetMesh(mesh.id); - List updatedVertsForThisMesh = new List(); - foreach (VertexKey v in updatedVerts) { - if (v.meshId == mesh.id) { - updatedVertsForThisMesh.Add(mesh.GetVertex(v.vertexId)); - } - } - // Figure out which vertices are being updated - HashSet updatedVertIds = new HashSet(updatedVertsForThisMesh.Select(v => v.id)); - // Fix the mutated mesh, keeping track of duplicated vertices that were not fixed. - DisjointSet dupVerts; - MeshFixer.FixMutatedMesh(originalMesh, mesh, updatedVertIds, /* splitNonCoplanarFaces */ true, - /* mergeAdjacentCoplanarFaces */ true); + if (MeshValidator.IsValidMesh(mesh, updatedVertIds)) + { + currentState[mesh.id] = mesh; + } + else + { + allMeshesValid = false; + break; + } + } - if (MeshValidator.IsValidMesh(mesh, updatedVertIds)) { - currentState[mesh.id] = mesh; - } else { - allMeshesValid = false; - break; + // Publish our findings. + lock (lockObject) + { + if (allMeshesValid) + { + lastValidState = currentState; + validity = Validity.VALID; + } + else + { + validity = Validity.INVALID; + } + } + } } - } - - // Publish our findings. - lock (lockObject) { - if (allMeshesValid) { - lastValidState = currentState; - validity = Validity.VALID; - } else { - validity = Validity.INVALID; + catch (Exception ex) + { + Debug.LogError(ex); } - } } - } catch (Exception ex) { - Debug.LogError(ex); - } } - } } \ No newline at end of file diff --git a/Assets/Scripts/tools/Deleter.cs b/Assets/Scripts/tools/Deleter.cs index efc6d6e4..3f61370e 100644 --- a/Assets/Scripts/tools/Deleter.cs +++ b/Assets/Scripts/tools/Deleter.cs @@ -19,456 +19,540 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.tools { - /// - /// Tool which handles deletion of meshes. - /// - public class Deleter : MonoBehaviour { - public ControllerMain controllerMain; - private PeltzerController peltzerController; - private Model model; - private Selector selector; - private AudioLibrary audioLibrary; - - /// - /// Whether we are currently deleting all hovered objects. - /// - public bool isDeleting { get; private set; } +namespace com.google.apps.peltzer.client.tools +{ /// - /// The set of meshes to delete when this deletion command finishes. + /// Tool which handles deletion of meshes. /// - private HashSet meshIdsToDelete = new HashSet(); - /// - /// When we last made a noise and buzzed because of a deletion. - /// - private float timeLastDeletionFeedbackPlayed; - /// - /// Leave some time between playing deletion feedback. - /// - private const float INTERVAL_BETWEEN_DELETION_FEEDBACKS = 0.5f; - /// - /// Whether we have shown the snap tooltip for this tool yet. (Show only once because there are no direct - /// snapping behaviors for Painter and Deleter). - /// - private bool snapTooltipShown = false; + public class Deleter : MonoBehaviour + { + public ControllerMain controllerMain; + private PeltzerController peltzerController; + private Model model; + private Selector selector; + private AudioLibrary audioLibrary; + + /// + /// Whether we are currently deleting all hovered objects. + /// + public bool isDeleting { get; private set; } + /// + /// The set of meshes to delete when this deletion command finishes. + /// + private HashSet meshIdsToDelete = new HashSet(); + /// + /// When we last made a noise and buzzed because of a deletion. + /// + private float timeLastDeletionFeedbackPlayed; + /// + /// Leave some time between playing deletion feedback. + /// + private const float INTERVAL_BETWEEN_DELETION_FEEDBACKS = 0.5f; + /// + /// Whether we have shown the snap tooltip for this tool yet. (Show only once because there are no direct + /// snapping behaviors for Painter and Deleter). + /// + private bool snapTooltipShown = false; + + /// + /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. + /// As such, this setup method must be called before the tool is used for it to have a valid state. + /// + public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, + Selector selector, AudioLibrary audioLibrary) + { + this.model = model; + this.controllerMain = controllerMain; + this.peltzerController = peltzerController; + this.selector = selector; + this.audioLibrary = audioLibrary; + controllerMain.ControllerActionHandler += ControllerEventHandler; + } - /// - /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. - /// As such, this setup method must be called before the tool is used for it to have a valid state. - /// - public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, - Selector selector, AudioLibrary audioLibrary) { - this.model = model; - this.controllerMain = controllerMain; - this.peltzerController = peltzerController; - this.selector = selector; - this.audioLibrary = audioLibrary; - controllerMain.ControllerActionHandler += ControllerEventHandler; - } + /// + /// If we are in delete mode, try and delete all hovered meshes. + /// + public void Update() + { + if (!PeltzerController.AcquireIfNecessary(ref peltzerController) || + !(peltzerController.mode == ControllerMode.delete || peltzerController.mode == ControllerMode.deletePart)) + { + return; + } - /// - /// If we are in delete mode, try and delete all hovered meshes. - /// - public void Update() { - if (!PeltzerController.AcquireIfNecessary(ref peltzerController) || - !(peltzerController.mode == ControllerMode.delete || peltzerController.mode == ControllerMode.deletePart)) { - return; - } - - if (peltzerController.mode == ControllerMode.deletePart) { - selector.UpdateInactive(Selector.FACES_EDGES_AND_VERTICES); - selector.SelectAtPosition(peltzerController.LastPositionModel, Selector.FACES_EDGES_AND_VERTICES); - } else { - // Update the position of the selector even if we aren't deleting yet so the selector can detect which meshes to - // delete. If we aren't deleting yet we want to hide meshes and show their highlights. - selector.SelectMeshAtPosition(peltzerController.LastPositionModel, Selector.MESHES_ONLY); - - foreach (int meshId in selector.hoverMeshes) { - PeltzerMain.Instance.highlightUtils.SetMeshStyleToDelete(meshId); + if (peltzerController.mode == ControllerMode.deletePart) + { + selector.UpdateInactive(Selector.FACES_EDGES_AND_VERTICES); + selector.SelectAtPosition(peltzerController.LastPositionModel, Selector.FACES_EDGES_AND_VERTICES); + } + else + { + // Update the position of the selector even if we aren't deleting yet so the selector can detect which meshes to + // delete. If we aren't deleting yet we want to hide meshes and show their highlights. + selector.SelectMeshAtPosition(peltzerController.LastPositionModel, Selector.MESHES_ONLY); + + foreach (int meshId in selector.hoverMeshes) + { + PeltzerMain.Instance.highlightUtils.SetMeshStyleToDelete(meshId); + } + + if (!isDeleting || selector.hoverMeshes.Count == 0) + { + return; + } + + // Stop rendering each hovered mesh, and mark it for deletion. + int[] hoveredKeys = new int[selector.hoverMeshes.Count]; + selector.hoverMeshes.CopyTo(hoveredKeys, 0); + foreach (int meshId in hoveredKeys) + { + if (meshIdsToDelete.Add(meshId)) + { + model.MarkMeshForDeletion(meshId); + PeltzerMain.Instance.highlightUtils.TurnOffMesh(meshId); + if (Time.time - timeLastDeletionFeedbackPlayed > INTERVAL_BETWEEN_DELETION_FEEDBACKS) + { + timeLastDeletionFeedbackPlayed = Time.time; + audioLibrary.PlayClip(audioLibrary.deleteSound); + peltzerController.TriggerHapticFeedback(); + } + } + } + } } - if (!isDeleting || selector.hoverMeshes.Count == 0) { - return; + /// + /// Whether this matches the pattern of a 'start deleting' event. + /// + /// The controller event arguments. + /// True if this is a start deleting event, false otherwise. + private bool IsStartDeletingEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN; } - // Stop rendering each hovered mesh, and mark it for deletion. - int[] hoveredKeys = new int[selector.hoverMeshes.Count]; - selector.hoverMeshes.CopyTo(hoveredKeys, 0); - foreach (int meshId in hoveredKeys) { - if (meshIdsToDelete.Add(meshId)) { - model.MarkMeshForDeletion(meshId); - PeltzerMain.Instance.highlightUtils.TurnOffMesh(meshId); - if (Time.time - timeLastDeletionFeedbackPlayed > INTERVAL_BETWEEN_DELETION_FEEDBACKS) { - timeLastDeletionFeedbackPlayed = Time.time; - audioLibrary.PlayClip(audioLibrary.deleteSound); - peltzerController.TriggerHapticFeedback(); + /// + /// Whether this matches the pattern of a 'stop deleting' event. + /// + /// The controller event arguments. + /// True if this is a stop deleting event, false otherwise. + private bool IsFinishDeletingEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.UP; + } + + /// + /// An event handler that listens for controller input and delegates accordingly. + /// + /// The sender of the controller event. + /// The controller event arguments. + private void ControllerEventHandler(object sender, ControllerEventArgs args) + { + if (peltzerController.mode == ControllerMode.delete) + { + if (IsStartDeletingEvent(args)) + { + StartDeleting(); + } + else if (IsFinishDeletingEvent(args)) + { + FinishDeleting(); + } + else if (IsSetSnapTriggerTooltipEvent(args) && !snapTooltipShown) + { + // Show tool tip about the snap trigger. + PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); + snapTooltipShown = true; + } + } + else if (peltzerController.mode == ControllerMode.deletePart) + { + if (IsStartDeletingEvent(args)) + { + DeleteAPart(); + } } - } } - } - } - /// - /// Whether this matches the pattern of a 'start deleting' event. - /// - /// The controller event arguments. - /// True if this is a start deleting event, false otherwise. - private bool IsStartDeletingEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN; - } + private void DeleteAPart() + { + if (selector.hoverFace != null) + { + DeleteFace(selector.hoverFace); + } + else if (selector.hoverVertex != null) + { + DeleteVertex(selector.hoverVertex); + } + else if (selector.hoverEdge != null) + { + DeleteEdge(selector.hoverEdge); + } + } - /// - /// Whether this matches the pattern of a 'stop deleting' event. - /// - /// The controller event arguments. - /// True if this is a stop deleting event, false otherwise. - private bool IsFinishDeletingEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.UP; - } + private void DeleteVertex(VertexKey vertexKey) + { + MMesh mesh = model.GetMesh(vertexKey.meshId).Clone(); + MMesh.GeometryOperation deleteVertOp = mesh.StartOperation(); + + // Keep track of the starting vert of each face's list of retained verts. We use this to determine which order + // to join the lists in. + Dictionary startVertToFace = new Dictionary(); + Dictionary> faceToRetainedVerts = new Dictionary>(); + + int nextFaceId = -1; + foreach (int faceId in mesh.reverseTable[vertexKey.vertexId]) + { + if (nextFaceId == -1) + { + nextFaceId = faceId; + } + Face f = mesh.GetFace(faceId); + for (int i = 0; i < f.vertexIds.Count; i++) + { + if (f.vertexIds[i] == vertexKey.vertexId) + { + List retainedVerts = new List(); + int startIndex = (i + 1) % f.vertexIds.Count; + startVertToFace[f.vertexIds[startIndex]] = faceId; + retainedVerts.Add(f.vertexIds[startIndex]); + while (f.vertexIds[(startIndex + 1) % f.vertexIds.Count] != vertexKey.vertexId) + { + startIndex = (startIndex + 1) % f.vertexIds.Count; + retainedVerts.Add(f.vertexIds[startIndex]); + } + faceToRetainedVerts[faceId] = retainedVerts; + } + } + } - /// - /// An event handler that listens for controller input and delegates accordingly. - /// - /// The sender of the controller event. - /// The controller event arguments. - private void ControllerEventHandler(object sender, ControllerEventArgs args) { - if (peltzerController.mode == ControllerMode.delete) { - if (IsStartDeletingEvent(args)) { - StartDeleting(); - } else if (IsFinishDeletingEvent(args)) { - FinishDeleting(); - } else if (IsSetSnapTriggerTooltipEvent(args) && !snapTooltipShown) { - // Show tool tip about the snap trigger. - PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); - snapTooltipShown = true; - } - } else if (peltzerController.mode == ControllerMode.deletePart) { - if (IsStartDeletingEvent(args)) { - DeleteAPart(); - } - } - } + List newFaceVertexIds = new List(); + HashSet faces = new HashSet(mesh.reverseTable[vertexKey.vertexId]); - private void DeleteAPart() { - if (selector.hoverFace != null) { - DeleteFace(selector.hoverFace); - } else if (selector.hoverVertex != null) { - DeleteVertex(selector.hoverVertex); - } else if (selector.hoverEdge != null) { - DeleteEdge(selector.hoverEdge); - } - } + while (faces.Count > 0) + { + List retainedVerts = faceToRetainedVerts[nextFaceId]; + newFaceVertexIds.AddRange(retainedVerts); + faces.Remove(nextFaceId); + deleteVertOp.DeleteFace(nextFaceId); + nextFaceId = startVertToFace[retainedVerts[retainedVerts.Count - 1]]; + } + + deleteVertOp.DeleteVertex(vertexKey.vertexId); + deleteVertOp.AddFace(newFaceVertexIds, mesh.GetFace(nextFaceId).properties); + deleteVertOp.Commit(); - private void DeleteVertex(VertexKey vertexKey) { - MMesh mesh = model.GetMesh(vertexKey.meshId).Clone(); - MMesh.GeometryOperation deleteVertOp = mesh.StartOperation(); - - // Keep track of the starting vert of each face's list of retained verts. We use this to determine which order - // to join the lists in. - Dictionary startVertToFace = new Dictionary(); - Dictionary> faceToRetainedVerts = new Dictionary>(); - - int nextFaceId = -1; - foreach (int faceId in mesh.reverseTable[vertexKey.vertexId]) { - if (nextFaceId == -1) { - nextFaceId = faceId; + model.ApplyCommand(new ReplaceMeshCommand(mesh.id, mesh)); } - Face f = mesh.GetFace(faceId); - for (int i = 0; i < f.vertexIds.Count; i++) { - if (f.vertexIds[i] == vertexKey.vertexId) { - List retainedVerts = new List(); - int startIndex = (i + 1) % f.vertexIds.Count; - startVertToFace[f.vertexIds[startIndex]] = faceId; - retainedVerts.Add(f.vertexIds[startIndex]); - while (f.vertexIds[(startIndex + 1) % f.vertexIds.Count] != vertexKey.vertexId) { - startIndex = (startIndex + 1) % f.vertexIds.Count; - retainedVerts.Add(f.vertexIds[startIndex]); + + private int FindLastEdgeVertexInFace(EdgeKey edge, Face face) + { + int face1EdgeKey1Index = -1; + for (int i = 0; i < face.vertexIds.Count; i++) + { + if (face.vertexIds[i] == edge.vertexId1) + { + if (face.vertexIds[(i + 1) % face.vertexIds.Count] == edge.vertexId2) + { + return (i + 1) % face.vertexIds.Count; + } + else + { + return i; + } + } } - faceToRetainedVerts[faceId] = retainedVerts; - } + return -1; } - } - - List newFaceVertexIds = new List(); - HashSet faces = new HashSet(mesh.reverseTable[vertexKey.vertexId]); - - while (faces.Count > 0) { - List retainedVerts = faceToRetainedVerts[nextFaceId]; - newFaceVertexIds.AddRange(retainedVerts); - faces.Remove(nextFaceId); - deleteVertOp.DeleteFace(nextFaceId); - nextFaceId = startVertToFace[retainedVerts[retainedVerts.Count - 1]]; - } - - deleteVertOp.DeleteVertex(vertexKey.vertexId); - deleteVertOp.AddFace(newFaceVertexIds, mesh.GetFace(nextFaceId).properties); - deleteVertOp.Commit(); - - model.ApplyCommand(new ReplaceMeshCommand(mesh.id, mesh)); - } - private int FindLastEdgeVertexInFace(EdgeKey edge, Face face) { - int face1EdgeKey1Index = -1; - for (int i = 0; i < face.vertexIds.Count; i++) { - if (face.vertexIds[i] == edge.vertexId1) { - if (face.vertexIds[(i + 1) % face.vertexIds.Count] == edge.vertexId2) { - return (i + 1) % face.vertexIds.Count; - } - else { - return i; - } - } - } - return -1; - } - - private void DeleteEdge(EdgeKey edgeKey) { - // To delete an edge we will: - // 1) Find the two faces that are incident to the edge (we can guarantee there will be exactly two*) - // 1a) Unless the edge is intruding within a single face. In which case we deal with the special case by removing - // the intruding edge. - // 2) In each of the two faces, find where the pair of vertices specified by the edgekey is within its vertex - // ids. Store the index of the second one of these to occur. - // 3) Create a new vertex list for the new face we need to replace the old ones with. - // 4) For each adjacent face add the vertexid at the index previously stored (which is one of the ones in the - // edgekey) and then from that index continue adding vertex ids until we hit another vert in the edgekey. That - // vertex is NOT added. - // 5) There's now a vertex list for the new face, so we add this along with the face properties from one of the - // two adjacent faces (choosing arbitrarily). - - MMesh mesh = model.GetMesh(edgeKey.meshId).Clone(); - - // Step 1: Find the two faces incident to the edge. - Face face1 = null; - Face face2 = null; - FindIncidentFaces(edgeKey, out face1, out face2); - if (face1 != null && face2 == null) { - //Special case - deleting an internal edge to the face. Delete the vertex that doesn't border other verts in the - //face - this will be shown in the vert list by a repeated vert in the face ABCDCE. To fix this, we delete the - //DC portion of the sequence resulting in ABCE. - //We do this by iterating through our vert list looking for a vert that is in the edgekey and has edgekey verts - //on either side - in our example the D. Then we construct a new face starting with the next element, but cut - //it off two vertices earlier - so CEAB/ - MMesh.GeometryOperation deleteInternalEdgeOperation = mesh.StartOperation(); - int vertCount = face1.vertexIds.Count; - // (x - 1) % count doesn't work, but (x + count - 1) % count is mathematically equivalent - int modulusMinusOne = vertCount - 1; - int startVert = -1; - // Find the point - for (int i = 0; i < vertCount; i++) { - if (edgeKey.ContainsVertex(face1.vertexIds[i])) { - if (edgeKey.ContainsVertex(face1.vertexIds[(i + 1) % vertCount]) - && edgeKey.ContainsVertex(face1.vertexIds[(i + modulusMinusOne) % vertCount])) { - startVert = (i + 1) % vertCount; + private void DeleteEdge(EdgeKey edgeKey) + { + // To delete an edge we will: + // 1) Find the two faces that are incident to the edge (we can guarantee there will be exactly two*) + // 1a) Unless the edge is intruding within a single face. In which case we deal with the special case by removing + // the intruding edge. + // 2) In each of the two faces, find where the pair of vertices specified by the edgekey is within its vertex + // ids. Store the index of the second one of these to occur. + // 3) Create a new vertex list for the new face we need to replace the old ones with. + // 4) For each adjacent face add the vertexid at the index previously stored (which is one of the ones in the + // edgekey) and then from that index continue adding vertex ids until we hit another vert in the edgekey. That + // vertex is NOT added. + // 5) There's now a vertex list for the new face, so we add this along with the face properties from one of the + // two adjacent faces (choosing arbitrarily). + + MMesh mesh = model.GetMesh(edgeKey.meshId).Clone(); + + // Step 1: Find the two faces incident to the edge. + Face face1 = null; + Face face2 = null; + FindIncidentFaces(edgeKey, out face1, out face2); + if (face1 != null && face2 == null) + { + //Special case - deleting an internal edge to the face. Delete the vertex that doesn't border other verts in the + //face - this will be shown in the vert list by a repeated vert in the face ABCDCE. To fix this, we delete the + //DC portion of the sequence resulting in ABCE. + //We do this by iterating through our vert list looking for a vert that is in the edgekey and has edgekey verts + //on either side - in our example the D. Then we construct a new face starting with the next element, but cut + //it off two vertices earlier - so CEAB/ + MMesh.GeometryOperation deleteInternalEdgeOperation = mesh.StartOperation(); + int vertCount = face1.vertexIds.Count; + // (x - 1) % count doesn't work, but (x + count - 1) % count is mathematically equivalent + int modulusMinusOne = vertCount - 1; + int startVert = -1; + // Find the point + for (int i = 0; i < vertCount; i++) + { + if (edgeKey.ContainsVertex(face1.vertexIds[i])) + { + if (edgeKey.ContainsVertex(face1.vertexIds[(i + 1) % vertCount]) + && edgeKey.ContainsVertex(face1.vertexIds[(i + modulusMinusOne) % vertCount])) + { + startVert = (i + 1) % vertCount; + } + } + } + List replacementFaceVertIds = new List(); + for (int i = 0; i < vertCount - 2; i++) + { + replacementFaceVertIds.Add(face1.vertexIds[(startVert + i) % vertCount]); + } + deleteInternalEdgeOperation.DeleteFace(face1.id); + deleteInternalEdgeOperation.AddFace(replacementFaceVertIds, face1.properties); + deleteInternalEdgeOperation.Commit(); + + MMesh.GeometryOperation cleanupOp = mesh.StartOperation(); + if (mesh.reverseTable[edgeKey.vertexId1].Count == 0) + { + cleanupOp.DeleteVertex(edgeKey.vertexId1); + } + if (mesh.reverseTable[edgeKey.vertexId2].Count == 0) + { + cleanupOp.DeleteVertex(edgeKey.vertexId2); + } + cleanupOp.Commit(); + if (MeshValidator.IsValidMesh(mesh, new HashSet(replacementFaceVertIds))) + { + model.ApplyCommand(new ReplaceMeshCommand(mesh.id, mesh)); + } + return; } - } - } - List replacementFaceVertIds = new List(); - for (int i = 0; i < vertCount - 2; i++) { - replacementFaceVertIds.Add(face1.vertexIds[(startVert + i) % vertCount]); - } - deleteInternalEdgeOperation.DeleteFace(face1.id); - deleteInternalEdgeOperation.AddFace(replacementFaceVertIds, face1.properties); - deleteInternalEdgeOperation.Commit(); - MMesh.GeometryOperation cleanupOp = mesh.StartOperation(); - if (mesh.reverseTable[edgeKey.vertexId1].Count == 0) { - cleanupOp.DeleteVertex(edgeKey.vertexId1); - } - if (mesh.reverseTable[edgeKey.vertexId2].Count == 0) { - cleanupOp.DeleteVertex(edgeKey.vertexId2); - } - cleanupOp.Commit(); - if (MeshValidator.IsValidMesh(mesh, new HashSet(replacementFaceVertIds))) { - model.ApplyCommand(new ReplaceMeshCommand(mesh.id, mesh)); - } - return; - } - - MMesh.GeometryOperation edgeDeletionOperation = mesh.StartOperation(); - edgeDeletionOperation.DeleteFace(face1.id); - edgeDeletionOperation.DeleteFace(face2.id); - - int face1EdgeKey1Index = FindLastEdgeVertexInFace(edgeKey, face1); - if (face1EdgeKey1Index == -1) return; - int face2EdgeKeyIndex = FindLastEdgeVertexInFace(edgeKey, face2); - if (face2EdgeKeyIndex == -1) return; - - List vertexIds = new List(); - vertexIds.Add(face1.vertexIds[face1EdgeKey1Index]); - while (!edgeKey.ContainsVertex(face1.vertexIds[(face1EdgeKey1Index + 1) % face1.vertexIds.Count])) { - face1EdgeKey1Index = (face1EdgeKey1Index + 1) % face1.vertexIds.Count; - vertexIds.Add(face1.vertexIds[face1EdgeKey1Index]); - } - vertexIds.Add(face2.vertexIds[face2EdgeKeyIndex]); - while (!edgeKey.ContainsVertex(face2.vertexIds[(face2EdgeKeyIndex + 1) % face2.vertexIds.Count])) { - face2EdgeKeyIndex = (face2EdgeKeyIndex + 1) % face2.vertexIds.Count; - vertexIds.Add(face2.vertexIds[face2EdgeKeyIndex]); - } - - edgeDeletionOperation.AddFace(vertexIds, face1.properties); - edgeDeletionOperation.Commit(); - if (MeshValidator.IsValidMesh(mesh, new HashSet(vertexIds))) { - model.ApplyCommand(new ReplaceMeshCommand(mesh.id, mesh)); - } - return; - } + MMesh.GeometryOperation edgeDeletionOperation = mesh.StartOperation(); + edgeDeletionOperation.DeleteFace(face1.id); + edgeDeletionOperation.DeleteFace(face2.id); + + int face1EdgeKey1Index = FindLastEdgeVertexInFace(edgeKey, face1); + if (face1EdgeKey1Index == -1) return; + int face2EdgeKeyIndex = FindLastEdgeVertexInFace(edgeKey, face2); + if (face2EdgeKeyIndex == -1) return; + + List vertexIds = new List(); + vertexIds.Add(face1.vertexIds[face1EdgeKey1Index]); + while (!edgeKey.ContainsVertex(face1.vertexIds[(face1EdgeKey1Index + 1) % face1.vertexIds.Count])) + { + face1EdgeKey1Index = (face1EdgeKey1Index + 1) % face1.vertexIds.Count; + vertexIds.Add(face1.vertexIds[face1EdgeKey1Index]); + } + vertexIds.Add(face2.vertexIds[face2EdgeKeyIndex]); + while (!edgeKey.ContainsVertex(face2.vertexIds[(face2EdgeKeyIndex + 1) % face2.vertexIds.Count])) + { + face2EdgeKeyIndex = (face2EdgeKeyIndex + 1) % face2.vertexIds.Count; + vertexIds.Add(face2.vertexIds[face2EdgeKeyIndex]); + } - // Finds the two faces incident to a given edge. - private void FindIncidentFaces(EdgeKey edgeKey, out Face face1, out Face face2) { - MMesh mesh = model.GetMesh(edgeKey.meshId); - face1 = null; - face2 = null; - foreach (Face face in mesh.GetFaces()) { - if (face.vertexIds.Contains(edgeKey.vertexId1) && face.vertexIds.Contains(edgeKey.vertexId2)) { // Could optimise this to be one pass - if (face1 == null) { - face1 = face; - } else { - face2 = face; + edgeDeletionOperation.AddFace(vertexIds, face1.properties); + edgeDeletionOperation.Commit(); + if (MeshValidator.IsValidMesh(mesh, new HashSet(vertexIds))) + { + model.ApplyCommand(new ReplaceMeshCommand(mesh.id, mesh)); + } return; - } } - } - } - private void DeleteFace(FaceKey faceKey) { - MMesh mesh = model.GetMesh(faceKey.meshId).Clone(); - MMesh.GeometryOperation deleteFaceOp = mesh.StartOperation(); - Face faceToDelete = mesh.GetFace(faceKey.faceId); - - Vector3 avgLogMeshSpace = Vector3.zero; - foreach (int vertexId in faceToDelete.vertexIds) { - avgLogMeshSpace += mesh.VertexPositionInMeshCoords(vertexId); - } - avgLogMeshSpace /= faceToDelete.vertexIds.Count; - - Vertex mergedVert = deleteFaceOp.AddVertexMeshSpace(avgLogMeshSpace); - - List facesToDelete = new List(); - facesToDelete.Add(faceToDelete.id); - List facesToAdd = new List(); - - foreach (Face f in mesh.GetFaces()) { - bool found = false; - foreach (int vertexId in faceToDelete.vertexIds) { - if (f.vertexIds.Contains(vertexId)) { - found = true; - break; - } + // Finds the two faces incident to a given edge. + private void FindIncidentFaces(EdgeKey edgeKey, out Face face1, out Face face2) + { + MMesh mesh = model.GetMesh(edgeKey.meshId); + face1 = null; + face2 = null; + foreach (Face face in mesh.GetFaces()) + { + if (face.vertexIds.Contains(edgeKey.vertexId1) && face.vertexIds.Contains(edgeKey.vertexId2)) + { // Could optimise this to be one pass + if (face1 == null) + { + face1 = face; + } + else + { + face2 = face; + return; + } + } + } } - if (!found) continue; + private void DeleteFace(FaceKey faceKey) + { + MMesh mesh = model.GetMesh(faceKey.meshId).Clone(); + MMesh.GeometryOperation deleteFaceOp = mesh.StartOperation(); + Face faceToDelete = mesh.GetFace(faceKey.faceId); - - facesToDelete.Add(f.id); - - List verts = new List(); - List vertLocs = new List(); - - bool added = false; - foreach (int vertexId in f.vertexIds) { - bool found2 = false; - foreach (int vertexId2 in faceToDelete.vertexIds) { - if (vertexId == vertexId2) { - found2 = true; - break; + Vector3 avgLogMeshSpace = Vector3.zero; + foreach (int vertexId in faceToDelete.vertexIds) + { + avgLogMeshSpace += mesh.VertexPositionInMeshCoords(vertexId); } - } - - if (!found2) { - verts.Add(vertexId); - vertLocs.Add(deleteFaceOp.GetCurrentVertexPositionMeshSpace(vertexId)); - } else { - // Open question: Is this what we want to do if multiple verts from the deleted face are in another face? Can we ensure they would always have been in order and this is safe? - // What about <3-gons generated. - if (added) { - continue; + avgLogMeshSpace /= faceToDelete.vertexIds.Count; + + Vertex mergedVert = deleteFaceOp.AddVertexMeshSpace(avgLogMeshSpace); + + List facesToDelete = new List(); + facesToDelete.Add(faceToDelete.id); + List facesToAdd = new List(); + + foreach (Face f in mesh.GetFaces()) + { + bool found = false; + foreach (int vertexId in faceToDelete.vertexIds) + { + if (f.vertexIds.Contains(vertexId)) + { + found = true; + break; + } + } + + if (!found) continue; + + + facesToDelete.Add(f.id); + + List verts = new List(); + List vertLocs = new List(); + + bool added = false; + foreach (int vertexId in f.vertexIds) + { + bool found2 = false; + foreach (int vertexId2 in faceToDelete.vertexIds) + { + if (vertexId == vertexId2) + { + found2 = true; + break; + } + } + + if (!found2) + { + verts.Add(vertexId); + vertLocs.Add(deleteFaceOp.GetCurrentVertexPositionMeshSpace(vertexId)); + } + else + { + // Open question: Is this what we want to do if multiple verts from the deleted face are in another face? Can we ensure they would always have been in order and this is safe? + // What about <3-gons generated. + if (added) + { + continue; + } + + added = true; + verts.Add(mergedVert.id); + vertLocs.Add(mergedVert.loc); + } + } + deleteFaceOp.AddFace(verts, f.properties); } - added = true; - verts.Add(mergedVert.id); - vertLocs.Add(mergedVert.loc); - } - } - deleteFaceOp.AddFace(verts, f.properties); - } - - foreach (int vertexId in faceToDelete.vertexIds) { - deleteFaceOp.DeleteVertex(vertexId); - } + foreach (int vertexId in faceToDelete.vertexIds) + { + deleteFaceOp.DeleteVertex(vertexId); + } - foreach (int f in facesToDelete) { - deleteFaceOp.DeleteFace(f); - } + foreach (int f in facesToDelete) + { + deleteFaceOp.DeleteFace(f); + } - deleteFaceOp.Commit(); + deleteFaceOp.Commit(); - model.ApplyCommand(new ReplaceMeshCommand(mesh.id, mesh)); - } + model.ApplyCommand(new ReplaceMeshCommand(mesh.id, mesh)); + } - private void StartDeleting() { - isDeleting = true; - } + private void StartDeleting() + { + isDeleting = true; + } - private void FinishDeleting() { - isDeleting = false; + private void FinishDeleting() + { + isDeleting = false; - selector.DeselectAll(); + selector.DeselectAll(); - List deleteCommands = new List(); - foreach (int meshId in meshIdsToDelete) { - deleteCommands.Add(new DeleteMeshCommand(meshId)); - } + List deleteCommands = new List(); + foreach (int meshId in meshIdsToDelete) + { + deleteCommands.Add(new DeleteMeshCommand(meshId)); + } - if (deleteCommands.Count > 0) { - Command compositeCommand = new CompositeCommand(deleteCommands); - model.ApplyCommand(compositeCommand); + if (deleteCommands.Count > 0) + { + Command compositeCommand = new CompositeCommand(deleteCommands); + model.ApplyCommand(compositeCommand); - } + } - meshIdsToDelete.Clear(); - } + meshIdsToDelete.Clear(); + } - private static bool IsSetSnapTriggerTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.LIGHT_DOWN; - } + private static bool IsSetSnapTriggerTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.LIGHT_DOWN; + } - /// - /// Cancel any deletions that have been performed in the current operation. - /// - public bool CancelDeletionsSoFar() { - bool anythingToDo = meshIdsToDelete.Count > 0; - foreach (int meshId in meshIdsToDelete) { - model.UnmarkMeshForDeletion(meshId); - } - meshIdsToDelete.Clear(); - return anythingToDo; - } + /// + /// Cancel any deletions that have been performed in the current operation. + /// + public bool CancelDeletionsSoFar() + { + bool anythingToDo = meshIdsToDelete.Count > 0; + foreach (int meshId in meshIdsToDelete) + { + model.UnmarkMeshForDeletion(meshId); + } + meshIdsToDelete.Clear(); + return anythingToDo; + } - // Test method. - public void TriggerUpdateForTest() { - Update(); - } + // Test method. + public void TriggerUpdateForTest() + { + Update(); + } - // This function returns a point which is a projection from a point to a plane. - public static Vector3 ProjectPointOnPlane(Vector3 planeNormal, Vector3 planePoint, Vector3 point) { - // First calculate the distance from the point to the plane: - float distance = Vector3.Dot(planeNormal.normalized, (point - planePoint)); + // This function returns a point which is a projection from a point to a plane. + public static Vector3 ProjectPointOnPlane(Vector3 planeNormal, Vector3 planePoint, Vector3 point) + { + // First calculate the distance from the point to the plane: + float distance = Vector3.Dot(planeNormal.normalized, (point - planePoint)); - // Reverse the sign of the distance. - distance *= -1; + // Reverse the sign of the distance. + distance *= -1; - // Get a translation vector. - Vector3 translationVector = planeNormal.normalized * distance; + // Get a translation vector. + Vector3 translationVector = planeNormal.normalized * distance; - // Translate the point to form a projection - return point + translationVector; + // Translate the point to form a projection + return point + translationVector; + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/tools/Extruder.cs b/Assets/Scripts/tools/Extruder.cs index 6fdab171..7b181827 100644 --- a/Assets/Scripts/tools/Extruder.cs +++ b/Assets/Scripts/tools/Extruder.cs @@ -23,467 +23,549 @@ using com.google.apps.peltzer.client.model.render; using com.google.apps.peltzer.client.tools.utils; -namespace com.google.apps.peltzer.client.tools { - /// - /// A tool responsible for extruding faces of meshes in the scene. Implemented as MonoBehaviour so we can have an - /// Update() loop. - /// An extrusion is composed of grabbing a face, (potentially) pulling out the face, and releasing. The face - /// itself is moved, and new faces are created to link the moved face back to the mesh. - /// - /// - public class Extruder : MonoBehaviour { - // TODO(bug): refactor this to use background mesh validation like Reshaper. - - public ControllerMain controllerMain; +namespace com.google.apps.peltzer.client.tools +{ /// - /// A reference to a controller capable of issuing extrude commands. + /// A tool responsible for extruding faces of meshes in the scene. Implemented as MonoBehaviour so we can have an + /// Update() loop. + /// An extrusion is composed of grabbing a face, (potentially) pulling out the face, and releasing. The face + /// itself is moved, and new faces are created to link the moved face back to the mesh. + /// /// - private PeltzerController peltzerController; - /// - /// A reference to the overall model being built. - /// - private Model model; - /// - /// Selector for detecting which face is hovered or selected. - /// - private Selector selector; - /// - /// Library for playing sounds. - /// - private AudioLibrary audioLibrary; - /// - /// The controller position in model space where the extrusion began. - /// - private Vector3 extrusionBeginPosition; - /// - /// The controller orientation in model space where the extrusion began. - /// - private Quaternion extrusionBeginOrientation; - /// - /// In-flight extrusions. - /// - private List extrusions = new List(); - /// - /// Temporary re-paint commands for in-flight extrusions. - /// - private List temporaryHeldFaceMaterialCommands = new List(); - - private WorldSpace worldSpace; - - // Detection for trigger down & straight back up, vs trigger down and hold -- either of which - // begins an extrusion. - private bool triggerUpToRelease; - private float triggerDownTime; - private bool waitingToDetermineReleaseType; - - /// - /// Used to determine if we should show the snap tooltip or not. Don't show the tooltip if the user already - /// showed enough knowledge of how to snap. - /// - private int completedSnaps = 0; - private const int SNAP_KNOW_HOW_COUNT = 3; - - /// - /// Do we snap to the face normal and the grid? - /// - private bool isSnapping = false; + public class Extruder : MonoBehaviour + { + // TODO(bug): refactor this to use background mesh validation like Reshaper. + + public ControllerMain controllerMain; + /// + /// A reference to a controller capable of issuing extrude commands. + /// + private PeltzerController peltzerController; + /// + /// A reference to the overall model being built. + /// + private Model model; + /// + /// Selector for detecting which face is hovered or selected. + /// + private Selector selector; + /// + /// Library for playing sounds. + /// + private AudioLibrary audioLibrary; + /// + /// The controller position in model space where the extrusion began. + /// + private Vector3 extrusionBeginPosition; + /// + /// The controller orientation in model space where the extrusion began. + /// + private Quaternion extrusionBeginOrientation; + /// + /// In-flight extrusions. + /// + private List extrusions = new List(); + /// + /// Temporary re-paint commands for in-flight extrusions. + /// + private List temporaryHeldFaceMaterialCommands = new List(); + + private WorldSpace worldSpace; + + // Detection for trigger down & straight back up, vs trigger down and hold -- either of which + // begins an extrusion. + private bool triggerUpToRelease; + private float triggerDownTime; + private bool waitingToDetermineReleaseType; + + /// + /// Used to determine if we should show the snap tooltip or not. Don't show the tooltip if the user already + /// showed enough knowledge of how to snap. + /// + private int completedSnaps = 0; + private const int SNAP_KNOW_HOW_COUNT = 3; + + /// + /// Do we snap to the face normal and the grid? + /// + private bool isSnapping = false; + + /// + /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. + /// As such, this setup method must be called before the tool is used for it to have a valid state. + /// + public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, + PaletteController paletteController, Selector selector, AudioLibrary audioLibrary, WorldSpace worldSpace) + { + this.model = model; + this.controllerMain = controllerMain; + this.peltzerController = peltzerController; + this.selector = selector; + this.audioLibrary = audioLibrary; + this.worldSpace = worldSpace; + controllerMain.ControllerActionHandler += ControllerEventHandler; + } - /// - /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. - /// As such, this setup method must be called before the tool is used for it to have a valid state. - /// - public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, - PaletteController paletteController, Selector selector, AudioLibrary audioLibrary, WorldSpace worldSpace) { - this.model = model; - this.controllerMain = controllerMain; - this.peltzerController = peltzerController; - this.selector = selector; - this.audioLibrary = audioLibrary; - this.worldSpace = worldSpace; - controllerMain.ControllerActionHandler += ControllerEventHandler; - } + private ExtrusionOperation.ExtrusionParams BuildExtrusionParams() + { + // Note: ExtrusionParams is a struct, so this is stack allocated and doesn't generate garbage. + ExtrusionOperation.ExtrusionParams extrusionParams = new ExtrusionOperation.ExtrusionParams(); + // If we are snapping or block mode is on, lock extrusion to the face's normal. + extrusionParams.lockToNormal = isSnapping || peltzerController.isBlockMode; + extrusionParams.translationModel = peltzerController.LastPositionModel - extrusionBeginPosition; + extrusionParams.rotationPivotModel = peltzerController.LastPositionModel; + extrusionParams.rotationModel = + peltzerController.LastRotationModel * Quaternion.Inverse(extrusionBeginOrientation); + return extrusionParams; + } - private ExtrusionOperation.ExtrusionParams BuildExtrusionParams() { - // Note: ExtrusionParams is a struct, so this is stack allocated and doesn't generate garbage. - ExtrusionOperation.ExtrusionParams extrusionParams = new ExtrusionOperation.ExtrusionParams(); - // If we are snapping or block mode is on, lock extrusion to the face's normal. - extrusionParams.lockToNormal = isSnapping || peltzerController.isBlockMode; - extrusionParams.translationModel = peltzerController.LastPositionModel - extrusionBeginPosition; - extrusionParams.rotationPivotModel = peltzerController.LastPositionModel; - extrusionParams.rotationModel = - peltzerController.LastRotationModel * Quaternion.Inverse(extrusionBeginOrientation); - return extrusionParams; - } + /// + /// Each frame, if a mesh is currently held, update its position in world-space relative + /// to its original position, and the delta between the controller's position at world-start + /// and the controller's current position. + /// + private void Update() + { + if (!PeltzerController.AcquireIfNecessary(ref peltzerController) || + peltzerController.mode != ControllerMode.extrude) + return; + + if (extrusions.Count == 0) + { + // Update the position of the selector if there aren't any extrusions yet so the selector can know what to + // extrude and to render the hover highlight. + selector.SelectAtPosition(peltzerController.LastPositionModel, Selector.FACES_ONLY); + if (selector.hoverFace != null) + { + FaceKey hoveredFaceKey = selector.hoverFace; + MMesh hoveredMesh = model.GetMesh(hoveredFaceKey.meshId); + Face hoveredFace = hoveredMesh.GetFace(hoveredFaceKey.faceId); + int materialId = hoveredFace.properties.materialId; + if (materialId == MaterialRegistry.GEM_ID || materialId == MaterialRegistry.GLASS_ID) + { + materialId = MaterialRegistry.BLACK_ID; + } + PeltzerMain.Instance.highlightUtils.SetFaceStyleToExtrude(selector.hoverFace, selector.selectorPosition, + MaterialRegistry.GetMaterialColorById(materialId)); + } + if (selector.selectedFaces != null) + { + foreach (FaceKey faceKey in selector.selectedFaces) + { + MMesh selectedMesh = model.GetMesh(faceKey.meshId); + Face selectedFace = selectedMesh.GetFace(faceKey.faceId); + int materialId = selectedFace.properties.materialId; + if (materialId == MaterialRegistry.GEM_ID || materialId == MaterialRegistry.GLASS_ID) + { + materialId = MaterialRegistry.BLACK_ID; + } + PeltzerMain.Instance.highlightUtils.SetFaceStyleToExtrude(faceKey, selector.selectorPosition, + MaterialRegistry.GetMaterialColorById(materialId)); + } + } + } + else + { + if (waitingToDetermineReleaseType && Time.time - triggerDownTime > PeltzerController.SINGLE_CLICK_THRESHOLD) + { + waitingToDetermineReleaseType = false; + triggerUpToRelease = true; + } + + ExtrusionOperation.ExtrusionParams extrusionParams = BuildExtrusionParams(); + foreach (ExtrusionOperation operation in extrusions) + { + operation.UpdateExtrudeGuide(extrusionParams); + } + } + } - /// - /// Each frame, if a mesh is currently held, update its position in world-space relative - /// to its original position, and the delta between the controller's position at world-start - /// and the controller's current position. - /// - private void Update() { - if (!PeltzerController.AcquireIfNecessary(ref peltzerController) || - peltzerController.mode != ControllerMode.extrude) - return; - - if (extrusions.Count == 0) { - // Update the position of the selector if there aren't any extrusions yet so the selector can know what to - // extrude and to render the hover highlight. - selector.SelectAtPosition(peltzerController.LastPositionModel, Selector.FACES_ONLY); - if (selector.hoverFace != null) { - FaceKey hoveredFaceKey = selector.hoverFace; - MMesh hoveredMesh = model.GetMesh(hoveredFaceKey.meshId); - Face hoveredFace = hoveredMesh.GetFace(hoveredFaceKey.faceId); - int materialId = hoveredFace.properties.materialId; - if (materialId == MaterialRegistry.GEM_ID || materialId == MaterialRegistry.GLASS_ID) { - materialId = MaterialRegistry.BLACK_ID; - } - PeltzerMain.Instance.highlightUtils.SetFaceStyleToExtrude(selector.hoverFace, selector.selectorPosition, - MaterialRegistry.GetMaterialColorById(materialId)); + private void LateUpdate() + { + foreach (ExtrusionOperation extrusion in extrusions) + { + extrusion.Render(); + } } - if (selector.selectedFaces != null) { - foreach (FaceKey faceKey in selector.selectedFaces) { - MMesh selectedMesh = model.GetMesh(faceKey.meshId); - Face selectedFace = selectedMesh.GetFace(faceKey.faceId); - int materialId = selectedFace.properties.materialId; - if (materialId == MaterialRegistry.GEM_ID || materialId == MaterialRegistry.GLASS_ID) { - materialId = MaterialRegistry.BLACK_ID; + + /// + /// Makes only the supplied tooltip visible and ensures the others are off. + /// + /// The tooltip text to activate. + /// The hover state. + private void SetHoverTooltip(GameObject tooltip, TouchpadHoverState state) + { + if (!tooltip.activeSelf) + { + UnsetAllHoverTooltips(); + tooltip.SetActive(true); + peltzerController.SetTouchpadHoverTexture(state); + peltzerController.TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_1, + 0.003f, + 0.15f + ); } - PeltzerMain.Instance.highlightUtils.SetFaceStyleToExtrude(faceKey, selector.selectorPosition, - MaterialRegistry.GetMaterialColorById(materialId)); - } } - } - else { - if (waitingToDetermineReleaseType && Time.time - triggerDownTime > PeltzerController.SINGLE_CLICK_THRESHOLD) { - waitingToDetermineReleaseType = false; - triggerUpToRelease = true; + + /// + /// Unset all of the touchpad hover text tooltips. + /// + private void UnsetAllHoverTooltips() + { + peltzerController.controllerGeometry.modifyTooltipUp.SetActive(false); + peltzerController.controllerGeometry.modifyTooltipLeft.SetActive(false); + peltzerController.controllerGeometry.modifyTooltipRight.SetActive(false); + peltzerController.controllerGeometry.resizeDownTooltip.SetActive(false); + peltzerController.controllerGeometry.resizeUpTooltip.SetActive(false); + peltzerController.SetTouchpadHoverTexture(TouchpadHoverState.NONE); } - ExtrusionOperation.ExtrusionParams extrusionParams = BuildExtrusionParams(); - foreach (ExtrusionOperation operation in extrusions) { - operation.UpdateExtrudeGuide(extrusionParams); + private bool IsBeginOperationEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN + && extrusions.Count == 0; } - } - } - private void LateUpdate() { - foreach(ExtrusionOperation extrusion in extrusions) { - extrusion.Render(); - } - } + private bool IsCompleteSingleClickEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.UP + && waitingToDetermineReleaseType; + } - /// - /// Makes only the supplied tooltip visible and ensures the others are off. - /// - /// The tooltip text to activate. - /// The hover state. - private void SetHoverTooltip(GameObject tooltip, TouchpadHoverState state) { - if (!tooltip.activeSelf) { - UnsetAllHoverTooltips(); - tooltip.SetActive(true); - peltzerController.SetTouchpadHoverTexture(state); - peltzerController.TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_1, - 0.003f, - 0.15f - ); - } - } + private bool IsReleaseEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && ((args.Action == ButtonAction.UP && triggerUpToRelease) + || (args.Action == ButtonAction.DOWN && !triggerUpToRelease)) + && extrusions.Count() > 0; + } - /// - /// Unset all of the touchpad hover text tooltips. - /// - private void UnsetAllHoverTooltips() { - peltzerController.controllerGeometry.modifyTooltipUp.SetActive(false); - peltzerController.controllerGeometry.modifyTooltipLeft.SetActive(false); - peltzerController.controllerGeometry.modifyTooltipRight.SetActive(false); - peltzerController.controllerGeometry.resizeDownTooltip.SetActive(false); - peltzerController.controllerGeometry.resizeUpTooltip.SetActive(false); - peltzerController.SetTouchpadHoverTexture(TouchpadHoverState.NONE); - } + private static bool IsEnlargeFaceEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN + && args.TouchpadLocation == TouchpadLocation.TOP + && args.TouchpadOverlay == TouchpadOverlay.RESIZE; + } - private bool IsBeginOperationEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN - && extrusions.Count == 0; - } + private static bool IsShrinkFaceEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN + && args.TouchpadLocation == TouchpadLocation.BOTTOM + && args.TouchpadOverlay == TouchpadOverlay.RESIZE; + } - private bool IsCompleteSingleClickEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.UP - && waitingToDetermineReleaseType; - } + private static bool IsStartSnapEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN; + } - private bool IsReleaseEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && ((args.Action == ButtonAction.UP && triggerUpToRelease) - || (args.Action == ButtonAction.DOWN && !triggerUpToRelease)) - && extrusions.Count() > 0; - } + private static bool IsEndSnapEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.UP; + } - private static bool IsEnlargeFaceEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN - && args.TouchpadLocation == TouchpadLocation.TOP - && args.TouchpadOverlay == TouchpadOverlay.RESIZE; - } + // Touchpad Hover + private bool IsSetUpHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.TOP; + } - private static bool IsShrinkFaceEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN - && args.TouchpadLocation == TouchpadLocation.BOTTOM - && args.TouchpadOverlay == TouchpadOverlay.RESIZE; - } + private bool IsSetDownHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.BOTTOM; + } - private static bool IsStartSnapEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN; - } + private bool IsSetLeftHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.LEFT; + } - private static bool IsEndSnapEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.UP; - } + private bool IsSetRightHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.RIGHT; + } - // Touchpad Hover - private bool IsSetUpHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.TOP; - } + private static bool IsUnsetAllHoverTooltipsEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.NONE; + } - private bool IsSetDownHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.BOTTOM; - } + /// + /// Grab all hovered/selected faces for extrusion, if any. + /// + public void TryGrabbingMesh() + { + IEnumerable selectedFaces = selector.SelectedOrHoveredFaces(); + if (selectedFaces.Count() == 0) + { + return; + } - private bool IsSetLeftHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.LEFT; - } + // Ensure we're not multi-selecting now. + selector.EndMultiSelection(); + + // Set up the tools state for extrusion. + peltzerController.HideTooltips(); + peltzerController.HideModifyOverlays(); + peltzerController.ChangeTouchpadOverlay(TouchpadOverlay.RESIZE); + peltzerController.controllerGeometry.modifyTooltips.SetActive(true); + extrusionBeginPosition = peltzerController.LastPositionModel; + extrusionBeginOrientation = peltzerController.LastRotationModel; + + // We will make held faces transparent for the duration of the operation. We do so by directly modifying the + // meshes in the model, such that ReMesher is correctly updated and we can correctly undo this once the + // operation is complete. + Dictionary> newFaceProperties = + new Dictionary>(); + + // Set up an extrusion per face. + foreach (FaceKey faceKey in selectedFaces) + { + MMesh selectedMesh = model.GetMesh(faceKey.meshId); + Face selectedFace = selectedMesh.GetFace(faceKey.faceId); + ExtrusionOperation newOperation = new ExtrusionOperation(worldSpace, selectedMesh, selectedFace); + extrusions.Add(newOperation); + if (!newFaceProperties.ContainsKey(faceKey.meshId)) + { + newFaceProperties.Add(faceKey.meshId, new Dictionary()); + } + newFaceProperties[faceKey.meshId].Add(faceKey.faceId, new FaceProperties( + MaterialRegistry.GLASS_ID)); + } + foreach (KeyValuePair> newFacePropertySet in newFaceProperties) + { + ChangeFacePropertiesCommand changeFacePropertiesCommand = + new ChangeFacePropertiesCommand(newFacePropertySet.Key, newFacePropertySet.Value); + temporaryHeldFaceMaterialCommands.Add(changeFacePropertiesCommand); + changeFacePropertiesCommand.ApplyToModel(model); + } - private bool IsSetRightHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.RIGHT; - } + // Play feedback. + audioLibrary.PlayClip(audioLibrary.grabMeshPartSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - private static bool IsUnsetAllHoverTooltipsEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.NONE; - } + // Deselect everything. + selector.DeselectAll(); + return; + } - /// - /// Grab all hovered/selected faces for extrusion, if any. - /// - public void TryGrabbingMesh() { - IEnumerable selectedFaces = selector.SelectedOrHoveredFaces(); - if (selectedFaces.Count() == 0) { - return; - } - - // Ensure we're not multi-selecting now. - selector.EndMultiSelection(); - - // Set up the tools state for extrusion. - peltzerController.HideTooltips(); - peltzerController.HideModifyOverlays(); - peltzerController.ChangeTouchpadOverlay(TouchpadOverlay.RESIZE); - peltzerController.controllerGeometry.modifyTooltips.SetActive(true); - extrusionBeginPosition = peltzerController.LastPositionModel; - extrusionBeginOrientation = peltzerController.LastRotationModel; - - // We will make held faces transparent for the duration of the operation. We do so by directly modifying the - // meshes in the model, such that ReMesher is correctly updated and we can correctly undo this once the - // operation is complete. - Dictionary> newFaceProperties = - new Dictionary>(); - - // Set up an extrusion per face. - foreach (FaceKey faceKey in selectedFaces) { - MMesh selectedMesh = model.GetMesh(faceKey.meshId); - Face selectedFace = selectedMesh.GetFace(faceKey.faceId); - ExtrusionOperation newOperation = new ExtrusionOperation(worldSpace, selectedMesh, selectedFace); - extrusions.Add(newOperation); - if (!newFaceProperties.ContainsKey(faceKey.meshId)) { - newFaceProperties.Add(faceKey.meshId, new Dictionary()); + /// + /// Undoes the hack of making the selected faces transparent. + /// + private void UndoTemporaryHeldFaceMaterialCommands() + { + foreach (Command command in temporaryHeldFaceMaterialCommands) + { + command.GetUndoCommand(model).ApplyToModel(model); + } + temporaryHeldFaceMaterialCommands.Clear(); } - newFaceProperties[faceKey.meshId].Add(faceKey.faceId, new FaceProperties( - MaterialRegistry.GLASS_ID)); - } - foreach (KeyValuePair> newFacePropertySet in newFaceProperties) { - ChangeFacePropertiesCommand changeFacePropertiesCommand = - new ChangeFacePropertiesCommand(newFacePropertySet.Key, newFacePropertySet.Value); - temporaryHeldFaceMaterialCommands.Add(changeFacePropertiesCommand); - changeFacePropertiesCommand.ApplyToModel(model); - } - - // Play feedback. - audioLibrary.PlayClip(audioLibrary.grabMeshPartSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - - // Deselect everything. - selector.DeselectAll(); - return; - } - /// - /// Undoes the hack of making the selected faces transparent. - /// - private void UndoTemporaryHeldFaceMaterialCommands() { - foreach (Command command in temporaryHeldFaceMaterialCommands) { - command.GetUndoCommand(model).ApplyToModel(model); - } - temporaryHeldFaceMaterialCommands.Clear(); - } + /// + /// Finalize extrusions and add the new faces to the mesh. + /// + private void ReleaseMesh() + { + peltzerController.ChangeTouchpadOverlay(TouchpadOverlay.MODIFY); + peltzerController.ShowModifyOverlays(); + peltzerController.ShowTooltips(); + + List commands = new List(); + Dictionary modifiedMeshes = new Dictionary(); + Dictionary> newVerticesInModifiedMeshes = new Dictionary>(); + ExtrusionOperation.ExtrusionParams extrusionParams = BuildExtrusionParams(); + + foreach (ExtrusionOperation extrusion in extrusions) + { + if (modifiedMeshes.ContainsKey(extrusion.mesh.id)) + { + HashSet newVertices = newVerticesInModifiedMeshes[extrusion.mesh.id]; + modifiedMeshes[extrusion.mesh.id] = extrusion.DoExtrusion(modifiedMeshes[extrusion.mesh.id], + extrusionParams, ref newVertices); + } + else + { + MMesh clonedMesh = extrusion.mesh.Clone(); + HashSet newVertices = new HashSet(); + modifiedMeshes.Add(extrusion.mesh.id, extrusion.DoExtrusion(clonedMesh, extrusionParams, ref newVertices)); + newVerticesInModifiedMeshes.Add(extrusion.mesh.id, newVertices); + } + } - /// - /// Finalize extrusions and add the new faces to the mesh. - /// - private void ReleaseMesh() { - peltzerController.ChangeTouchpadOverlay(TouchpadOverlay.MODIFY); - peltzerController.ShowModifyOverlays(); - peltzerController.ShowTooltips(); - - List commands = new List(); - Dictionary modifiedMeshes = new Dictionary(); - Dictionary> newVerticesInModifiedMeshes = new Dictionary>(); - ExtrusionOperation.ExtrusionParams extrusionParams = BuildExtrusionParams(); - - foreach (ExtrusionOperation extrusion in extrusions) { - if (modifiedMeshes.ContainsKey(extrusion.mesh.id)) { - HashSet newVertices = newVerticesInModifiedMeshes[extrusion.mesh.id]; - modifiedMeshes[extrusion.mesh.id] = extrusion.DoExtrusion(modifiedMeshes[extrusion.mesh.id], - extrusionParams, ref newVertices); - } else { - MMesh clonedMesh = extrusion.mesh.Clone(); - HashSet newVertices = new HashSet(); - modifiedMeshes.Add(extrusion.mesh.id, extrusion.DoExtrusion(clonedMesh, extrusionParams, ref newVertices)); - newVerticesInModifiedMeshes.Add(extrusion.mesh.id, newVertices); - } - } - - foreach (KeyValuePair newMeshPair in modifiedMeshes) { - int meshId = newMeshPair.Key; - MMesh newMesh = newMeshPair.Value; - newMesh.RecalcBounds(); - List updatedVerts = new List(newVerticesInModifiedMeshes[meshId]); - MeshFixer.FixMutatedMesh(model.GetMesh(newMesh.id), newMesh, new HashSet(updatedVerts.Select(v => v.id)), - /* splitNonCoplanarFaces */ true, /* mergeAdjacentCoplanarFaces*/ false); - - HashSet updatedVertIds = new HashSet(); - for (int i = 0; i < updatedVerts.Count; i++) { - updatedVertIds.Add(updatedVerts[i].id); - } - - bool isValidMesh = MeshValidator.IsValidMesh(newMesh, updatedVertIds); - if (isValidMesh && model.CanAddMesh(newMesh)) { - commands.Add(new ReplaceMeshCommand(meshId, newMesh)); - } else { - // If any new mesh is invalid, abort everything. - audioLibrary.PlayClip(audioLibrary.errorSound); - peltzerController.TriggerHapticFeedback(); - UndoTemporaryHeldFaceMaterialCommands(); - return; - } - } - - // Expire the temporary held face commands, because the originally-held faces have now been removed - // following a successful extrusion. - temporaryHeldFaceMaterialCommands.Clear(); - model.ApplyCommand(new CompositeCommand(commands)); - audioLibrary.PlayClip(audioLibrary.releaseMeshSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - PeltzerMain.Instance.extrudesCompleted++; - } + foreach (KeyValuePair newMeshPair in modifiedMeshes) + { + int meshId = newMeshPair.Key; + MMesh newMesh = newMeshPair.Value; + newMesh.RecalcBounds(); + List updatedVerts = new List(newVerticesInModifiedMeshes[meshId]); + MeshFixer.FixMutatedMesh(model.GetMesh(newMesh.id), newMesh, new HashSet(updatedVerts.Select(v => v.id)), + /* splitNonCoplanarFaces */ true, /* mergeAdjacentCoplanarFaces*/ false); + + HashSet updatedVertIds = new HashSet(); + for (int i = 0; i < updatedVerts.Count; i++) + { + updatedVertIds.Add(updatedVerts[i].id); + } + + bool isValidMesh = MeshValidator.IsValidMesh(newMesh, updatedVertIds); + if (isValidMesh && model.CanAddMesh(newMesh)) + { + commands.Add(new ReplaceMeshCommand(meshId, newMesh)); + } + else + { + // If any new mesh is invalid, abort everything. + audioLibrary.PlayClip(audioLibrary.errorSound); + peltzerController.TriggerHapticFeedback(); + UndoTemporaryHeldFaceMaterialCommands(); + return; + } + } - /// - /// An event handler that listens for controller input and delegates accordingly. - /// - private void ControllerEventHandler(object sender, ControllerEventArgs args) { - if (peltzerController.mode != ControllerMode.extrude) - return; - - if (IsBeginOperationEvent(args)) { - // If we are about to operate on selected faces, ensure the click is near those faces. - if (selector.selectedFaces.Count > 0) { - if (!selector.ClickIsWithinCurrentSelection(peltzerController.LastPositionModel)) { - return; - } - } - triggerUpToRelease = false; - waitingToDetermineReleaseType = true; - triggerDownTime = Time.time; - TryGrabbingMesh(); - } else if (IsCompleteSingleClickEvent(args)) { - waitingToDetermineReleaseType = false; - triggerUpToRelease = false; - } else if (IsReleaseEvent(args)) { - if (isSnapping) { - // We snapped while modifying, so we have learned a bit more about snapping. - completedSnaps++; + // Expire the temporary held face commands, because the originally-held faces have now been removed + // following a successful extrusion. + temporaryHeldFaceMaterialCommands.Clear(); + model.ApplyCommand(new CompositeCommand(commands)); + audioLibrary.PlayClip(audioLibrary.releaseMeshSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + PeltzerMain.Instance.extrudesCompleted++; } - ReleaseMesh(); - ClearState(); - } else if (IsEnlargeFaceEvent(args) && extrusions.Count > 0) { - extrusions.ForEach(e => e.EnlargeExtrusionFace()); - } else if (IsShrinkFaceEvent(args) && extrusions.Count > 0) { - extrusions.ForEach(e => e.ShrinkExtrusionFace()); - } else if (IsStartSnapEvent(args) && !peltzerController.isBlockMode) { - isSnapping = true; - if (completedSnaps < SNAP_KNOW_HOW_COUNT) { - PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); - } - PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.alignSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - PeltzerMain.Instance.snappedInExtruder = true; - } else if (IsEndSnapEvent(args) && !peltzerController.isBlockMode) { - isSnapping = false; - if (IsExtrudingFace()) { - // We snapped while modifying, so we have learned a bit more about snapping. - completedSnaps++; + + /// + /// An event handler that listens for controller input and delegates accordingly. + /// + private void ControllerEventHandler(object sender, ControllerEventArgs args) + { + if (peltzerController.mode != ControllerMode.extrude) + return; + + if (IsBeginOperationEvent(args)) + { + // If we are about to operate on selected faces, ensure the click is near those faces. + if (selector.selectedFaces.Count > 0) + { + if (!selector.ClickIsWithinCurrentSelection(peltzerController.LastPositionModel)) + { + return; + } + } + triggerUpToRelease = false; + waitingToDetermineReleaseType = true; + triggerDownTime = Time.time; + TryGrabbingMesh(); + } + else if (IsCompleteSingleClickEvent(args)) + { + waitingToDetermineReleaseType = false; + triggerUpToRelease = false; + } + else if (IsReleaseEvent(args)) + { + if (isSnapping) + { + // We snapped while modifying, so we have learned a bit more about snapping. + completedSnaps++; + } + ReleaseMesh(); + ClearState(); + } + else if (IsEnlargeFaceEvent(args) && extrusions.Count > 0) + { + extrusions.ForEach(e => e.EnlargeExtrusionFace()); + } + else if (IsShrinkFaceEvent(args) && extrusions.Count > 0) + { + extrusions.ForEach(e => e.ShrinkExtrusionFace()); + } + else if (IsStartSnapEvent(args) && !peltzerController.isBlockMode) + { + isSnapping = true; + if (completedSnaps < SNAP_KNOW_HOW_COUNT) + { + PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); + } + PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.alignSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + PeltzerMain.Instance.snappedInExtruder = true; + } + else if (IsEndSnapEvent(args) && !peltzerController.isBlockMode) + { + isSnapping = false; + if (IsExtrudingFace()) + { + // We snapped while modifying, so we have learned a bit more about snapping. + completedSnaps++; + } + PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); + } + else if (IsSetUpHoverTooltipEvent(args) && !IsExtrudingFace() + && PeltzerMain.Instance.restrictionManager.touchpadUpAllowed) + { + SetHoverTooltip(peltzerController.controllerGeometry.modifyTooltipUp, TouchpadHoverState.UP); + } + else if (IsSetUpHoverTooltipEvent(args) && IsExtrudingFace()) + { + SetHoverTooltip(peltzerController.controllerGeometry.resizeUpTooltip, TouchpadHoverState.RESIZE_UP); + } + else if (IsSetDownHoverTooltipEvent(args) && IsExtrudingFace()) + { + SetHoverTooltip(peltzerController.controllerGeometry.resizeDownTooltip, TouchpadHoverState.RESIZE_DOWN); + } + else if (IsSetLeftHoverTooltipEvent(args) && !IsExtrudingFace() + && PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed) + { + SetHoverTooltip(peltzerController.controllerGeometry.modifyTooltipLeft, TouchpadHoverState.LEFT); + } + else if (IsSetRightHoverTooltipEvent(args) && !IsExtrudingFace() + && PeltzerMain.Instance.restrictionManager.touchpadRightAllowed) + { + SetHoverTooltip(peltzerController.controllerGeometry.modifyTooltipRight, TouchpadHoverState.RIGHT); + } + else if (IsUnsetAllHoverTooltipsEvent(args)) + { + UnsetAllHoverTooltips(); + } } - PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); - } else if (IsSetUpHoverTooltipEvent(args) && !IsExtrudingFace() - && PeltzerMain.Instance.restrictionManager.touchpadUpAllowed) { - SetHoverTooltip(peltzerController.controllerGeometry.modifyTooltipUp, TouchpadHoverState.UP); - } else if (IsSetUpHoverTooltipEvent(args) && IsExtrudingFace()) { - SetHoverTooltip(peltzerController.controllerGeometry.resizeUpTooltip, TouchpadHoverState.RESIZE_UP); - } else if (IsSetDownHoverTooltipEvent(args) && IsExtrudingFace()) { - SetHoverTooltip(peltzerController.controllerGeometry.resizeDownTooltip, TouchpadHoverState.RESIZE_DOWN); - } else if (IsSetLeftHoverTooltipEvent(args) && !IsExtrudingFace() - && PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed) { - SetHoverTooltip(peltzerController.controllerGeometry.modifyTooltipLeft, TouchpadHoverState.LEFT); - } else if (IsSetRightHoverTooltipEvent(args) && !IsExtrudingFace() - && PeltzerMain.Instance.restrictionManager.touchpadRightAllowed) { - SetHoverTooltip(peltzerController.controllerGeometry.modifyTooltipRight, TouchpadHoverState.RIGHT); - } else if (IsUnsetAllHoverTooltipsEvent(args)) { - UnsetAllHoverTooltips(); - } - } - public bool IsExtrudingFace() { - return extrusions.Count > 0; - } + public bool IsExtrudingFace() + { + return extrusions.Count > 0; + } - public void ClearState() { - foreach (ExtrusionOperation extrusion in extrusions) { - extrusion.ClearExtrusionGuide(); - } - waitingToDetermineReleaseType = false; - extrusions.Clear(); - // Deselect everything. - selector.DeselectAll(); + public void ClearState() + { + foreach (ExtrusionOperation extrusion in extrusions) + { + extrusion.ClearExtrusionGuide(); + } + waitingToDetermineReleaseType = false; + extrusions.Clear(); + // Deselect everything. + selector.DeselectAll(); + } } - } } diff --git a/Assets/Scripts/tools/ExtrusionOperation.cs b/Assets/Scripts/tools/ExtrusionOperation.cs index 078d90a1..78a8e3eb 100644 --- a/Assets/Scripts/tools/ExtrusionOperation.cs +++ b/Assets/Scripts/tools/ExtrusionOperation.cs @@ -28,510 +28,572 @@ /// THIS OPERATION CURRENTLY OPERATES ON VERTICES IN WORLD SPACE. This should be changed to model space at some point /// in the future - bug /// -public class ExtrusionOperation { - - // If the distance between two vertices is less than this distance, we merge. - private static readonly float MERGE_DIST_THRESH = .01f; - - /// - /// Specifies the parameters for an extrusion operation. - /// - public struct ExtrusionParams { - // If true, extrusion is locked to the face's normal. Translation will be projected onto the normal vector. - // Rotation will be ignored. - public bool lockToNormal; - // Indicates by how much we should translate the extruded face. This is in MODEL space. - public Vector3 translationModel; - // Indicates how we should rotate the extruded face. This is in MODEL space. - public Quaternion rotationModel; - // Indicates the point about which the extruded face should be rotated, AFTER the translation. - // This is in MODEL space. - public Vector3 rotationPivotModel; - } - - /// - /// Represents a side of our extrusion. A side is the face that connects the base face to the extrusion face. - /// It may be a triangle or a quadrilateral. If it's a triangle, it has two base vertices and one extrusion - /// vertex. If it's a quad, it has two base vertices and two extrusion vertices. - /// - /// QUADRILATERAL SIDE TRIANGULAR SIDE - /// - /// EL------------ER EL == ER - /// | | /\ - /// | | / \ - /// | | / \ - /// | | / \ - /// BL------------BR BL +--------+ BR - /// - /// - /// BL = base left vertex - /// BR = base right vertex - /// EL = extrusion left vertex - /// ER = extrusion right vertex - /// - /// Note that "left" and "right" are defined from the point of view of someone looking at the face - /// and thinking of the base face as being "below" and the extrusion face as being "above". - /// - /// This class is public for testing. - /// - public class ExtrusionSideVertices { - public readonly Vector3 baseLeft; - public readonly int baseLeftIndex; - public readonly Vector3 baseRight; - public readonly int baseRightIndex; - - private ExtrusionOperation parent; +public class ExtrusionOperation +{ - /// - /// Returns whether this face is a triangle (true) or a quadrilateral (false). - /// - public bool isTriangle { get; private set; } + // If the distance between two vertices is less than this distance, we merge. + private static readonly float MERGE_DIST_THRESH = .01f; /// - /// Returns the extrusion left vertex. - /// Note that if this face is a triangle, the extrusion left and right vertices are the same. + /// Specifies the parameters for an extrusion operation. /// - public Vector3 extrusionLeft { get; private set; } + public struct ExtrusionParams + { + // If true, extrusion is locked to the face's normal. Translation will be projected onto the normal vector. + // Rotation will be ignored. + public bool lockToNormal; + // Indicates by how much we should translate the extruded face. This is in MODEL space. + public Vector3 translationModel; + // Indicates how we should rotate the extruded face. This is in MODEL space. + public Quaternion rotationModel; + // Indicates the point about which the extruded face should be rotated, AFTER the translation. + // This is in MODEL space. + public Vector3 rotationPivotModel; + } /// - /// Returns the extrusion right vertex. - /// Note that if this face is a triangle, the extrusion left and right vertices are the same. + /// Represents a side of our extrusion. A side is the face that connects the base face to the extrusion face. + /// It may be a triangle or a quadrilateral. If it's a triangle, it has two base vertices and one extrusion + /// vertex. If it's a quad, it has two base vertices and two extrusion vertices. + /// + /// QUADRILATERAL SIDE TRIANGULAR SIDE + /// + /// EL------------ER EL == ER + /// | | /\ + /// | | / \ + /// | | / \ + /// | | / \ + /// BL------------BR BL +--------+ BR + /// + /// + /// BL = base left vertex + /// BR = base right vertex + /// EL = extrusion left vertex + /// ER = extrusion right vertex + /// + /// Note that "left" and "right" are defined from the point of view of someone looking at the face + /// and thinking of the base face as being "below" and the extrusion face as being "above". + /// + /// This class is public for testing. /// - public Vector3 extrusionRight { get; private set; } + public class ExtrusionSideVertices + { + public readonly Vector3 baseLeft; + public readonly int baseLeftIndex; + public readonly Vector3 baseRight; + public readonly int baseRightIndex; + + private ExtrusionOperation parent; + + /// + /// Returns whether this face is a triangle (true) or a quadrilateral (false). + /// + public bool isTriangle { get; private set; } + + /// + /// Returns the extrusion left vertex. + /// Note that if this face is a triangle, the extrusion left and right vertices are the same. + /// + public Vector3 extrusionLeft { get; private set; } + + /// + /// Returns the extrusion right vertex. + /// Note that if this face is a triangle, the extrusion left and right vertices are the same. + /// + public Vector3 extrusionRight { get; private set; } + + /// + /// Returns true if this face is a quadrilateral and the extrusion vertices are close enough together + /// to require a merge. + /// + public bool requiresMerge { get; private set; } + + /// + /// Creates a quadrilateral extrusion face with the given vertices. + /// + /// The base left vertex. + /// The index of the base left vertex (in the base face). + /// The base right vertex. + /// The index of the base right vertex (in the base face). + /// The extrusion left vertex. + /// The extrusion right vertex. + public ExtrusionSideVertices(ExtrusionOperation parent, + Vector3 baseLeft, int baseLeftIndex, Vector3 baseRight, int baseRightIndex, + Vector3 extrusionLeft, Vector3 extrusionRight) + { + this.parent = parent; + this.baseLeft = baseLeft; + this.baseLeftIndex = baseLeftIndex; + this.baseRight = baseRight; + this.baseRightIndex = baseRightIndex; + this.extrusionLeft = extrusionLeft; + this.extrusionRight = extrusionRight; + isTriangle = false; + CheckIfUpdateRequiresMerge(); + } - /// - /// Returns true if this face is a quadrilateral and the extrusion vertices are close enough together - /// to require a merge. - /// - public bool requiresMerge { get; private set; } + /// + /// Convert this face from a quadrilateral to a triangle. This means that instead of having two extrusion vertices, + /// this face will now only have one extrusion vertex, which will be computed as the average of the two. + /// + public void ConvertToTriangle() + { + AssertOrThrow.True(!isTriangle, "ExtrusionSideVertices is already a triangle."); + isTriangle = true; + Vector3 average = (extrusionRight + extrusionLeft) / 2.0f; + extrusionRight = extrusionLeft = average; + CheckIfUpdateRequiresMerge(); + } - /// - /// Creates a quadrilateral extrusion face with the given vertices. - /// - /// The base left vertex. - /// The index of the base left vertex (in the base face). - /// The base right vertex. - /// The index of the base right vertex (in the base face). - /// The extrusion left vertex. - /// The extrusion right vertex. - public ExtrusionSideVertices(ExtrusionOperation parent, - Vector3 baseLeft, int baseLeftIndex, Vector3 baseRight, int baseRightIndex, - Vector3 extrusionLeft, Vector3 extrusionRight) { - this.parent = parent; - this.baseLeft = baseLeft; - this.baseLeftIndex = baseLeftIndex; - this.baseRight = baseRight; - this.baseRightIndex = baseRightIndex; - this.extrusionLeft = extrusionLeft; - this.extrusionRight = extrusionRight; - isTriangle = false; - CheckIfUpdateRequiresMerge(); + /// + /// Sets the position of the extrusion left vertex. + /// If this face is a triangle, the extrusion right vertex is also updated, as they coincide. + /// + /// The new position. + public void SetExtrusionLeft(Vector3 newValue) + { + extrusionLeft = newValue; + if (isTriangle) + { + extrusionRight = newValue; + } + CheckIfUpdateRequiresMerge(); + } + + /// + /// Sets the position of the extrusion right vertex. + /// If this face is a triangle, the extrusion left vertex is also updated, as they coincide. + /// + /// The new position. + public void SetExtrusionRight(Vector3 newValue) + { + extrusionRight = newValue; + if (isTriangle) + { + extrusionLeft = newValue; + } + CheckIfUpdateRequiresMerge(); + } + + private void CheckIfUpdateRequiresMerge() + { + // We only merge if a user has enlarged or shrunk the face -- otherwise we preserve the original face the + // user has grabbed. + if (parent.scaleOffset == 0) + { + return; + } + // A face requires a merge if it's a quadrilateral and its two extrusion vertices are too close together. + float distanceBetweenVertsInModelSpace = + Vector3.Distance(extrusionLeft, extrusionRight); + requiresMerge = !isTriangle && distanceBetweenVertsInModelSpace < MERGE_DIST_THRESH; + } } - /// - /// Convert this face from a quadrilateral to a triangle. This means that instead of having two extrusion vertices, - /// this face will now only have one extrusion vertex, which will be computed as the average of the two. - /// - public void ConvertToTriangle() { - AssertOrThrow.True(!isTriangle, "ExtrusionSideVertices is already a triangle."); - isTriangle = true; - Vector3 average = (extrusionRight + extrusionLeft) / 2.0f; - extrusionRight = extrusionLeft = average; - CheckIfUpdateRequiresMerge(); + private WorldSpace worldSpace; + private MMesh _mesh; + private Face heldFace; + private List sideMeshes; + private Mesh extrudeFaceMesh; + private float size = 1.0f; + float lastSizeBeforeZero; + float oldSize; + private FaceProperties originalFaceProperties; + // The cumulation of enlarge/shrink operations. At 0, this means the extrusion has not been enlarged or shrunk. + private int scaleOffset; + + public ExtrusionOperation(WorldSpace worldSpace, MMesh mesh, Face heldFace) + { + this.worldSpace = worldSpace; + this._mesh = mesh; + this.heldFace = heldFace; + + originalFaceProperties = heldFace.properties; + SetupExtrusionGuide(); } /// - /// Sets the position of the extrusion left vertex. - /// If this face is a triangle, the extrusion right vertex is also updated, as they coincide. + /// Sets up a guide for the user which will show the current state of the extrusion. /// - /// The new position. - public void SetExtrusionLeft(Vector3 newValue) { - extrusionLeft = newValue; - if (isTriangle) { - extrusionRight = newValue; - } - CheckIfUpdateRequiresMerge(); + private void SetupExtrusionGuide() + { + extrudeFaceMesh = new Mesh(); + sideMeshes = new List(heldFace.vertexIds.Count); + for (int i = 0; i < heldFace.vertexIds.Count; i++) + { + Mesh sideGuideMesh = new Mesh(); + sideMeshes.Add(sideGuideMesh); + } } /// - /// Sets the position of the extrusion right vertex. - /// If this face is a triangle, the extrusion left vertex is also updated, as they coincide. + /// Updates the guide showing what the current extrusion will look like if the user + /// completes the operation. /// - /// The new position. - public void SetExtrusionRight(Vector3 newValue) { - extrusionRight = newValue; - if (isTriangle) { - extrusionLeft = newValue; - } - CheckIfUpdateRequiresMerge(); - } - - private void CheckIfUpdateRequiresMerge() { - // We only merge if a user has enlarged or shrunk the face -- otherwise we preserve the original face the - // user has grabbed. - if (parent.scaleOffset == 0) { - return; - } - // A face requires a merge if it's a quadrilateral and its two extrusion vertices are too close together. - float distanceBetweenVertsInModelSpace = - Vector3.Distance(extrusionLeft, extrusionRight); - requiresMerge = !isTriangle && distanceBetweenVertsInModelSpace < MERGE_DIST_THRESH; - } - } - - private WorldSpace worldSpace; - private MMesh _mesh; - private Face heldFace; - private List sideMeshes; - private Mesh extrudeFaceMesh; - private float size = 1.0f; - float lastSizeBeforeZero; - float oldSize; - private FaceProperties originalFaceProperties; - // The cumulation of enlarge/shrink operations. At 0, this means the extrusion has not been enlarged or shrunk. - private int scaleOffset; - - public ExtrusionOperation(WorldSpace worldSpace, MMesh mesh, Face heldFace) { - this.worldSpace = worldSpace; - this._mesh = mesh; - this.heldFace = heldFace; - - originalFaceProperties = heldFace.properties; - SetupExtrusionGuide(); - } - - /// - /// Sets up a guide for the user which will show the current state of the extrusion. - /// - private void SetupExtrusionGuide() { - extrudeFaceMesh = new Mesh(); - sideMeshes = new List(heldFace.vertexIds.Count); - for (int i = 0; i < heldFace.vertexIds.Count; i++) { - Mesh sideGuideMesh = new Mesh(); - sideMeshes.Add(sideGuideMesh); - } - } - - /// - /// Updates the guide showing what the current extrusion will look like if the user - /// completes the operation. - /// - /// Extrusion parameters indicating how to extrude. - public void UpdateExtrudeGuide(ExtrusionParams extrusionParams) { - extrudeFaceMesh.Clear(); - bool mergeOccurred; - List extrusionSides = - BuildExtrusionSides(this, _mesh, heldFace, extrusionParams, size, out mergeOccurred); - if (mergeOccurred && size != 0 && oldSize > size) { - // If the vertices were merged because they were too close together on a resize down event, - // the size is effectively 0 and should be updated. The lastSizeBeforeZero reflects the size the face was - // before it had to be merged. - lastSizeBeforeZero = oldSize; - size = 0; - } + /// Extrusion parameters indicating how to extrude. + public void UpdateExtrudeGuide(ExtrusionParams extrusionParams) + { + extrudeFaceMesh.Clear(); + bool mergeOccurred; + List extrusionSides = + BuildExtrusionSides(this, _mesh, heldFace, extrusionParams, size, out mergeOccurred); + if (mergeOccurred && size != 0 && oldSize > size) + { + // If the vertices were merged because they were too close together on a resize down event, + // the size is effectively 0 and should be updated. The lastSizeBeforeZero reflects the size the face was + // before it had to be merged. + lastSizeBeforeZero = oldSize; + size = 0; + } - // Create our extrude face - List extrusionFaceVertices = new List(); - for (int i = 0; i < extrusionSides.Count; i++) { - ExtrusionSideVertices side = extrusionSides[i]; - extrusionFaceVertices.Add(side.extrusionLeft); - if (!side.isTriangle) { - extrusionFaceVertices.Add(side.extrusionRight); - } - } + // Create our extrude face + List extrusionFaceVertices = new List(); + for (int i = 0; i < extrusionSides.Count; i++) + { + ExtrusionSideVertices side = extrusionSides[i]; + extrusionFaceVertices.Add(side.extrusionLeft); + if (!side.isTriangle) + { + extrusionFaceVertices.Add(side.extrusionRight); + } + } - // Poly's material shaders require vertex colors - this sets up a color channel for the extrustion face vertices. - int materialId = originalFaceProperties.materialId; - if (materialId == MaterialRegistry.GEM_ID || materialId == MaterialRegistry.GLASS_ID) { - materialId = MaterialRegistry.BLACK_ID; - } - Color vertColor = MaterialRegistry.GetMaterialColorById(materialId); - Color[] colors = new Color[extrusionFaceVertices.Count]; - for (int i = 0; i < colors.Length; i++) { - colors[i] = vertColor; - } - extrudeFaceMesh.SetVertices(extrusionFaceVertices); - extrudeFaceMesh.SetTriangles(MeshHelper.GetTrianglesAsFan(extrusionFaceVertices.Count), 0); - extrudeFaceMesh.colors = colors; - extrudeFaceMesh.RecalculateNormals(); - extrudeFaceMesh.RecalculateBounds(); - - // okay, now we connect the original face to the new extrude face by creating the sides. - for (int i = 0; i < sideMeshes.Count; i++) { - Mesh sideGuide = sideMeshes[i]; - sideGuide.Clear(); - ExtrusionSideVertices extrusionSideVertices = extrusionSides[i]; - if (extrusionSideVertices.isTriangle) { - sideGuide.SetVertices(new List() { + // Poly's material shaders require vertex colors - this sets up a color channel for the extrustion face vertices. + int materialId = originalFaceProperties.materialId; + if (materialId == MaterialRegistry.GEM_ID || materialId == MaterialRegistry.GLASS_ID) + { + materialId = MaterialRegistry.BLACK_ID; + } + Color vertColor = MaterialRegistry.GetMaterialColorById(materialId); + Color[] colors = new Color[extrusionFaceVertices.Count]; + for (int i = 0; i < colors.Length; i++) + { + colors[i] = vertColor; + } + extrudeFaceMesh.SetVertices(extrusionFaceVertices); + extrudeFaceMesh.SetTriangles(MeshHelper.GetTrianglesAsFan(extrusionFaceVertices.Count), 0); + extrudeFaceMesh.colors = colors; + extrudeFaceMesh.RecalculateNormals(); + extrudeFaceMesh.RecalculateBounds(); + + // okay, now we connect the original face to the new extrude face by creating the sides. + for (int i = 0; i < sideMeshes.Count; i++) + { + Mesh sideGuide = sideMeshes[i]; + sideGuide.Clear(); + ExtrusionSideVertices extrusionSideVertices = extrusionSides[i]; + if (extrusionSideVertices.isTriangle) + { + sideGuide.SetVertices(new List() { extrusionSideVertices.baseLeft, extrusionSideVertices.baseRight, extrusionSideVertices.extrusionLeft, }); - // Vertex colors for side faces - sideGuide.colors = new[] {vertColor, vertColor, vertColor}; - // The size of the extrude face is zero, so its sides are just triangles. - sideGuide.SetTriangles(new int[] { 0, 1, 2 }, /** submesh */ 0); - } else { - sideGuide.SetVertices(new List() { + // Vertex colors for side faces + sideGuide.colors = new[] { vertColor, vertColor, vertColor }; + // The size of the extrude face is zero, so its sides are just triangles. + sideGuide.SetTriangles(new int[] { 0, 1, 2 }, /** submesh */ 0); + } + else + { + sideGuide.SetVertices(new List() { extrusionSideVertices.baseLeft, extrusionSideVertices.baseRight, extrusionSideVertices.extrusionRight, extrusionSideVertices.extrusionLeft }); - // Vertex colors for side faces - sideGuide.colors = new[] {vertColor, vertColor, vertColor, vertColor}; - // Side faces always have four vertices, so there are two triangles. - sideGuide.SetTriangles(new int[] { 0, 1, 2, 0, 2, 3 }, /** submesh */ 0); - } - sideGuide.RecalculateNormals(); - sideGuide.RecalculateBounds(); - } - } - - /// - /// Render extrustion guide. - /// - public void Render() { - foreach (Mesh sideMesh in sideMeshes) { - Graphics.DrawMesh(sideMesh, worldSpace.modelToWorld, - MaterialRegistry.GetMaterialAndColorById(originalFaceProperties.materialId).material, 0); - } - if (extrudeFaceMesh != null) { - Graphics.DrawMesh(extrudeFaceMesh, worldSpace.modelToWorld, - MaterialRegistry.GetMaterialAndColorById(originalFaceProperties.materialId).material, 0); - } - } - - /// - /// Extrusion is over (or cancelled), so get rid of the guides. - /// - public void ClearExtrusionGuide() { - extrudeFaceMesh = null; - sideMeshes.Clear(); - - heldFace.SetProperties(originalFaceProperties); - } - - /// - /// Shrinks the face the user is extruding. - /// - public void ShrinkExtrusionFace() { - oldSize = size; - if (size <= .5f) { - size -= .08f; - } else { - size *= .9f; - } - if (size < 0) { - size = 0; - } - if (size != oldSize) { - scaleOffset--; + // Vertex colors for side faces + sideGuide.colors = new[] { vertColor, vertColor, vertColor, vertColor }; + // Side faces always have four vertices, so there are two triangles. + sideGuide.SetTriangles(new int[] { 0, 1, 2, 0, 2, 3 }, /** submesh */ 0); + } + sideGuide.RecalculateNormals(); + sideGuide.RecalculateBounds(); + } } - // Keep track of the size before a user scales to zero, so the next scale-up is the opposite of the scale-down. - if (oldSize != 0 && size == 0) { - lastSizeBeforeZero = oldSize; + /// + /// Render extrustion guide. + /// + public void Render() + { + foreach (Mesh sideMesh in sideMeshes) + { + Graphics.DrawMesh(sideMesh, worldSpace.modelToWorld, + MaterialRegistry.GetMaterialAndColorById(originalFaceProperties.materialId).material, 0); + } + if (extrudeFaceMesh != null) + { + Graphics.DrawMesh(extrudeFaceMesh, worldSpace.modelToWorld, + MaterialRegistry.GetMaterialAndColorById(originalFaceProperties.materialId).material, 0); + } } - } - - /// - /// Enlarges the face the user is extruding - /// - public void EnlargeExtrusionFace() { - oldSize = size; - if (size == 0) { - size = lastSizeBeforeZero; - } else { - size *= 1.1f; + + /// + /// Extrusion is over (or cancelled), so get rid of the guides. + /// + public void ClearExtrusionGuide() + { + extrudeFaceMesh = null; + sideMeshes.Clear(); + + heldFace.SetProperties(originalFaceProperties); } - scaleOffset++; - } - - /// - /// Generates a new mesh with the extrusion performed upon it. - /// - /// Mesh to extrude. - /// The parameters indicating how to perform the extrusion. - /// All added vertices. - /// Extruded version of the mesh. - public MMesh DoExtrusion(MMesh mesh, ExtrusionParams extrusionParams, ref HashSet addedVertices) { - heldFace.SetProperties(originalFaceProperties); - MMesh.GeometryOperation operation = mesh.StartOperation(); - operation.DeleteFace(heldFace.id); - - bool mergeOccurred; - List extrusionSides = - BuildExtrusionSides(this, _mesh, heldFace, extrusionParams, size, out mergeOccurred); - if (mergeOccurred && size != 0 && oldSize > size) { - // If the vertices were merged because they were too close together on a resize down event, - // the size is effectively 0 and should be updated. The lastSizeBeforeZero reflects the size the face was - // before it had to be merged. - lastSizeBeforeZero = oldSize; - size = 0; + + /// + /// Shrinks the face the user is extruding. + /// + public void ShrinkExtrusionFace() + { + oldSize = size; + if (size <= .5f) + { + size -= .08f; + } + else + { + size *= .9f; + } + if (size < 0) + { + size = 0; + } + if (size != oldSize) + { + scaleOffset--; + } + + // Keep track of the size before a user scales to zero, so the next scale-up is the opposite of the scale-down. + if (oldSize != 0 && size == 0) + { + lastSizeBeforeZero = oldSize; + } } - List extrusionFaceVertices = new List(); - foreach (ExtrusionSideVertices side in extrusionSides) { - Vector3 extrusion1Local = mesh.ModelCoordsToMeshCoords(side.extrusionLeft); - Vertex vertex1 = extrusionFaceVertices.FirstOrDefault(v => v.loc == extrusion1Local); - if (vertex1 == null) { - vertex1 = operation.AddVertexMeshSpace(extrusion1Local); - - addedVertices.Add(vertex1); - extrusionFaceVertices.Add(vertex1); - } - if (side.isTriangle) { - operation.AddFace(new List() { side.baseLeftIndex, side.baseRightIndex, vertex1.id }, - originalFaceProperties); - } else { - Vector3 extrusion2Local = mesh.ModelCoordsToMeshCoords(side.extrusionRight); - Vertex vertex2 = extrusionFaceVertices.FirstOrDefault(v => v.loc == extrusion2Local); - if (vertex2 == null) { - vertex2 = operation.AddVertexMeshSpace(extrusion2Local); - addedVertices.Add(vertex2); - extrusionFaceVertices.Add(vertex2); + /// + /// Enlarges the face the user is extruding + /// + public void EnlargeExtrusionFace() + { + oldSize = size; + if (size == 0) + { + size = lastSizeBeforeZero; + } + else + { + size *= 1.1f; } - List indicesForFace = new List() { side.baseLeftIndex, side.baseRightIndex, vertex2.id, vertex1.id }; - operation.AddFace(indicesForFace, originalFaceProperties); - } + scaleOffset++; } - if (extrusionFaceVertices.Count > 2) { - operation.AddFace(extrusionFaceVertices, originalFaceProperties); + + /// + /// Generates a new mesh with the extrusion performed upon it. + /// + /// Mesh to extrude. + /// The parameters indicating how to perform the extrusion. + /// All added vertices. + /// Extruded version of the mesh. + public MMesh DoExtrusion(MMesh mesh, ExtrusionParams extrusionParams, ref HashSet addedVertices) + { + heldFace.SetProperties(originalFaceProperties); + MMesh.GeometryOperation operation = mesh.StartOperation(); + operation.DeleteFace(heldFace.id); + + bool mergeOccurred; + List extrusionSides = + BuildExtrusionSides(this, _mesh, heldFace, extrusionParams, size, out mergeOccurred); + if (mergeOccurred && size != 0 && oldSize > size) + { + // If the vertices were merged because they were too close together on a resize down event, + // the size is effectively 0 and should be updated. The lastSizeBeforeZero reflects the size the face was + // before it had to be merged. + lastSizeBeforeZero = oldSize; + size = 0; + } + + List extrusionFaceVertices = new List(); + foreach (ExtrusionSideVertices side in extrusionSides) + { + Vector3 extrusion1Local = mesh.ModelCoordsToMeshCoords(side.extrusionLeft); + Vertex vertex1 = extrusionFaceVertices.FirstOrDefault(v => v.loc == extrusion1Local); + if (vertex1 == null) + { + vertex1 = operation.AddVertexMeshSpace(extrusion1Local); + + addedVertices.Add(vertex1); + extrusionFaceVertices.Add(vertex1); + } + if (side.isTriangle) + { + operation.AddFace(new List() { side.baseLeftIndex, side.baseRightIndex, vertex1.id }, + originalFaceProperties); + } + else + { + Vector3 extrusion2Local = mesh.ModelCoordsToMeshCoords(side.extrusionRight); + Vertex vertex2 = extrusionFaceVertices.FirstOrDefault(v => v.loc == extrusion2Local); + if (vertex2 == null) + { + vertex2 = operation.AddVertexMeshSpace(extrusion2Local); + addedVertices.Add(vertex2); + extrusionFaceVertices.Add(vertex2); + } + List indicesForFace = new List() { side.baseLeftIndex, side.baseRightIndex, vertex2.id, vertex1.id }; + operation.AddFace(indicesForFace, originalFaceProperties); + } + } + if (extrusionFaceVertices.Count > 2) + { + operation.AddFace(extrusionFaceVertices, originalFaceProperties); + } + operation.Commit(); + return mesh; } - operation.Commit(); - return mesh; - } - - public MMesh mesh { get { return _mesh; } } - public Face face { get { return heldFace; } } - - /// - /// Builds the sides of the extrusion. If the size is small enough, some vertices of the extrusion face may merge. - /// A size of zero means the vertices will merge into a point and you'll be extruding a conical shape. - /// - /// This method is public for testing. - /// - /// Mesh being extruded. - /// Face being extruded. - /// Parameters indicating how to perform the extrusion. - /// Current size of extrude face. - /// Whether or not this extrusion side creation needed to merge vertices of the face. - /// A list of extrusion sides. The sides contain info whether or not they are triangular. - public static List BuildExtrusionSides( - ExtrusionOperation extrusionOperation, MMesh mesh, Face face, - ExtrusionParams extrusionParams, float size, out bool mergeOccurred) { - mergeOccurred = false; - - Vector3 projectedDelta; - if (extrusionParams.lockToNormal) { - List coplanar = new List() { + + public MMesh mesh { get { return _mesh; } } + public Face face { get { return heldFace; } } + + /// + /// Builds the sides of the extrusion. If the size is small enough, some vertices of the extrusion face may merge. + /// A size of zero means the vertices will merge into a point and you'll be extruding a conical shape. + /// + /// This method is public for testing. + /// + /// Mesh being extruded. + /// Face being extruded. + /// Parameters indicating how to perform the extrusion. + /// Current size of extrude face. + /// Whether or not this extrusion side creation needed to merge vertices of the face. + /// A list of extrusion sides. The sides contain info whether or not they are triangular. + public static List BuildExtrusionSides( + ExtrusionOperation extrusionOperation, MMesh mesh, Face face, + ExtrusionParams extrusionParams, float size, out bool mergeOccurred) + { + mergeOccurred = false; + + Vector3 projectedDelta; + if (extrusionParams.lockToNormal) + { + List coplanar = new List() { mesh.VertexPositionInModelCoords(face.vertexIds[0]), mesh.VertexPositionInModelCoords(face.vertexIds[1]), mesh.VertexPositionInModelCoords(face.vertexIds[2]) }; - Vector3 normal = MeshMath.CalculateNormal(coplanar); - projectedDelta = - Vector3.Project(GridUtils.SnapToGrid(extrusionParams.translationModel), normal); - } else { - projectedDelta = extrusionParams.translationModel; - } + Vector3 normal = MeshMath.CalculateNormal(coplanar); + projectedDelta = + Vector3.Project(GridUtils.SnapToGrid(extrusionParams.translationModel), normal); + } + else + { + projectedDelta = extrusionParams.translationModel; + } - List originalFace = new List(face.vertexIds.Count); - for (int i = 0; i < face.vertexIds.Count; i++) { - originalFace.Add(mesh.VertexPositionInModelCoords(face.vertexIds[i])); - } - Vector3 extrudeFaceCenter = MeshMath.CalculateGeometricCenter(originalFace) + projectedDelta; - List extrudeFace = originalFace.Select(delegate (Vector3 v) { - Vector3 moved = v + projectedDelta; - - if (!extrusionParams.lockToNormal) { - // Rotate the point about the requested pivot. - Vector3 fromPivotToPosition = moved - extrusionParams.rotationPivotModel; - moved = extrusionParams.rotationPivotModel + extrusionParams.rotationModel * fromPivotToPosition; - } - - return extrudeFaceCenter + (moved - extrudeFaceCenter) * size; - }).ToList(); - - // Build the sides, using quads for each side. Later we'll figure out if we need to convert any of the quads - // into triangles. - bool requiresMerge = false; - List sides = new List(); - for (int i = 0; i < originalFace.Count; i++) { - int nextIndex = (i + 1) % originalFace.Count; - // Make a quad connecting corresponding sides of the original and extruded face. - ExtrusionSideVertices side = new ExtrusionSideVertices(extrusionOperation, - // Base left, first vertex of base (on original face): - originalFace[i], face.vertexIds[i], - // Base right, second vertex of base (on original face): - originalFace[nextIndex], face.vertexIds[nextIndex], - // Extrusion vertex left (on extruded face): - extrudeFace[i], - // Extrusion vertex right (on extruded face): - extrudeFace[nextIndex]); - sides.Add(side); - requiresMerge |= side.requiresMerge; - } + List originalFace = new List(face.vertexIds.Count); + for (int i = 0; i < face.vertexIds.Count; i++) + { + originalFace.Add(mesh.VertexPositionInModelCoords(face.vertexIds[i])); + } + Vector3 extrudeFaceCenter = MeshMath.CalculateGeometricCenter(originalFace) + projectedDelta; + List extrudeFace = originalFace.Select(delegate (Vector3 v) + { + Vector3 moved = v + projectedDelta; + + if (!extrusionParams.lockToNormal) + { + // Rotate the point about the requested pivot. + Vector3 fromPivotToPosition = moved - extrusionParams.rotationPivotModel; + moved = extrusionParams.rotationPivotModel + extrusionParams.rotationModel * fromPivotToPosition; + } + + return extrudeFaceCenter + (moved - extrudeFaceCenter) * size; + }).ToList(); + + // Build the sides, using quads for each side. Later we'll figure out if we need to convert any of the quads + // into triangles. + bool requiresMerge = false; + List sides = new List(); + for (int i = 0; i < originalFace.Count; i++) + { + int nextIndex = (i + 1) % originalFace.Count; + // Make a quad connecting corresponding sides of the original and extruded face. + ExtrusionSideVertices side = new ExtrusionSideVertices(extrusionOperation, + // Base left, first vertex of base (on original face): + originalFace[i], face.vertexIds[i], + // Base right, second vertex of base (on original face): + originalFace[nextIndex], face.vertexIds[nextIndex], + // Extrusion vertex left (on extruded face): + extrudeFace[i], + // Extrusion vertex right (on extruded face): + extrudeFace[nextIndex]); + sides.Add(side); + requiresMerge |= side.requiresMerge; + } - // Now we will start merging extrusion vertices, which means converting quadrilateral sides into triangular - // sides whenever we find a quadrilateral side where the extrusion vertices are too close together. - while (requiresMerge) { - requiresMerge = false; - // Look for a side that requires a merge (quads with extrusion vertices too close together). - int i; - for (i = 0; i < sides.Count; i++) { - if (sides[i].requiresMerge) { - // Perform the merge. Note that the merge may affect other sides, as the mutation propagates to - // adjacent faces. - Merge(sides, i); - // After the merge, sides may have changed, so we have to start over. - requiresMerge = true; - mergeOccurred = true; - break; + // Now we will start merging extrusion vertices, which means converting quadrilateral sides into triangular + // sides whenever we find a quadrilateral side where the extrusion vertices are too close together. + while (requiresMerge) + { + requiresMerge = false; + // Look for a side that requires a merge (quads with extrusion vertices too close together). + int i; + for (i = 0; i < sides.Count; i++) + { + if (sides[i].requiresMerge) + { + // Perform the merge. Note that the merge may affect other sides, as the mutation propagates to + // adjacent faces. + Merge(sides, i); + // After the merge, sides may have changed, so we have to start over. + requiresMerge = true; + mergeOccurred = true; + break; + } + } } - } - } - return sides; - } - - /// - /// Merges the two extrusion vertices on the given side, updating the other sides as necessary to propagate the - /// mutation. All other sides may potentially be mutated as a result of the merge. - /// - /// The array of sides. - /// The index of the side on which to perform the merge. Must be a quadrilateral side, not - /// triangular, since merging only makes sense on quadrilaterals. - private static void Merge(List sides, int sideToMerge) { - // Merging converts a quadrilateral face into a triangular face. - AssertOrThrow.True(!sides[sideToMerge].isTriangle, "Can't merge extrusion verts of a triangular face."); - - // Make it into a triangle. - sides[sideToMerge].ConvertToTriangle(); - Vector3 newPos = sides[sideToMerge].extrusionLeft; - - // One doesn't simply convert a side into a triangle. We must now propagate this mutation to the other sides. - // First, let's go to the left. We only have to propagate until we hit a quadrilateral. - for (int offsetLeft = 0; offsetLeft < sides.Count; offsetLeft++) { - // Index of the side we're looking at right now (corrected for circular indexing). - int thisSide = (sideToMerge - offsetLeft + sides.Count) % sides.Count; - // Move the side's right extrusion vertex to match with the updated extrusion vertex. - sides[thisSide].SetExtrusionRight(newPos); - // If this side is a quad, then it has a left and a right extrusion vertices, which means the propagation - // stops, because further faces to the left are not affected. - if (!sides[thisSide].isTriangle) break; + return sides; } - // Now let's propagate to the right until we hit a quad. - for (int offsetRight = 0; offsetRight < sides.Count; offsetRight++) { - // Index of the side we're looking at right now (corrected for circular indexing). - int thisSide = (sideToMerge + offsetRight + sides.Count) % sides.Count; - // Move the side's left extrusion vertex to match with the updated extrusion vertex. - sides[thisSide].SetExtrusionLeft(newPos); - // By the same logic as above, if we hit a quad, then we stop propagating because further faces to - // the right are not affected. - if (!sides[thisSide].isTriangle) break; + /// + /// Merges the two extrusion vertices on the given side, updating the other sides as necessary to propagate the + /// mutation. All other sides may potentially be mutated as a result of the merge. + /// + /// The array of sides. + /// The index of the side on which to perform the merge. Must be a quadrilateral side, not + /// triangular, since merging only makes sense on quadrilaterals. + private static void Merge(List sides, int sideToMerge) + { + // Merging converts a quadrilateral face into a triangular face. + AssertOrThrow.True(!sides[sideToMerge].isTriangle, "Can't merge extrusion verts of a triangular face."); + + // Make it into a triangle. + sides[sideToMerge].ConvertToTriangle(); + Vector3 newPos = sides[sideToMerge].extrusionLeft; + + // One doesn't simply convert a side into a triangle. We must now propagate this mutation to the other sides. + // First, let's go to the left. We only have to propagate until we hit a quadrilateral. + for (int offsetLeft = 0; offsetLeft < sides.Count; offsetLeft++) + { + // Index of the side we're looking at right now (corrected for circular indexing). + int thisSide = (sideToMerge - offsetLeft + sides.Count) % sides.Count; + // Move the side's right extrusion vertex to match with the updated extrusion vertex. + sides[thisSide].SetExtrusionRight(newPos); + // If this side is a quad, then it has a left and a right extrusion vertices, which means the propagation + // stops, because further faces to the left are not affected. + if (!sides[thisSide].isTriangle) break; + } + + // Now let's propagate to the right until we hit a quad. + for (int offsetRight = 0; offsetRight < sides.Count; offsetRight++) + { + // Index of the side we're looking at right now (corrected for circular indexing). + int thisSide = (sideToMerge + offsetRight + sides.Count) % sides.Count; + // Move the side's left extrusion vertex to match with the updated extrusion vertex. + sides[thisSide].SetExtrusionLeft(newPos); + // By the same logic as above, if we hit a quad, then we stop propagating because further faces to + // the right are not affected. + if (!sides[thisSide].isTriangle) break; + } } - } } diff --git a/Assets/Scripts/tools/Flipper.cs b/Assets/Scripts/tools/Flipper.cs index 4cc07d9f..ba0b99fb 100644 --- a/Assets/Scripts/tools/Flipper.cs +++ b/Assets/Scripts/tools/Flipper.cs @@ -18,94 +18,107 @@ using System.Linq; using UnityEngine; -namespace com.google.apps.peltzer.client.tools { - class Flipper { - /// - /// Enum to indicate which axis to flip over. - /// - private enum FlipAxis { X_AXIS, Y_AXIS, Z_AXIS }; +namespace com.google.apps.peltzer.client.tools +{ + class Flipper + { + /// + /// Enum to indicate which axis to flip over. + /// + private enum FlipAxis { X_AXIS, Y_AXIS, Z_AXIS }; - /// - /// Use the lastRotationModel of the controller to gauge if the controller is pointing up, - /// left, or right and return the appropriate axis to flip over. - /// - /// The rotation of the controller in model space. - /// The relevant FlipAxis enum value. - private static FlipAxis GetFlipAxis(Quaternion lastRotationModel) { - Vector3 controllerNormal = lastRotationModel * Vector3.forward; + /// + /// Use the lastRotationModel of the controller to gauge if the controller is pointing up, + /// left, or right and return the appropriate axis to flip over. + /// + /// The rotation of the controller in model space. + /// The relevant FlipAxis enum value. + private static FlipAxis GetFlipAxis(Quaternion lastRotationModel) + { + Vector3 controllerNormal = lastRotationModel * Vector3.forward; - float forwardAxisDiff = (controllerNormal - Vector3.forward).magnitude; - float rightAxisDiff = (controllerNormal - Vector3.right).magnitude; - float upAxisDiff = (controllerNormal - Vector3.up).magnitude; + float forwardAxisDiff = (controllerNormal - Vector3.forward).magnitude; + float rightAxisDiff = (controllerNormal - Vector3.right).magnitude; + float upAxisDiff = (controllerNormal - Vector3.up).magnitude; - if (Mathf.Min(forwardAxisDiff, rightAxisDiff, upAxisDiff) == forwardAxisDiff) { - return FlipAxis.X_AXIS; - } else if (Mathf.Min(forwardAxisDiff, rightAxisDiff, upAxisDiff) == rightAxisDiff) { - return FlipAxis.Z_AXIS; - } else { - return FlipAxis.Y_AXIS; - } - } + if (Mathf.Min(forwardAxisDiff, rightAxisDiff, upAxisDiff) == forwardAxisDiff) + { + return FlipAxis.X_AXIS; + } + else if (Mathf.Min(forwardAxisDiff, rightAxisDiff, upAxisDiff) == rightAxisDiff) + { + return FlipAxis.Z_AXIS; + } + else + { + return FlipAxis.Y_AXIS; + } + } - /// - /// Calculates the result of flipping the given meshes on an axis of symmetry determined by the controller - /// position. The flip will be pivoted about the centroid of the collection of meshes. This does not modify - /// the original meshes. - /// - /// A list of MMeshes. - /// The peltzer controller rotation, used to determine the flipping axis. - /// - /// Out parameter. If this method returns true, this will be a list of meshes that represents - /// the result of flipping the given meshes. The returned meshes are not added to the model (it's the caller's - /// responsibility to do so, if that is desired). - /// - /// True if the operation was successful (in which case the result out param indicates - /// the resulting flipped meshes). False if the operation failed (in which case result is in an - /// undefined state and shouldn't be used). - public static bool FlipMeshes(List meshesToFlip, Quaternion controllerRotation, - out List result) { - result = new List(); + /// + /// Calculates the result of flipping the given meshes on an axis of symmetry determined by the controller + /// position. The flip will be pivoted about the centroid of the collection of meshes. This does not modify + /// the original meshes. + /// + /// A list of MMeshes. + /// The peltzer controller rotation, used to determine the flipping axis. + /// + /// Out parameter. If this method returns true, this will be a list of meshes that represents + /// the result of flipping the given meshes. The returned meshes are not added to the model (it's the caller's + /// responsibility to do so, if that is desired). + /// + /// True if the operation was successful (in which case the result out param indicates + /// the resulting flipped meshes). False if the operation failed (in which case result is in an + /// undefined state and shouldn't be used). + public static bool FlipMeshes(List meshesToFlip, Quaternion controllerRotation, + out List result) + { + result = new List(); - // Find the centroid of the previews, which will be our flip pivot point. - Vector3 centroid = Math3d.FindCentroid(meshesToFlip); - FlipAxis flippingAxis = GetFlipAxis(controllerRotation); + // Find the centroid of the previews, which will be our flip pivot point. + Vector3 centroid = Math3d.FindCentroid(meshesToFlip); + FlipAxis flippingAxis = GetFlipAxis(controllerRotation); - foreach (MMesh originalMesh in meshesToFlip) { - // First, convert the vertices to model space, flip them about the model centroid over the chosen axis, - // and then convert back to mesh space. - Dictionary newVerticesById = new Dictionary(); - foreach (int vertexId in originalMesh.GetVertexIds()) { - Vector3 vertexModelSpace = originalMesh.VertexPositionInModelCoords(vertexId); - Vector3 newLocModelSpace = vertexModelSpace; - switch (flippingAxis) { - case FlipAxis.X_AXIS: - newLocModelSpace.x = centroid.x - (vertexModelSpace.x - centroid.x); - break; - case FlipAxis.Y_AXIS: - newLocModelSpace.y = centroid.y - (vertexModelSpace.y - centroid.y); - break; - default: - newLocModelSpace.z = centroid.z - (vertexModelSpace.z - centroid.z); - break; - } - Vector3 newLocMeshSpace = originalMesh.ModelCoordsToMeshCoords(newLocModelSpace); - newVerticesById.Add(vertexId, new Vertex(vertexId, newLocMeshSpace)); - } - // Flip the normals on each face around the axis of symmetry, and reverse the winding of its vertices. - Dictionary newFacesById = new Dictionary(); - foreach (Face originalFace in originalMesh.GetFaces()) { - int id = originalFace.id; - List newVertices = originalFace.vertexIds.Reverse().ToList(); + foreach (MMesh originalMesh in meshesToFlip) + { + // First, convert the vertices to model space, flip them about the model centroid over the chosen axis, + // and then convert back to mesh space. + Dictionary newVerticesById = new Dictionary(); + foreach (int vertexId in originalMesh.GetVertexIds()) + { + Vector3 vertexModelSpace = originalMesh.VertexPositionInModelCoords(vertexId); + Vector3 newLocModelSpace = vertexModelSpace; + switch (flippingAxis) + { + case FlipAxis.X_AXIS: + newLocModelSpace.x = centroid.x - (vertexModelSpace.x - centroid.x); + break; + case FlipAxis.Y_AXIS: + newLocModelSpace.y = centroid.y - (vertexModelSpace.y - centroid.y); + break; + default: + newLocModelSpace.z = centroid.z - (vertexModelSpace.z - centroid.z); + break; + } + Vector3 newLocMeshSpace = originalMesh.ModelCoordsToMeshCoords(newLocModelSpace); + newVerticesById.Add(vertexId, new Vertex(vertexId, newLocMeshSpace)); + } + // Flip the normals on each face around the axis of symmetry, and reverse the winding of its vertices. + Dictionary newFacesById = new Dictionary(); + foreach (Face originalFace in originalMesh.GetFaces()) + { + int id = originalFace.id; + List newVertices = originalFace.vertexIds.Reverse().ToList(); - newFacesById.Add(id, new Face(id, newVertices.AsReadOnly(), - newVerticesById, originalFace.properties)); + newFacesById.Add(id, new Face(id, newVertices.AsReadOnly(), + newVerticesById, originalFace.properties)); + } + MMesh newMesh = new MMesh(originalMesh.id, originalMesh.offset, originalMesh.rotation, + newVerticesById, newFacesById, originalMesh.groupId, + originalMesh.remixIds != null ? new HashSet(originalMesh.remixIds) : null); + result.Add(newMesh); + } + return true; } - MMesh newMesh = new MMesh(originalMesh.id, originalMesh.offset, originalMesh.rotation, - newVerticesById, newFacesById, originalMesh.groupId, - originalMesh.remixIds != null ? new HashSet(originalMesh.remixIds) : null); - result.Add(newMesh); - } - return true; } - } } diff --git a/Assets/Scripts/tools/Freeform.cs b/Assets/Scripts/tools/Freeform.cs index 58ddac60..6834082a 100644 --- a/Assets/Scripts/tools/Freeform.cs +++ b/Assets/Scripts/tools/Freeform.cs @@ -25,1299 +25,1465 @@ using com.google.apps.peltzer.client.tools.utils; using com.google.apps.peltzer.client.app; -namespace com.google.apps.peltzer.client.tools { - /// - /// A tool for inserting freeform 'strokes'. - /// - public class Freeform : MonoBehaviour { - /// - /// The threshold for reversing the direction of the stroke at start. - /// Set to 90 degrees because its a perfect right angle, this must be equal to or greater than 90. - /// - private const float REVERSE_FACE_ANGLE_THRESHOLD = 90f; - /// - /// The angle from the normal of a spine that we consider to be "backwards". - /// - private const float BACKWARDS_ANGLE_THRESHOLD = 90f; - /// - /// The number of spine.lengths backwards a user must move before we think they are intentionally trying to go - /// backwards. - /// - private const float BACKWARDS_DISTANCE_THRESHOLD = 0.9f; - /// - /// The distance a user has to move to enforce a pending checkpoint. - /// - private const float FORCE_CHECKPOINT_CHANGE_THRESHOLD = 0.04f; - /// - /// The number of spine.lengths we remove from the distance the user moved. This makes it so that the user - /// has to move further before we consider adding a new spine; making stroke creation more controlled. - /// - private const float CONTROLLER_CHANGE_THRESHOLD = 1f; - /// - /// The number of seconds the user scales an object continuously before we start increasing the rate of the - /// scaling process. - /// - private const float FAST_SCALE_THRESHOLD = 1f; - /// - /// If user is scaling for a what we consider a long time, we will increase the scaling rate by this amount. - /// - private const int LONG_TERM_SCALE_FACTOR = 2; - /// - /// The default scale of the stroke to be inserted. - /// - private const float DEFAULT_SCALE_INDEX = 7; - /// - /// The max scale of the stroke to be inserted. - /// - private const int MAX_SCALE_INDEX = 20; - /// - /// The min scale of the stroke to be inserted. - /// - private const int MIN_SCALE_INDEX = 1; - /// - /// The maximum number of vertices a face of a stroke can have. - /// - private const int MAX_VERTEX_COUNT = 10; - /// - /// The minimum number of vertices a face of a stroke can have. - /// - private const int MIN_VERTEX_COUNT = 3; - /// - /// The maximum number of checkpoints a stroke can have before we segment it. - /// - private const int MAX_CHECKPOINT_COUNT = 15; - /// - /// Handle floating point errors. - /// - private float EPSILON = 0.1f; +namespace com.google.apps.peltzer.client.tools +{ + /// + /// A tool for inserting freeform 'strokes'. + /// + public class Freeform : MonoBehaviour + { + /// + /// The threshold for reversing the direction of the stroke at start. + /// Set to 90 degrees because its a perfect right angle, this must be equal to or greater than 90. + /// + private const float REVERSE_FACE_ANGLE_THRESHOLD = 90f; + /// + /// The angle from the normal of a spine that we consider to be "backwards". + /// + private const float BACKWARDS_ANGLE_THRESHOLD = 90f; + /// + /// The number of spine.lengths backwards a user must move before we think they are intentionally trying to go + /// backwards. + /// + private const float BACKWARDS_DISTANCE_THRESHOLD = 0.9f; + /// + /// The distance a user has to move to enforce a pending checkpoint. + /// + private const float FORCE_CHECKPOINT_CHANGE_THRESHOLD = 0.04f; + /// + /// The number of spine.lengths we remove from the distance the user moved. This makes it so that the user + /// has to move further before we consider adding a new spine; making stroke creation more controlled. + /// + private const float CONTROLLER_CHANGE_THRESHOLD = 1f; + /// + /// The number of seconds the user scales an object continuously before we start increasing the rate of the + /// scaling process. + /// + private const float FAST_SCALE_THRESHOLD = 1f; + /// + /// If user is scaling for a what we consider a long time, we will increase the scaling rate by this amount. + /// + private const int LONG_TERM_SCALE_FACTOR = 2; + /// + /// The default scale of the stroke to be inserted. + /// + private const float DEFAULT_SCALE_INDEX = 7; + /// + /// The max scale of the stroke to be inserted. + /// + private const int MAX_SCALE_INDEX = 20; + /// + /// The min scale of the stroke to be inserted. + /// + private const int MIN_SCALE_INDEX = 1; + /// + /// The maximum number of vertices a face of a stroke can have. + /// + private const int MAX_VERTEX_COUNT = 10; + /// + /// The minimum number of vertices a face of a stroke can have. + /// + private const int MIN_VERTEX_COUNT = 3; + /// + /// The maximum number of checkpoints a stroke can have before we segment it. + /// + private const int MAX_CHECKPOINT_COUNT = 15; + /// + /// Handle floating point errors. + /// + private float EPSILON = 0.1f; + + /// + /// The distance between the controller and the *edge* (not the center) of the stroke hint. + /// The position of the stroke is adjusted so that approximately this much of a gap, in WORLD SPACE, + /// is kept between the controller and the edge of the stroke. This is expressed in world space + /// because we want this to be independent of zoom level, for UX purposes (otherwise zooming in + /// would make the stroke seem further away, which is confusing). + /// + /// Once the user starts to draw, though, this is converted to a fixed offset in model space that is + /// retained during the entirety of the drawing operation, regardless of zoom level. + /// + private const float DISTANCE_TO_PREVIEW_EDGE_WORLD_SPACE = 0.01f; + + private PeltzerController peltzerController; + private Model model; + private AudioLibrary audioLibrary; + private WorldSpace worldSpace; + + private ScaleType scaleType = ScaleType.NONE; + + /// + /// The current number of vertices of the stroke face. + /// + private int vertexCount = 4; + /// + /// The scale of the shape to be inserted. + /// + private float insertScaleIndex = DEFAULT_SCALE_INDEX; + /// + /// The scale of the current front-face. Stored separately so that when a stroke is complete, we can revert to the + /// prior starting scale. + /// + private float currentScaleIndex; + + /// + /// Whether an insertion is in progress. + /// + private bool insertionInProgress; + /// + /// Whether we are snapping. + /// + private bool isSnapping; + /// + /// Whether the user is manually checkpointing. + /// + private bool isManualCheckpointing; + /// + // The benchmark we set to determine when the user has been scaling for a long time. + /// + private float longTermScaleStartTime = float.MaxValue; + /// + /// All the MMesh segments of the current stroke excluding the currentVolume. + /// + private List strokeVolumeSegments; + /// + /// The current MMesh for the stroke being inserted. + /// + private MMesh currentVolume; + /// + /// The preview of the stroke being inserted. + /// + private GameObject currentHighlight; + /// + /// The face being moved to form the stroke. + /// + private Face currentFrontFace; + /// + /// The face not being moved to form the stroke. + /// + private Face currentBackFace; + /// + /// The location of each vertex at the time the last checkpoint was added. + /// + private Dictionary originalVertexLocations; + /// + /// The structure that defines the stroke and all possible positions for the next stroke segment using the last + /// spine in strokeSpine. + /// + private List strokeSpine = new List(); + /// + /// The offset from the controller position. + /// + private Vector3 freeformModelSpaceOffsetWhileDrawing; + /// + /// The normal of the front face at start. This is used to determine if we should reverse the front face. + /// + private Vector3 frontFaceNormalAtStart; + /// + /// The position of the controller at the start of a stroke. + /// + private Vector3 controllerPositionAtStart; + /// + /// The distance that a user has to move for us to lock in which face is going to be the front face and start making + /// the stroke. + /// + private float chooseFaceDistance; + /// + /// The axis that defines the plane that the freeform is being made in when snapping. + /// + private Vector3 definingAxis; + /// + /// Allows the Spine logic to tell the Freeform to checkpoint. This happens when a user moves back on themself and + /// we want to autogenerate a portion of the spine. + /// + private bool waitingToForceCheckpoint; + /// + /// Where the controller was when the Spine logic told the freeform it should be checkpointing. This will be used to + /// determine if the controller has moved enough to enforce the checkpoint. + /// + private Vector3 controllerPositionAtPromptToCheckpoint; + /// + /// The number of checkpoints in the currentVolume. When the current number of checkpoints hits the maximum number + /// we will segment the stroke. + /// + private int numCheckpointsInCurrentVolume; + /// + /// Used to determine if we should show the snap tooltip or not. Don't show the tooltip if the user already + /// showed enough knowledge of how to snap. + /// + private int completedSnaps = 0; + private const int SNAP_KNOW_HOW_COUNT = 3; + /// + /// Because snaps during strokes are triggered in rapid succession when the trigger is held down, count + /// one completed snap per stroke only. + /// + private bool recordedSnapThisStroke = false; + + // Detection for trigger down & straight back up, vs trigger down and hold -- either of which + // begins a stroke. + private bool triggerUpToEnd; + private bool waitingToDetermineReleaseType; + private float triggerDownTime; + + // Controller UI elements. + private GameObject strokeOverlay_CENTER; + private Vector3 strokeOriginAtLastCheckpoint; + + /// + /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. + /// As such, this setup method must be called before the tool is used for it to have a valid state. + /// + public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, + AudioLibrary audioLibrary, WorldSpace worldSpace) + { + // Nothing interesting to see here... + this.model = model; + this.peltzerController = peltzerController; + this.audioLibrary = audioLibrary; + this.worldSpace = worldSpace; + controllerMain.ControllerActionHandler += ControllerEventHandler; + peltzerController.MaterialChangedHandler += MaterialChangeHandler; + peltzerController.ModeChangedHandler += ModeChangedHandler; + CreateNewVolumeAndHighlight(); + currentHighlight.SetActive(peltzerController.mode == ControllerMode.insertStroke); + } - /// - /// The distance between the controller and the *edge* (not the center) of the stroke hint. - /// The position of the stroke is adjusted so that approximately this much of a gap, in WORLD SPACE, - /// is kept between the controller and the edge of the stroke. This is expressed in world space - /// because we want this to be independent of zoom level, for UX purposes (otherwise zooming in - /// would make the stroke seem further away, which is confusing). - /// - /// Once the user starts to draw, though, this is converted to a fixed offset in model space that is - /// retained during the entirety of the drawing operation, regardless of zoom level. - /// - private const float DISTANCE_TO_PREVIEW_EDGE_WORLD_SPACE = 0.01f; + /// + /// Creates a new MMesh and a highlight (GameObject) for a new stroke. + /// + private void CreateNewVolumeAndHighlight() + { + int id = model.GenerateMeshId(); + currentVolume = new MMesh(id, Vector3.zero, Quaternion.identity, new Dictionary(vertexCount * 2), + new Dictionary(2 + vertexCount * 2)); + MMesh.GeometryOperation meshConstructionOperation = currentVolume.StartOperation(); + + originalVertexLocations = new Dictionary(vertexCount * 2); + + // If we had something prior, we'll delete it when we're done. + GameObject previousHighlight = null; + if (currentHighlight != null) + { + previousHighlight = currentHighlight; + } - private PeltzerController peltzerController; - private Model model; - private AudioLibrary audioLibrary; - private WorldSpace worldSpace; + // The vertices and faces of the MMesh we're creating. + // Go in a circle and add the currently-selected number of vertices for the front and back faces. + float scale = (GridUtils.GRID_SIZE / 2f) * insertScaleIndex; + + // Determine the width so that it is approximately a spine length. + float width = GridUtils.GRID_SIZE / 2.0f; + List frontVertIds = new List(vertexCount); + List backVertIds = new List(vertexCount); + for (int i = 0; i < vertexCount; i++) + { + float theta = (Mathf.PI / 2f) + i * (2 * Mathf.PI / vertexCount); + // We add a ring of vertices for the 'back face'. + Vertex backVert = meshConstructionOperation.AddVertexMeshSpace( + new Vector3(Mathf.Cos(theta) * scale, 0f, Mathf.Sin(theta) * scale)); + backVertIds.Add(backVert.id); + // Remember the original vertex locations, so we can correctly compute deltas. + originalVertexLocations[backVert.id] = backVert.loc; + + // And a ring of vertices for the 'front face' + Vertex frontVert = meshConstructionOperation.AddVertexMeshSpace( + new Vector3(Mathf.Cos(theta) * scale, width, Mathf.Sin(theta) * scale)); + frontVertIds.Add(frontVert.id); + originalVertexLocations[frontVert.id] = frontVert.loc; + } - private ScaleType scaleType = ScaleType.NONE; + // Create the 'side faces' with clockwise ordering. Each side face has 4 verts. + for (int i = 0; i < vertexCount; i++) + { + List vertexIds = new List() { + frontVertIds[i], + frontVertIds[(i + 1) % frontVertIds.Count], + backVertIds[(i + 1) % backVertIds.Count], + backVertIds[i] }; - /// - /// The current number of vertices of the stroke face. - /// - private int vertexCount = 4; - /// - /// The scale of the shape to be inserted. - /// - private float insertScaleIndex = DEFAULT_SCALE_INDEX; - /// - /// The scale of the current front-face. Stored separately so that when a stroke is complete, we can revert to the - /// prior starting scale. - /// - private float currentScaleIndex; - /// - /// Whether an insertion is in progress. - /// - private bool insertionInProgress; - /// - /// Whether we are snapping. - /// - private bool isSnapping; - /// - /// Whether the user is manually checkpointing. - /// - private bool isManualCheckpointing; - /// - // The benchmark we set to determine when the user has been scaling for a long time. - /// - private float longTermScaleStartTime = float.MaxValue; - /// - /// All the MMesh segments of the current stroke excluding the currentVolume. - /// - private List strokeVolumeSegments; - /// - /// The current MMesh for the stroke being inserted. - /// - private MMesh currentVolume; - /// - /// The preview of the stroke being inserted. - /// - private GameObject currentHighlight; - /// - /// The face being moved to form the stroke. - /// - private Face currentFrontFace; - /// - /// The face not being moved to form the stroke. - /// - private Face currentBackFace; - /// - /// The location of each vertex at the time the last checkpoint was added. - /// - private Dictionary originalVertexLocations; - /// - /// The structure that defines the stroke and all possible positions for the next stroke segment using the last - /// spine in strokeSpine. - /// - private List strokeSpine = new List(); - /// - /// The offset from the controller position. - /// - private Vector3 freeformModelSpaceOffsetWhileDrawing; - /// - /// The normal of the front face at start. This is used to determine if we should reverse the front face. - /// - private Vector3 frontFaceNormalAtStart; - /// - /// The position of the controller at the start of a stroke. - /// - private Vector3 controllerPositionAtStart; - /// - /// The distance that a user has to move for us to lock in which face is going to be the front face and start making - /// the stroke. - /// - private float chooseFaceDistance; - /// - /// The axis that defines the plane that the freeform is being made in when snapping. - /// - private Vector3 definingAxis; - /// - /// Allows the Spine logic to tell the Freeform to checkpoint. This happens when a user moves back on themself and - /// we want to autogenerate a portion of the spine. - /// - private bool waitingToForceCheckpoint; - /// - /// Where the controller was when the Spine logic told the freeform it should be checkpointing. This will be used to - /// determine if the controller has moved enough to enforce the checkpoint. - /// - private Vector3 controllerPositionAtPromptToCheckpoint; - /// - /// The number of checkpoints in the currentVolume. When the current number of checkpoints hits the maximum number - /// we will segment the stroke. - /// - private int numCheckpointsInCurrentVolume; - /// - /// Used to determine if we should show the snap tooltip or not. Don't show the tooltip if the user already - /// showed enough knowledge of how to snap. - /// - private int completedSnaps = 0; - private const int SNAP_KNOW_HOW_COUNT = 3; - /// - /// Because snaps during strokes are triggered in rapid succession when the trigger is held down, count - /// one completed snap per stroke only. - /// - private bool recordedSnapThisStroke = false; + // Note: We don't need to calculate the normals here, they will be recalculated once we are done manipulating + // these faces in the stroke. + meshConstructionOperation.AddFace(vertexIds, new FaceProperties(peltzerController.currentMaterial)); + } - // Detection for trigger down & straight back up, vs trigger down and hold -- either of which - // begins a stroke. - private bool triggerUpToEnd; - private bool waitingToDetermineReleaseType; - private float triggerDownTime; + // Create the front and back faces. + frontVertIds.Reverse(); + currentFrontFace = meshConstructionOperation.AddFace(frontVertIds, new FaceProperties(peltzerController.currentMaterial)); + currentBackFace = meshConstructionOperation.AddFace(backVertIds, new FaceProperties(peltzerController.currentMaterial)); - // Controller UI elements. - private GameObject strokeOverlay_CENTER; - private Vector3 strokeOriginAtLastCheckpoint; + meshConstructionOperation.Commit(); + currentVolume.RecalcBounds(); - /// - /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. - /// As such, this setup method must be called before the tool is used for it to have a valid state. - /// - public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, - AudioLibrary audioLibrary, WorldSpace worldSpace) { - // Nothing interesting to see here... - this.model = model; - this.peltzerController = peltzerController; - this.audioLibrary = audioLibrary; - this.worldSpace = worldSpace; - controllerMain.ControllerActionHandler += ControllerEventHandler; - peltzerController.MaterialChangedHandler += MaterialChangeHandler; - peltzerController.ModeChangedHandler += ModeChangedHandler; - CreateNewVolumeAndHighlight(); - currentHighlight.SetActive(peltzerController.mode == ControllerMode.insertStroke); - } + // We generate the highlight directly and don't go via the cache as this isn't a permanent mesh for which + // we want a cached highlight. + currentHighlight = MeshHelper.GameObjectFromMMesh(worldSpace, currentVolume); - /// - /// Creates a new MMesh and a highlight (GameObject) for a new stroke. - /// - private void CreateNewVolumeAndHighlight() { - int id = model.GenerateMeshId(); - currentVolume = new MMesh(id, Vector3.zero, Quaternion.identity, new Dictionary(vertexCount * 2), - new Dictionary(2 + vertexCount * 2)); - MMesh.GeometryOperation meshConstructionOperation = currentVolume.StartOperation(); - - originalVertexLocations = new Dictionary(vertexCount * 2); - - // If we had something prior, we'll delete it when we're done. - GameObject previousHighlight = null; - if (currentHighlight != null) { - previousHighlight = currentHighlight; - } - - // The vertices and faces of the MMesh we're creating. - // Go in a circle and add the currently-selected number of vertices for the front and back faces. - float scale = (GridUtils.GRID_SIZE / 2f) * insertScaleIndex; - - // Determine the width so that it is approximately a spine length. - float width = GridUtils.GRID_SIZE / 2.0f; - List frontVertIds = new List(vertexCount); - List backVertIds = new List(vertexCount); - for (int i = 0; i < vertexCount; i++) { - float theta = (Mathf.PI / 2f) + i * (2 * Mathf.PI / vertexCount); - // We add a ring of vertices for the 'back face'. - Vertex backVert = meshConstructionOperation.AddVertexMeshSpace( - new Vector3(Mathf.Cos(theta) * scale, 0f, Mathf.Sin(theta) * scale)); - backVertIds.Add(backVert.id); - // Remember the original vertex locations, so we can correctly compute deltas. - originalVertexLocations[backVert.id] = backVert.loc; - - // And a ring of vertices for the 'front face' - Vertex frontVert = meshConstructionOperation.AddVertexMeshSpace( - new Vector3(Mathf.Cos(theta) * scale, width, Mathf.Sin(theta) * scale)); - frontVertIds.Add(frontVert.id); - originalVertexLocations[frontVert.id] = frontVert.loc; - } - - // Create the 'side faces' with clockwise ordering. Each side face has 4 verts. - for (int i = 0; i < vertexCount; i++) { - List vertexIds = new List() { - frontVertIds[i], - frontVertIds[(i + 1) % frontVertIds.Count], - backVertIds[(i + 1) % backVertIds.Count], - backVertIds[i] }; - - - // Note: We don't need to calculate the normals here, they will be recalculated once we are done manipulating - // these faces in the stroke. - meshConstructionOperation.AddFace(vertexIds, new FaceProperties(peltzerController.currentMaterial)); - } - - // Create the front and back faces. - frontVertIds.Reverse(); - currentFrontFace = meshConstructionOperation.AddFace(frontVertIds, new FaceProperties(peltzerController.currentMaterial)); - currentBackFace = meshConstructionOperation.AddFace(backVertIds, new FaceProperties(peltzerController.currentMaterial)); - - meshConstructionOperation.Commit(); - currentVolume.RecalcBounds(); - - // We generate the highlight directly and don't go via the cache as this isn't a permanent mesh for which - // we want a cached highlight. - currentHighlight = MeshHelper.GameObjectFromMMesh(worldSpace, currentVolume); - - // Force an update to get the position and rotation right, so we don't see a flash. - UpdatePreviewPositionAndRotation(); - - // Destroy the previous preview, if any. - if (previousHighlight != null) { - GameObject.Destroy(previousHighlight); - } - } + // Force an update to get the position and rotation right, so we don't see a flash. + UpdatePreviewPositionAndRotation(); - /// - /// Creates a new MMesh and a highlight (GameObject) for a new segment of the stroke from the current front face. - /// - private void CreateNewVolumeAndHighlightSegment() { - int id = model.GenerateMeshId(); - // If we had something prior, we'll delete it when we're done. - GameObject previousHighlight = null; - if (currentHighlight != null) { - previousHighlight = currentHighlight; - } - - // Create the mesh and its highlight. - MMesh newVolume = - new MMesh(id, currentVolume.offset, currentVolume.rotation, - new Dictionary(vertexCount * 2), new Dictionary(2 + vertexCount * 2)); - - MMesh.GeometryOperation segmentOperation = newVolume.StartOperation(); - - originalVertexLocations = new Dictionary(vertexCount * 2); - - List frontVertIds = new List(vertexCount); - List backVertIds = new List(vertexCount); - - - // Determine the width so that it is approximately a spine length. - for (int i = 0; i < vertexCount; i++) { - // We add a ring of vertices for the 'back face'. - Vertex backVert = segmentOperation.AddVertexMeshSpace( - currentVolume.VertexPositionInMeshCoords(currentFrontFace.vertexIds[(vertexCount - 1) - i])); - backVertIds.Add(backVert.id); - // Remember the original vertex locations, so we can correctly compute deltas. - originalVertexLocations[backVert.id] = backVert.loc; - - // And a ring of vertices for the 'front face' - Vertex frontVert = segmentOperation.AddVertexMeshSpace( - currentVolume.VertexPositionInMeshCoords(currentFrontFace.vertexIds[(vertexCount - 1) - i])); - frontVertIds.Add(frontVert.id); - originalVertexLocations[frontVert.id] = frontVert.loc; - } - - // Create the 'side faces' with clockwise ordering. Each side face has 4 verts. - for (int i = 0; i < vertexCount; i++) { - List vertexIds = new List() { + // Destroy the previous preview, if any. + if (previousHighlight != null) + { + GameObject.Destroy(previousHighlight); + } + } + + /// + /// Creates a new MMesh and a highlight (GameObject) for a new segment of the stroke from the current front face. + /// + private void CreateNewVolumeAndHighlightSegment() + { + int id = model.GenerateMeshId(); + // If we had something prior, we'll delete it when we're done. + GameObject previousHighlight = null; + if (currentHighlight != null) + { + previousHighlight = currentHighlight; + } + + // Create the mesh and its highlight. + MMesh newVolume = + new MMesh(id, currentVolume.offset, currentVolume.rotation, + new Dictionary(vertexCount * 2), new Dictionary(2 + vertexCount * 2)); + + MMesh.GeometryOperation segmentOperation = newVolume.StartOperation(); + + originalVertexLocations = new Dictionary(vertexCount * 2); + + List frontVertIds = new List(vertexCount); + List backVertIds = new List(vertexCount); + + + // Determine the width so that it is approximately a spine length. + for (int i = 0; i < vertexCount; i++) + { + // We add a ring of vertices for the 'back face'. + Vertex backVert = segmentOperation.AddVertexMeshSpace( + currentVolume.VertexPositionInMeshCoords(currentFrontFace.vertexIds[(vertexCount - 1) - i])); + backVertIds.Add(backVert.id); + // Remember the original vertex locations, so we can correctly compute deltas. + originalVertexLocations[backVert.id] = backVert.loc; + + // And a ring of vertices for the 'front face' + Vertex frontVert = segmentOperation.AddVertexMeshSpace( + currentVolume.VertexPositionInMeshCoords(currentFrontFace.vertexIds[(vertexCount - 1) - i])); + frontVertIds.Add(frontVert.id); + originalVertexLocations[frontVert.id] = frontVert.loc; + } + + // Create the 'side faces' with clockwise ordering. Each side face has 4 verts. + for (int i = 0; i < vertexCount; i++) + { + List vertexIds = new List() { frontVertIds[i], frontVertIds[(i + 1) % frontVertIds.Count], backVertIds[(i + 1) % backVertIds.Count], backVertIds[i] }; - - - // Note: We don't need to calculate the normals here, they will be recalculated once we are done manipulating - // these faces in the stroke. - segmentOperation.AddFace(vertexIds, new FaceProperties(peltzerController.currentMaterial)); - } - - // Create the front and back faces. - frontVertIds.Reverse(); - currentFrontFace = segmentOperation.AddFace(frontVertIds, new FaceProperties(peltzerController.currentMaterial)); - currentBackFace = segmentOperation.AddFace(backVertIds, new FaceProperties(peltzerController.currentMaterial)); - - segmentOperation.Commit(); - newVolume.RecalcBounds(); - - currentVolume = newVolume; - - // We generate the highlight directly and don't go via the cache as this isn't a permanent mesh for which - // we want a cached highlight. - currentHighlight = MeshHelper.GameObjectFromMMesh(worldSpace, currentVolume); - - // Destroy the previous preview, if any. - if (previousHighlight != null) { - GameObject.Destroy(previousHighlight); - } - } - private void Update() { - // Nothing to see here... - if (!PeltzerController.AcquireIfNecessary(ref peltzerController) || - peltzerController.mode != ControllerMode.insertStroke) { - return; - } - - // If the Freeform was told to force checkpoint by the Spine logic see if the user has moved far enough yet to - // do this. - if (waitingToForceCheckpoint && Vector3.Distance(controllerPositionAtPromptToCheckpoint, - worldSpace.ModelToWorld(peltzerController.LastPositionModel)) > FORCE_CHECKPOINT_CHANGE_THRESHOLD) { - AddCheckpoint(); - AddSpine(); - } - - UpdatePreviewPositionAndRotation(); - } - /// - /// Updates the freeform tool by either: positioning and rotating the preview if the user is not inserting a - /// stroke yet, or adding spines to the stroke to fill the space between the last spine and the controller. - /// - private void UpdatePreviewPositionAndRotation() { - if (waitingToDetermineReleaseType) { - // If a stroke is in progress, and the trigger has been down for longer than WAIT_THRESHOLD, then this is - // a hold-trigger-and-drag operation which can be completed by raising the trigger. - if (Time.time - triggerDownTime > PeltzerController.SINGLE_CLICK_THRESHOLD) { - waitingToDetermineReleaseType = false; - triggerUpToEnd = true; + // Note: We don't need to calculate the normals here, they will be recalculated once we are done manipulating + // these faces in the stroke. + segmentOperation.AddFace(vertexIds, new FaceProperties(peltzerController.currentMaterial)); + } + + // Create the front and back faces. + frontVertIds.Reverse(); + currentFrontFace = segmentOperation.AddFace(frontVertIds, new FaceProperties(peltzerController.currentMaterial)); + currentBackFace = segmentOperation.AddFace(backVertIds, new FaceProperties(peltzerController.currentMaterial)); + + segmentOperation.Commit(); + newVolume.RecalcBounds(); + + currentVolume = newVolume; + + // We generate the highlight directly and don't go via the cache as this isn't a permanent mesh for which + // we want a cached highlight. + currentHighlight = MeshHelper.GameObjectFromMMesh(worldSpace, currentVolume); + + // Destroy the previous preview, if any. + if (previousHighlight != null) + { + GameObject.Destroy(previousHighlight); + } } - } - - // Determine the offset from the controller depending on mode. - if (!insertionInProgress) { - currentHighlight.SetActive(!PeltzerMain.Instance.peltzerController.isPointingAtMenu); - - MeshWithMaterialRenderer renderer = currentHighlight.GetComponent(); - - // The user isn't in the middle of making a freeform stroke, - // place/rotate the preview based on the current controller position/rotation. - renderer.SetPositionModelSpace(peltzerController.LastPositionModel + GetFreeformPreviewOffSetModelSpace()); - - // If we are snapping we want to help the user create an arch by orientating the preview to their eyes and their - // body's natural rotation. This means their arm movement will be aligned with the preview and they will create - // better arches naturally. - Quaternion focalRotation = Quaternion.Euler(0, Camera.main.transform.rotation.eulerAngles.y, 0); - - Quaternion rotation = isSnapping ? - GridUtils.SnapToNearest(peltzerController.LastRotationWorld, Quaternion.identity, 90f) : - peltzerController.LastRotationWorld; - - // Level out the rotation so that an edge always points down. We only have to do this if the polygonal face has - // an even number of vertices. The odd number faces do this naturally. We want to do this since a user will - // naturally create an arch with a downward swipe. - if (vertexCount % 2 == 0) { - rotation = rotation * Quaternion.Euler(0f, 180f / vertexCount, 0f); + + private void Update() + { + // Nothing to see here... + if (!PeltzerController.AcquireIfNecessary(ref peltzerController) || + peltzerController.mode != ControllerMode.insertStroke) + { + return; + } + + // If the Freeform was told to force checkpoint by the Spine logic see if the user has moved far enough yet to + // do this. + if (waitingToForceCheckpoint && Vector3.Distance(controllerPositionAtPromptToCheckpoint, + worldSpace.ModelToWorld(peltzerController.LastPositionModel)) > FORCE_CHECKPOINT_CHANGE_THRESHOLD) + { + AddCheckpoint(); + AddSpine(); + } + + UpdatePreviewPositionAndRotation(); } - renderer.SetOrientationModelSpace(worldSpace.WorldOrientationToModel(rotation), /* smooth */ false); - } else if (strokeSpine.Count() == 0) { - // We have yet to determine which is going to be the "front face". - Vector3 controllerChange = peltzerController.LastPositionModel - controllerPositionAtStart; - - // If the user has moved a certain distance since start we'll decide. - if (controllerChange.magnitude > chooseFaceDistance) { - // We'll make the back face the front face if the user is moving in the opposite direction than the front - // face normal. - if (Vector3.Angle(frontFaceNormalAtStart, controllerChange) > REVERSE_FACE_ANGLE_THRESHOLD) { - Face faceTemp = currentFrontFace; - currentFrontFace = currentBackFace; - currentBackFace = faceTemp; - } - - StartSpine(); + /// + /// Updates the freeform tool by either: positioning and rotating the preview if the user is not inserting a + /// stroke yet, or adding spines to the stroke to fill the space between the last spine and the controller. + /// + private void UpdatePreviewPositionAndRotation() + { + if (waitingToDetermineReleaseType) + { + // If a stroke is in progress, and the trigger has been down for longer than WAIT_THRESHOLD, then this is + // a hold-trigger-and-drag operation which can be completed by raising the trigger. + if (Time.time - triggerDownTime > PeltzerController.SINGLE_CLICK_THRESHOLD) + { + waitingToDetermineReleaseType = false; + triggerUpToEnd = true; + } + } + + // Determine the offset from the controller depending on mode. + if (!insertionInProgress) + { + currentHighlight.SetActive(!PeltzerMain.Instance.peltzerController.isPointingAtMenu); + + MeshWithMaterialRenderer renderer = currentHighlight.GetComponent(); + + // The user isn't in the middle of making a freeform stroke, + // place/rotate the preview based on the current controller position/rotation. + renderer.SetPositionModelSpace(peltzerController.LastPositionModel + GetFreeformPreviewOffSetModelSpace()); + + // If we are snapping we want to help the user create an arch by orientating the preview to their eyes and their + // body's natural rotation. This means their arm movement will be aligned with the preview and they will create + // better arches naturally. + Quaternion focalRotation = Quaternion.Euler(0, Camera.main.transform.rotation.eulerAngles.y, 0); + + Quaternion rotation = isSnapping ? + GridUtils.SnapToNearest(peltzerController.LastRotationWorld, Quaternion.identity, 90f) : + peltzerController.LastRotationWorld; + + // Level out the rotation so that an edge always points down. We only have to do this if the polygonal face has + // an even number of vertices. The odd number faces do this naturally. We want to do this since a user will + // naturally create an arch with a downward swipe. + if (vertexCount % 2 == 0) + { + rotation = rotation * Quaternion.Euler(0f, 180f / vertexCount, 0f); + } + + renderer.SetOrientationModelSpace(worldSpace.WorldOrientationToModel(rotation), /* smooth */ false); + } + else if (strokeSpine.Count() == 0) + { + // We have yet to determine which is going to be the "front face". + Vector3 controllerChange = peltzerController.LastPositionModel - controllerPositionAtStart; + + // If the user has moved a certain distance since start we'll decide. + if (controllerChange.magnitude > chooseFaceDistance) + { + // We'll make the back face the front face if the user is moving in the opposite direction than the front + // face normal. + if (Vector3.Angle(frontFaceNormalAtStart, controllerChange) > REVERSE_FACE_ANGLE_THRESHOLD) + { + Face faceTemp = currentFrontFace; + currentFrontFace = currentBackFace; + currentBackFace = faceTemp; + } + + StartSpine(); + } + } + else + { + Vector3 controllerPosition = (peltzerController.LastPositionModel + freeformModelSpaceOffsetWhileDrawing); + + // If we are creating a snapped freeform we only want to consider controller movement in the plane the freeform + // is being built on. + controllerPosition = definingAxis == Vector3.zero || !isSnapping ? + controllerPosition : + Math3d.ProjectPointOnPlane(definingAxis, strokeSpine.Last().origin, controllerPosition); + + // Find the vector from the origin of the last spine to the controller position. + Vector3 controllerChange = controllerPosition - strokeSpine.Last().origin; + + // Find the distance we want to fill with spines. This is the distance from the origin to the controller + // minus a threshold. The threshold makes the user have to move more before creating a spine giving stroke + // creation more stability. + float distanceToFill = controllerChange.magnitude - CONTROLLER_CHANGE_THRESHOLD * strokeSpine.Last().length; + + // Find how much space there is between the controller and the stroke being made. + // To generate the stroke we will fill this distance with as many spines as possible. + float estimatedSpinesToAdd = Mathf.Floor(distanceToFill / strokeSpine.Last().length); + + // We only auto-generate spines if the user isn't checkpointing. If they are then they are responsible for + // generating their own spines with physically added checkpoints. + if (!isManualCheckpointing) + { + // Prevent the user from moving back on the stroke accidentally. A stroke will turn around to start building + // itself backwards but this should only happen if the user moves far enough away. + if (Vector3.Angle(strokeSpine.Last().normal, controllerChange) > BACKWARDS_ANGLE_THRESHOLD + && distanceToFill < strokeSpine.Last().length * BACKWARDS_DISTANCE_THRESHOLD) + { + return; + } + + + + // While there is still space between the controller and the stroke that we can fill with a spine, find the + // right spine. We pre-compute the number of spines and use a for loop instead of a while loop that checks on + // every checkpoint if there is still room for a new spine. This prevents the code from getting stuck in a + // while loop on update crashing the app. This can happen if the generated geometry causes the stroke to loop + // around the controller position. + for (int i = 0; i < estimatedSpinesToAdd; i++) + { + // Now that we've updated the front face to be in the correct position we see if we should checkpoint or + // "extend" the last spine. We don't actually lengthen the spine but it appears that way because we don't + // checkpoint in between the current spine and the previous spine. + UpdateSpine(); + + if (!SpineIsAligned()) + { + AddCheckpoint(); + + // If this checkpoint causes us to insert an invalid segment the stroke will stop drawing and we + // need to break out of this loop. + if (strokeSpine.Count == 0) + { + break; + } + } + else + { + UpdateOriginalPositions(); + } + + // If we are about to update the front face position for the first time collapse the preview width. + // We don't do this until the stroke updates the front face position the first time so that the user can't + // create a stroke with no width by quickly releasing the trigger. + if (strokeSpine.Count == 1) + { + // Move the verts of the front face so that they are flush with the back face. We will let the algorithm build + // the volume up for us. The width we saw before was just for the preview. + Vector3 delta = MeshMath.CalculateGeometricCenter(currentBackFace, currentVolume) + - MeshMath.CalculateGeometricCenter(currentFrontFace, currentVolume); + MMesh.GeometryOperation adjustOperation = currentVolume.StartOperation(); + foreach (int vertexId in currentFrontFace.vertexIds) + { + // Apply the positional delta in model space. + Vector3 newModelPosition = currentVolume.MeshCoordsToModelCoords(originalVertexLocations[vertexId]) + delta; + adjustOperation.ModifyVertexModelSpace(vertexId, newModelPosition); + } + adjustOperation.Commit(); + UpdateOriginalPositions(); + } + + UpdateFrontFacePosition(); + AddSpine(); + + // Now that a new vertebra has selected added check the remaining distance from the new spine to the + // controller. If it happens to be less then a spine length break out. We include this extra check in case + // the estimated number of spines to add was wrong. + distanceToFill = Vector3.Distance(controllerPosition, strokeSpine.Last().origin) + - (CONTROLLER_CHANGE_THRESHOLD * strokeSpine.Last().length); + if (distanceToFill < strokeSpine.Last().length) + { + break; + } + } + + UpdateOriginalPositions(); + } + else + { + UpdateSpine(); + UpdateFrontFacePosition(); + } + + // Update the mesh. + currentVolume.RecalcBounds(); + MMesh.AttachMeshToGameObject( + worldSpace, currentHighlight, currentVolume, /* updateOnly */ estimatedSpinesToAdd == 0); + } } - } else { - Vector3 controllerPosition = (peltzerController.LastPositionModel + freeformModelSpaceOffsetWhileDrawing); - - // If we are creating a snapped freeform we only want to consider controller movement in the plane the freeform - // is being built on. - controllerPosition = definingAxis == Vector3.zero || !isSnapping ? - controllerPosition : - Math3d.ProjectPointOnPlane(definingAxis, strokeSpine.Last().origin, controllerPosition); - - // Find the vector from the origin of the last spine to the controller position. - Vector3 controllerChange = controllerPosition - strokeSpine.Last().origin; - - // Find the distance we want to fill with spines. This is the distance from the origin to the controller - // minus a threshold. The threshold makes the user have to move more before creating a spine giving stroke - // creation more stability. - float distanceToFill = controllerChange.magnitude - CONTROLLER_CHANGE_THRESHOLD * strokeSpine.Last().length; - - // Find how much space there is between the controller and the stroke being made. - // To generate the stroke we will fill this distance with as many spines as possible. - float estimatedSpinesToAdd = Mathf.Floor(distanceToFill / strokeSpine.Last().length); - - // We only auto-generate spines if the user isn't checkpointing. If they are then they are responsible for - // generating their own spines with physically added checkpoints. - if (!isManualCheckpointing) { - // Prevent the user from moving back on the stroke accidentally. A stroke will turn around to start building - // itself backwards but this should only happen if the user moves far enough away. - if (Vector3.Angle(strokeSpine.Last().normal, controllerChange) > BACKWARDS_ANGLE_THRESHOLD - && distanceToFill < strokeSpine.Last().length * BACKWARDS_DISTANCE_THRESHOLD) { - return; - } - - - - // While there is still space between the controller and the stroke that we can fill with a spine, find the - // right spine. We pre-compute the number of spines and use a for loop instead of a while loop that checks on - // every checkpoint if there is still room for a new spine. This prevents the code from getting stuck in a - // while loop on update crashing the app. This can happen if the generated geometry causes the stroke to loop - // around the controller position. - for (int i = 0; i < estimatedSpinesToAdd; i++) { - // Now that we've updated the front face to be in the correct position we see if we should checkpoint or - // "extend" the last spine. We don't actually lengthen the spine but it appears that way because we don't - // checkpoint in between the current spine and the previous spine. - UpdateSpine(); - - if (!SpineIsAligned()) { - AddCheckpoint(); - - // If this checkpoint causes us to insert an invalid segment the stroke will stop drawing and we - // need to break out of this loop. - if (strokeSpine.Count == 0) { - break; - } - } else { - UpdateOriginalPositions(); + + /// + /// Updates the Spine by finding the nearestVertebra and selecting that to make up the current segement of the + /// spine. + /// + private void UpdateSpine() + { + Vertebra previousVertebra = + strokeSpine.Count() > 1 ? strokeSpine[strokeSpine.Count() - 2].CurrentVertebra() : null; + + // Allows the Spine logic to tell the Freeform to checkpoint. This happens when a user moves back on themself and + // we want to autogenerate a portion of the spine. + bool shouldForceCheckpoint; + + Vector3 controllerUpVector; + if (Config.Instance.VrHardware == VrHardware.Vive) + { + controllerUpVector = worldSpace.WorldVectorToModel(peltzerController.transform.up); + } + else + { + controllerUpVector = worldSpace.WorldVectorToModel(peltzerController.wandTip.transform.up); + } + + // Find which vertebra of the active spine is closest to the controller's position. This will be used as the + // origin for the next spine and as the center of the next checkpointed face. + Vertebra nearestVertebra = strokeSpine.Last().NearestVertebra( + peltzerController.LastPositionModel + freeformModelSpaceOffsetWhileDrawing, isSnapping, isManualCheckpointing, + previousVertebra, controllerUpVector, out shouldForceCheckpoint); + + strokeSpine.Last().SelectVertebra(nearestVertebra); + + // We've chosen a vertebra that should be checkpointed before progressing. If we are generating an auto-stroke + // that will be handled later in the the update loop, but if we are not we need to force this condition. We do + // this by marking that the stroke should be checkpointing and then waiting until the user has moved far enough + // to stabilize the stroke. If we are already waitingToForceCheckpointing don't do anything. + if (!waitingToForceCheckpoint && shouldForceCheckpoint && isManualCheckpointing) + { + waitingToForceCheckpoint = true; + controllerPositionAtPromptToCheckpoint = worldSpace.ModelToWorld(peltzerController.LastPositionModel); } + else if (!shouldForceCheckpoint && waitingToForceCheckpoint) + { + // If we the Spine logic is telling us we don't have to checkpoint anymore cancel any pending waits to + // checkpoint. + waitingToForceCheckpoint = false; + } + } - // If we are about to update the front face position for the first time collapse the preview width. - // We don't do this until the stroke updates the front face position the first time so that the user can't - // create a stroke with no width by quickly releasing the trigger. - if (strokeSpine.Count == 1) { - // Move the verts of the front face so that they are flush with the back face. We will let the algorithm build - // the volume up for us. The width we saw before was just for the preview. - Vector3 delta = MeshMath.CalculateGeometricCenter(currentBackFace, currentVolume) - - MeshMath.CalculateGeometricCenter(currentFrontFace, currentVolume); - MMesh.GeometryOperation adjustOperation = currentVolume.StartOperation(); - foreach (int vertexId in currentFrontFace.vertexIds) { + /// + /// Moves the front face to its current position. + /// + private void UpdateFrontFacePosition() + { + Vertebra nearestVertebra = strokeSpine.Last().CurrentVertebra(); + + // Find the rotational delta being applied to the front face when its at the nearestVertebra. + Quaternion faceRotDelta = Quaternion.FromToRotation(strokeSpine.Last().normal, nearestVertebra.normal); + // Find the positional delta being applied to the vertices of the front face. + Vector3 delta = nearestVertebra.position - strokeSpine.Last().origin; + MMesh.GeometryOperation updateFrontOperation = currentVolume.StartOperation(); + foreach (int vertexId in currentFrontFace.vertexIds) + { + // Apply the rotational delta in model space. + Vector3 rotatedModelPosition = Math3d.RotatePointAroundPivot( + currentVolume.MeshCoordsToModelCoords(originalVertexLocations[vertexId]), strokeSpine.Last().origin, faceRotDelta); // Apply the positional delta in model space. - Vector3 newModelPosition = currentVolume.MeshCoordsToModelCoords(originalVertexLocations[vertexId]) + delta; - adjustOperation.ModifyVertexModelSpace(vertexId, newModelPosition); - } - adjustOperation.Commit(); - UpdateOriginalPositions(); + Vector3 newModelPosition = rotatedModelPosition + delta; + updateFrontOperation.ModifyVertexModelSpace(vertexId, newModelPosition); + } + updateFrontOperation.Commit(); + } + + /// + /// Checks to see if the current spine and previous spine in the strokeSpine are aligned. If they are we won't add + /// a checkpoint. + /// + /// True if the current spine and previous spine are aligned. + private bool SpineIsAligned() + { + if (strokeSpine.Count() < 2) + { + return true; } - UpdateFrontFacePosition(); - AddSpine(); + Vertebra currentVertebra = strokeSpine.Last().CurrentVertebra(); + Vertebra previousVertebra = strokeSpine[strokeSpine.Count() - 2].CurrentVertebra(); - // Now that a new vertebra has selected added check the remaining distance from the new spine to the - // controller. If it happens to be less then a spine length break out. We include this extra check in case - // the estimated number of spines to add was wrong. - distanceToFill = Vector3.Distance(controllerPosition, strokeSpine.Last().origin) - - (CONTROLLER_CHANGE_THRESHOLD * strokeSpine.Last().length); - if (distanceToFill < strokeSpine.Last().length) { - break; + // If a vertebra hasn't been selected for the active spine yet, return. + if (currentVertebra == null || previousVertebra == null) + { + return false; } - } - - UpdateOriginalPositions(); - } else { - UpdateSpine(); - UpdateFrontFacePosition(); + + // Check if the direction and normals of the previous and current vertebra are the same. + // This indicates that the two spine segements are aligned and we shouldn't place a checkpoint. + return Math3d.CompareVectors(currentVertebra.direction, previousVertebra.direction, 0.001f) + && Math3d.CompareVectors(currentVertebra.normal, previousVertebra.normal, 0.001f); } - // Update the mesh. - currentVolume.RecalcBounds(); - MMesh.AttachMeshToGameObject( - worldSpace, currentHighlight, currentVolume, /* updateOnly */ estimatedSpinesToAdd == 0); - } - } + /// + /// Adds the first spine. + /// + private void StartSpine() + { + List vertices = new List(); + foreach (int id in currentBackFace.vertexIds) + { + vertices.Add(currentVolume.VertexPositionInModelCoords(id)); + } - /// - /// Updates the Spine by finding the nearestVertebra and selecting that to make up the current segement of the - /// spine. - /// - private void UpdateSpine() { - Vertebra previousVertebra = - strokeSpine.Count() > 1 ? strokeSpine[strokeSpine.Count() - 2].CurrentVertebra() : null; - - // Allows the Spine logic to tell the Freeform to checkpoint. This happens when a user moves back on themself and - // we want to autogenerate a portion of the spine. - bool shouldForceCheckpoint; - - Vector3 controllerUpVector; - if (Config.Instance.VrHardware == VrHardware.Vive) { - controllerUpVector = worldSpace.WorldVectorToModel(peltzerController.transform.up); - } else { - controllerUpVector = worldSpace.WorldVectorToModel(peltzerController.wandTip.transform.up); - } - - // Find which vertebra of the active spine is closest to the controller's position. This will be used as the - // origin for the next spine and as the center of the next checkpointed face. - Vertebra nearestVertebra = strokeSpine.Last().NearestVertebra( - peltzerController.LastPositionModel + freeformModelSpaceOffsetWhileDrawing, isSnapping, isManualCheckpointing, - previousVertebra, controllerUpVector, out shouldForceCheckpoint); - - strokeSpine.Last().SelectVertebra(nearestVertebra); - - // We've chosen a vertebra that should be checkpointed before progressing. If we are generating an auto-stroke - // that will be handled later in the the update loop, but if we are not we need to force this condition. We do - // this by marking that the stroke should be checkpointing and then waiting until the user has moved far enough - // to stabilize the stroke. If we are already waitingToForceCheckpointing don't do anything. - if (!waitingToForceCheckpoint && shouldForceCheckpoint && isManualCheckpointing) { - waitingToForceCheckpoint = true; - controllerPositionAtPromptToCheckpoint = worldSpace.ModelToWorld(peltzerController.LastPositionModel); - } else if (!shouldForceCheckpoint && waitingToForceCheckpoint) { - // If we the Spine logic is telling us we don't have to checkpoint anymore cancel any pending waits to - // checkpoint. - waitingToForceCheckpoint = false; - } - } + vertices.Reverse(); + definingAxis = Vector3.zero; - /// - /// Moves the front face to its current position. - /// - private void UpdateFrontFacePosition() { - Vertebra nearestVertebra = strokeSpine.Last().CurrentVertebra(); - - // Find the rotational delta being applied to the front face when its at the nearestVertebra. - Quaternion faceRotDelta = Quaternion.FromToRotation(strokeSpine.Last().normal, nearestVertebra.normal); - // Find the positional delta being applied to the vertices of the front face. - Vector3 delta = nearestVertebra.position - strokeSpine.Last().origin; - MMesh.GeometryOperation updateFrontOperation = currentVolume.StartOperation(); - foreach (int vertexId in currentFrontFace.vertexIds) { - // Apply the rotational delta in model space. - Vector3 rotatedModelPosition = Math3d.RotatePointAroundPivot( - currentVolume.MeshCoordsToModelCoords(originalVertexLocations[vertexId]), strokeSpine.Last().origin, faceRotDelta); - // Apply the positional delta in model space. - Vector3 newModelPosition = rotatedModelPosition + delta; - updateFrontOperation.ModifyVertexModelSpace(vertexId, newModelPosition); - } - updateFrontOperation.Commit(); - } + strokeSpine.Add(new Spine(vertices, definingAxis)); + strokeOriginAtLastCheckpoint = strokeSpine.Last().origin; + } - /// - /// Checks to see if the current spine and previous spine in the strokeSpine are aligned. If they are we won't add - /// a checkpoint. - /// - /// True if the current spine and previous spine are aligned. - private bool SpineIsAligned() { - if (strokeSpine.Count() < 2) { - return true; - } - - Vertebra currentVertebra = strokeSpine.Last().CurrentVertebra(); - Vertebra previousVertebra = strokeSpine[strokeSpine.Count() - 2].CurrentVertebra(); - - // If a vertebra hasn't been selected for the active spine yet, return. - if (currentVertebra == null || previousVertebra == null) { - return false; - } - - // Check if the direction and normals of the previous and current vertebra are the same. - // This indicates that the two spine segements are aligned and we shouldn't place a checkpoint. - return Math3d.CompareVectors(currentVertebra.direction, previousVertebra.direction, 0.001f) - && Math3d.CompareVectors(currentVertebra.normal, previousVertebra.normal, 0.001f); - } + /// + /// Finds the offset from the controller, in model space, where the preview should be. + /// This is the offset that should be added to the controller's position in model space to obtain + /// the position where the center of the stroke preview should be. + /// + /// The offset. + private Vector3 GetFreeformPreviewOffSetModelSpace() + { + // Size of the volume currently being inserted. Note that the scale is already computed into this because + // when we generate the volume, we bake the scale into its geometry. + float size = currentVolume.bounds.size.z; + // Compute the distance from the controller to the edge of the preview. This is given as a constant in + // world space, so here we just convert it to model space. + float distanceToEdgeModelSpace = DISTANCE_TO_PREVIEW_EDGE_WORLD_SPACE / worldSpace.scale; + // Now that we know how big the volume is and the distance to the edge, it's easy to find the distance + // to the center. + float distanceToCenterModelSpace = distanceToEdgeModelSpace + size * 0.5f; + // Now we have a distance from the controller that we want to convert into a model space offset. + // To do that, we just multiply it by Vector3.forward to get it as a vector, and then transform it + // by the controller's current orientation to get it to point the right way. + return peltzerController.LastRotationModel * Vector3.forward * distanceToCenterModelSpace; + } - /// - /// Adds the first spine. - /// - private void StartSpine() { - List vertices = new List(); - foreach (int id in currentBackFace.vertexIds) { - vertices.Add(currentVolume.VertexPositionInModelCoords(id)); - } + /// + /// Begins a new stroke, setting up some default variables. + /// + private void StartStroke() + { + audioLibrary.PlayClip(audioLibrary.genericSelectSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + insertionInProgress = true; + MeshWithMaterialRenderer renderer = currentHighlight.GetComponent(); + currentVolume.offset = renderer.GetPositionInModelSpace(); + currentVolume.rotation = renderer.GetOrientationInModelSpace(); + currentScaleIndex = insertScaleIndex; + numCheckpointsInCurrentVolume = 0; + strokeVolumeSegments = new List(); + + // Find the normal of the current "front face". We'll use it to detect if the user moves in the opposite direction + // and we should change the front face to be the back face. + List vertices = new List(); + foreach (int id in currentFrontFace.vertexIds) + { + vertices.Add(currentVolume.MeshCoordsToModelCoords(originalVertexLocations[id])); + } + chooseFaceDistance = Spine.FindSpineLength(MeshMath.FindHeightOfARegularPolygonalFace(vertices)) * 1.1f; + frontFaceNormalAtStart = MeshMath.CalculateNormal(vertices); + controllerPositionAtStart = peltzerController.LastPositionModel; - vertices.Reverse(); - definingAxis = Vector3.zero; + // Instantiate the strokeSpine which will hold all the spines that make the stroke. + strokeSpine = new List(); + freeformModelSpaceOffsetWhileDrawing = GetFreeformPreviewOffSetModelSpace(); + } - strokeSpine.Add(new Spine(vertices, definingAxis)); - strokeOriginAtLastCheckpoint = strokeSpine.Last().origin; - } + /// + /// Finishes a stroke, and inserts it into the model. + /// + private void EndStroke() + { + if (strokeVolumeSegments.Count > 0) + { + model.ApplyCommand(SetMeshGroupsCommand.CreateGroupMeshesCommand(model, strokeVolumeSegments)); + audioLibrary.PlayClip(audioLibrary.genericReleaseSound); + } + ClearState(); + } - /// - /// Finds the offset from the controller, in model space, where the preview should be. - /// This is the offset that should be added to the controller's position in model space to obtain - /// the position where the center of the stroke preview should be. - /// - /// The offset. - private Vector3 GetFreeformPreviewOffSetModelSpace() { - // Size of the volume currently being inserted. Note that the scale is already computed into this because - // when we generate the volume, we bake the scale into its geometry. - float size = currentVolume.bounds.size.z; - // Compute the distance from the controller to the edge of the preview. This is given as a constant in - // world space, so here we just convert it to model space. - float distanceToEdgeModelSpace = DISTANCE_TO_PREVIEW_EDGE_WORLD_SPACE / worldSpace.scale; - // Now that we know how big the volume is and the distance to the edge, it's easy to find the distance - // to the center. - float distanceToCenterModelSpace = distanceToEdgeModelSpace + size * 0.5f; - // Now we have a distance from the controller that we want to convert into a model space offset. - // To do that, we just multiply it by Vector3.forward to get it as a vector, and then transform it - // by the controller's current orientation to get it to point the right way. - return peltzerController.LastRotationModel * Vector3.forward * distanceToCenterModelSpace; - } + /// + /// Finishes a stroke segment, and inserts it into the model. + /// + private void EndStrokeSegment() + { - /// - /// Begins a new stroke, setting up some default variables. - /// - private void StartStroke() { - audioLibrary.PlayClip(audioLibrary.genericSelectSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - insertionInProgress = true; - MeshWithMaterialRenderer renderer = currentHighlight.GetComponent(); - currentVolume.offset = renderer.GetPositionInModelSpace(); - currentVolume.rotation = renderer.GetOrientationInModelSpace(); - currentScaleIndex = insertScaleIndex; - numCheckpointsInCurrentVolume = 0; - strokeVolumeSegments = new List(); - - // Find the normal of the current "front face". We'll use it to detect if the user moves in the opposite direction - // and we should change the front face to be the back face. - List vertices = new List(); - foreach (int id in currentFrontFace.vertexIds) { - vertices.Add(currentVolume.MeshCoordsToModelCoords(originalVertexLocations[id])); - } - chooseFaceDistance = Spine.FindSpineLength(MeshMath.FindHeightOfARegularPolygonalFace(vertices)) * 1.1f; - frontFaceNormalAtStart = MeshMath.CalculateNormal(vertices); - controllerPositionAtStart = peltzerController.LastPositionModel; - - // Instantiate the strokeSpine which will hold all the spines that make the stroke. - strokeSpine = new List(); - freeformModelSpaceOffsetWhileDrawing = GetFreeformPreviewOffSetModelSpace(); - } + // Ensure nothing (such as redo, or tool switch) has caused an id clash since this mesh was created. + if (model.HasMesh(currentVolume.id)) + { + currentVolume.ChangeId(model.GenerateMeshId()); + } - /// - /// Finishes a stroke, and inserts it into the model. - /// - private void EndStroke() { - if (strokeVolumeSegments.Count > 0) { - model.ApplyCommand(SetMeshGroupsCommand.CreateGroupMeshesCommand(model, strokeVolumeSegments)); - audioLibrary.PlayClip(audioLibrary.genericReleaseSound); - } - ClearState(); - } + MeshFixer.FixMutatedMesh(currentVolume, currentVolume, + new HashSet(currentVolume.GetVertexIds()), + /* splitNonCoplanarFaces */ false, /* mergeAdjacentCoplanarFaces*/ true); - /// - /// Finishes a stroke segment, and inserts it into the model. - /// - private void EndStrokeSegment() { + if (!model.CanAddMesh(currentVolume)) + { + audioLibrary.PlayClip(audioLibrary.errorSound); + peltzerController.TriggerHapticFeedback(); - // Ensure nothing (such as redo, or tool switch) has caused an id clash since this mesh was created. - if (model.HasMesh(currentVolume.id)) { - currentVolume.ChangeId(model.GenerateMeshId()); - } + EndStroke(); + } + else + { + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + model.ApplyCommand(new AddMeshCommand(currentVolume)); - MeshFixer.FixMutatedMesh(currentVolume, currentVolume, - new HashSet(currentVolume.GetVertexIds()), - /* splitNonCoplanarFaces */ false, /* mergeAdjacentCoplanarFaces*/ true); + strokeVolumeSegments.Add(currentVolume.id); + CreateNewVolumeAndHighlightSegment(); + } + } - if (!model.CanAddMesh(currentVolume)) { - audioLibrary.PlayClip(audioLibrary.errorSound); - peltzerController.TriggerHapticFeedback(); + /// + /// Scales the front-face of an in-progress stroke: achieved by recalculating the X and Y points from first + /// principles given the new current scale, rather than trying to move them by some scale factor. + /// + private void ScaleFrontFace(bool scaleUp) + { + // Nothing too big, nothing too small. + if ((scaleUp && currentScaleIndex >= MAX_SCALE_INDEX) || (!scaleUp && currentScaleIndex == MIN_SCALE_INDEX)) + { + return; + } - EndStroke(); - } else { - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - model.ApplyCommand(new AddMeshCommand(currentVolume)); + if (scaleUp) + { + currentScaleIndex += IsLongTermScale() ? LONG_TERM_SCALE_FACTOR : 1; + audioLibrary.PlayClip(audioLibrary.incrementSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + } + else + { + currentScaleIndex -= IsLongTermScale() ? LONG_TERM_SCALE_FACTOR : 1; + audioLibrary.PlayClip(audioLibrary.decrementSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + } + // We find the new vertex positions by drawing a vector from the center of the currentFace to a vertex, then spin + // it around the normal of the face stopping at each required angle and adding the vertex. + float radius = (GridUtils.GRID_SIZE / 2) * currentScaleIndex; + float angle = 360f / currentFrontFace.vertexIds.Count; + + List meshSpaceFaceVertices = new List(); + for (int i = 0; i < currentFrontFace.vertexIds.Count; i++) + { + meshSpaceFaceVertices.Add(originalVertexLocations[currentFrontFace.vertexIds[i]]); + } - strokeVolumeSegments.Add(currentVolume.id); - CreateNewVolumeAndHighlightSegment(); - } - } + Vector3 center = MeshMath.CalculateGeometricCenter(meshSpaceFaceVertices); + Vector3 normal = MeshMath.CalculateNormal(meshSpaceFaceVertices); + // This is the vector we will spin around the normal. + Vector3 radialArm = radius * (meshSpaceFaceVertices[0] - center).normalized; - /// - /// Scales the front-face of an in-progress stroke: achieved by recalculating the X and Y points from first - /// principles given the new current scale, rather than trying to move them by some scale factor. - /// - private void ScaleFrontFace(bool scaleUp) { - // Nothing too big, nothing too small. - if ((scaleUp && currentScaleIndex >= MAX_SCALE_INDEX) || (!scaleUp && currentScaleIndex == MIN_SCALE_INDEX)) { - return; - } - - if (scaleUp) { - currentScaleIndex += IsLongTermScale() ? LONG_TERM_SCALE_FACTOR : 1; - audioLibrary.PlayClip(audioLibrary.incrementSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - } else { - currentScaleIndex -= IsLongTermScale() ? LONG_TERM_SCALE_FACTOR : 1; - audioLibrary.PlayClip(audioLibrary.decrementSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - } - // We find the new vertex positions by drawing a vector from the center of the currentFace to a vertex, then spin - // it around the normal of the face stopping at each required angle and adding the vertex. - float radius = (GridUtils.GRID_SIZE / 2) * currentScaleIndex; - float angle = 360f / currentFrontFace.vertexIds.Count; - - List meshSpaceFaceVertices = new List(); - for (int i = 0; i < currentFrontFace.vertexIds.Count; i++) { - meshSpaceFaceVertices.Add(originalVertexLocations[currentFrontFace.vertexIds[i]]); - } - - Vector3 center = MeshMath.CalculateGeometricCenter(meshSpaceFaceVertices); - Vector3 normal = MeshMath.CalculateNormal(meshSpaceFaceVertices); - // This is the vector we will spin around the normal. - Vector3 radialArm = radius * (meshSpaceFaceVertices[0] - center).normalized; - - MMesh.GeometryOperation scaleFrontfaceOperation = currentVolume.StartOperation(); - - for (int i = 0; i * angle < (360f - EPSILON); i++) { - Vector3 meshSpaceLocation = center + Quaternion.AngleAxis(i * angle, normal) * radialArm; - int vertexId = currentFrontFace.vertexIds[i]; - scaleFrontfaceOperation.ModifyVertex(new Vertex(vertexId, meshSpaceLocation)); - } - scaleFrontfaceOperation.Commit(); - UpdateOriginalPositions(); - } + MMesh.GeometryOperation scaleFrontfaceOperation = currentVolume.StartOperation(); - // Adding a checkpoint means adding a new front face; removing the prior front face (but keeping its verts); - // and adding a whole new set of sides between the prior and new front face. - private void AddCheckpoint() { - // We will segment the stroke when the numCheckpointsInCurrentVolume exceeds the max but only once the - // definingAxis has been choosen if we are snapping otherwise when we reorient the stroke only the most recent - // segment will rotate. - if (definingAxis != Vector3.zero || !isSnapping) { - numCheckpointsInCurrentVolume++; - } - - // If adding this checkpoint is going to go over the max allowed number end the seg - if (numCheckpointsInCurrentVolume > MAX_CHECKPOINT_COUNT) { - // Create a new currentVolume segement. - EndStrokeSegment(); - numCheckpointsInCurrentVolume = 0; - return; - } - - waitingToForceCheckpoint = false; - strokeOriginAtLastCheckpoint = strokeSpine.Last().origin; - - List newVertices = new List(vertexCount); - List newVertexIds = new List(vertexCount); - - MMesh.GeometryOperation checkpointOperation = currentVolume.StartOperation(); - - // Add the new front face vertices, keeping track of the old front face vertices. - for (int i = 0; i <= vertexCount; i++) { - if (i != vertexCount) { - // Get the current vertexId. - int vertexId = currentFrontFace.vertexIds[i]; - - Vertex newVertex = checkpointOperation.AddVertexMeshSpace( - checkpointOperation.GetCurrentVertexPositionMeshSpace(vertexId)); - - // Add a new vertex ahead of it. - newVertexIds.Add(newVertex.id); - - newVertices.Add(newVertex); - originalVertexLocations[newVertex.id] = newVertex.loc; + for (int i = 0; i * angle < (360f - EPSILON); i++) + { + Vector3 meshSpaceLocation = center + Quaternion.AngleAxis(i * angle, normal) * radialArm; + int vertexId = currentFrontFace.vertexIds[i]; + scaleFrontfaceOperation.ModifyVertex(new Vertex(vertexId, meshSpaceLocation)); + } + scaleFrontfaceOperation.Commit(); + UpdateOriginalPositions(); } - if (i == 0) { - continue; - } + // Adding a checkpoint means adding a new front face; removing the prior front face (but keeping its verts); + // and adding a whole new set of sides between the prior and new front face. + private void AddCheckpoint() + { + // We will segment the stroke when the numCheckpointsInCurrentVolume exceeds the max but only once the + // definingAxis has been choosen if we are snapping otherwise when we reorient the stroke only the most recent + // segment will rotate. + if (definingAxis != Vector3.zero || !isSnapping) + { + numCheckpointsInCurrentVolume++; + } - // Create the new 'side faces' with clockwise ordering. Each side face has 4 verts. - // The first face 'loops around'. - List newFaceVertexIds = new List() { - currentFrontFace.vertexIds[i-1], - currentFrontFace.vertexIds[i % vertexCount], - newVertices[i % vertexCount].id, - newVertices[i-1].id - }; + // If adding this checkpoint is going to go over the max allowed number end the seg + if (numCheckpointsInCurrentVolume > MAX_CHECKPOINT_COUNT) + { + // Create a new currentVolume segement. + EndStrokeSegment(); + numCheckpointsInCurrentVolume = 0; + return; + } - checkpointOperation.AddFace(newFaceVertexIds, new FaceProperties(peltzerController.currentMaterial)); - } + waitingToForceCheckpoint = false; + strokeOriginAtLastCheckpoint = strokeSpine.Last().origin; - // Create a new front face. - currentFrontFace = checkpointOperation.AddFace(newVertexIds, currentFrontFace.properties); - checkpointOperation.Commit(); - } + List newVertices = new List(vertexCount); + List newVertexIds = new List(vertexCount); - /// - /// Adds a spine to the strokeSpine based on the current front face. - /// - private void AddSpine() { - // If we haven't defined the definingAxis see if we can. We need to have determined two unique vectors that we - // want to be in the plane we are creating the stroke in. Then we can get the normal of that plane by taking the - // cross product of the two vectors. - if (strokeSpine.Count() > 0 && definingAxis == Vector3.zero && isSnapping && !isManualCheckpointing) { - Vertebra lastSelectedVertebra = strokeSpine.Last().CurrentVertebra(); - - if (completedSnaps < SNAP_KNOW_HOW_COUNT && !recordedSnapThisStroke) { - // The user successfully added a snap during this stroke. Record it as a completed snap. - completedSnaps++; - recordedSnapThisStroke = true; - } + MMesh.GeometryOperation checkpointOperation = currentVolume.StartOperation(); - if (lastSelectedVertebra != null && - !Math3d.CompareVectors(lastSelectedVertebra.direction.normalized, strokeSpine.Last().normal, 0.001f)) { + // Add the new front face vertices, keeping track of the old front face vertices. + for (int i = 0; i <= vertexCount; i++) + { + if (i != vertexCount) + { + // Get the current vertexId. + int vertexId = currentFrontFace.vertexIds[i]; - // Rotate the mesh so that the defining axis lines up with the users hand. - Vector3 vertebraProjectedPosition = Math3d.ProjectPointOnPlane( - strokeSpine.Last().normal, - strokeSpine.Last().origin, - lastSelectedVertebra.position); + Vertex newVertex = checkpointOperation.AddVertexMeshSpace( + checkpointOperation.GetCurrentVertexPositionMeshSpace(vertexId)); - Vector3 controllerProjectedPosition = Math3d.ProjectPointOnPlane( - strokeSpine.Last().normal, - strokeSpine.Last().origin, - (peltzerController.LastPositionModel + freeformModelSpaceOffsetWhileDrawing)); + // Add a new vertex ahead of it. + newVertexIds.Add(newVertex.id); - Vector3 vertebraProjectedDirection = - ((vertebraProjectedPosition - strokeSpine.Last().origin) * 1000f).normalized; - Vector3 controllerProjectedDirection = - ((controllerProjectedPosition - strokeSpine.Last().origin) * 1000f).normalized; + newVertices.Add(newVertex); + originalVertexLocations[newVertex.id] = newVertex.loc; + } - Quaternion rotationalDelta = Quaternion.FromToRotation(vertebraProjectedDirection, controllerProjectedDirection); - currentVolume.rotation = rotationalDelta * currentVolume.rotation; + if (i == 0) + { + continue; + } - definingAxis = Vector3.Cross(controllerProjectedDirection, strokeSpine.Last().normal); - } - } + // Create the new 'side faces' with clockwise ordering. Each side face has 4 verts. + // The first face 'loops around'. + List newFaceVertexIds = new List() { + currentFrontFace.vertexIds[i-1], + currentFrontFace.vertexIds[i % vertexCount], + newVertices[i % vertexCount].id, + newVertices[i-1].id + }; - List vertices = new List(); - foreach (int id in currentFrontFace.vertexIds) { - vertices.Add(currentVolume.VertexPositionInModelCoords(id)); - } + checkpointOperation.AddFace(newFaceVertexIds, new FaceProperties(peltzerController.currentMaterial)); + } - strokeSpine.Add(new Spine(vertices, definingAxis)); - } + // Create a new front face. + currentFrontFace = checkpointOperation.AddFace(newVertexIds, currentFrontFace.properties); + checkpointOperation.Commit(); + } - /// - /// Updates the originalVertexLocations to be their current position so that we can simulate a checkpoint without - /// actually adding more vertices to the mesh. - /// - private void UpdateOriginalPositions() { - foreach (int vertexId in currentFrontFace.vertexIds) { - originalVertexLocations[vertexId] = currentVolume.VertexPositionInMeshCoords(vertexId); - } - } + /// + /// Adds a spine to the strokeSpine based on the current front face. + /// + private void AddSpine() + { + // If we haven't defined the definingAxis see if we can. We need to have determined two unique vectors that we + // want to be in the plane we are creating the stroke in. Then we can get the normal of that plane by taking the + // cross product of the two vectors. + if (strokeSpine.Count() > 0 && definingAxis == Vector3.zero && isSnapping && !isManualCheckpointing) + { + Vertebra lastSelectedVertebra = strokeSpine.Last().CurrentVertebra(); + + if (completedSnaps < SNAP_KNOW_HOW_COUNT && !recordedSnapThisStroke) + { + // The user successfully added a snap during this stroke. Record it as a completed snap. + completedSnaps++; + recordedSnapThisStroke = true; + } + + if (lastSelectedVertebra != null && + !Math3d.CompareVectors(lastSelectedVertebra.direction.normalized, strokeSpine.Last().normal, 0.001f)) + { + + // Rotate the mesh so that the defining axis lines up with the users hand. + Vector3 vertebraProjectedPosition = Math3d.ProjectPointOnPlane( + strokeSpine.Last().normal, + strokeSpine.Last().origin, + lastSelectedVertebra.position); + + Vector3 controllerProjectedPosition = Math3d.ProjectPointOnPlane( + strokeSpine.Last().normal, + strokeSpine.Last().origin, + (peltzerController.LastPositionModel + freeformModelSpaceOffsetWhileDrawing)); + + Vector3 vertebraProjectedDirection = + ((vertebraProjectedPosition - strokeSpine.Last().origin) * 1000f).normalized; + Vector3 controllerProjectedDirection = + ((controllerProjectedPosition - strokeSpine.Last().origin) * 1000f).normalized; + + Quaternion rotationalDelta = Quaternion.FromToRotation(vertebraProjectedDirection, controllerProjectedDirection); + currentVolume.rotation = rotationalDelta * currentVolume.rotation; + + definingAxis = Vector3.Cross(controllerProjectedDirection, strokeSpine.Last().normal); + } + } - /// - /// Changes the scale of the preview if the change is within the min and max scale index. - /// - /// Whether we should increase the scale. - private void ChangeScale(bool increase) { - int change = increase ? 1 : -1; - if (IsLongTermScale()) { - change *= LONG_TERM_SCALE_FACTOR; - } - - if (insertScaleIndex + change >= MIN_SCALE_INDEX && insertScaleIndex + change <= MAX_SCALE_INDEX) { - insertScaleIndex += change; - CreateNewVolumeAndHighlight(); - if (change > 0) { - audioLibrary.PlayClip(audioLibrary.incrementSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - } else { - audioLibrary.PlayClip(audioLibrary.decrementSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + List vertices = new List(); + foreach (int id in currentFrontFace.vertexIds) + { + vertices.Add(currentVolume.VertexPositionInModelCoords(id)); + } + + strokeSpine.Add(new Spine(vertices, definingAxis)); } - } - } - /// - /// Changes the number of vertices on the preview if the change is within the min and max vertex count. - /// - /// - private void ChangeVertexCount(bool increase) { - int change = increase ? 1 : -1; - - if (vertexCount + change >= MIN_VERTEX_COUNT && vertexCount + change <= MAX_VERTEX_COUNT) { - vertexCount += change; - CreateNewVolumeAndHighlight(); - if (change > 0) { - audioLibrary.PlayClip(audioLibrary.swipeRightSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - } else { - audioLibrary.PlayClip(audioLibrary.swipeLeftSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + /// + /// Updates the originalVertexLocations to be their current position so that we can simulate a checkpoint without + /// actually adding more vertices to the mesh. + /// + private void UpdateOriginalPositions() + { + foreach (int vertexId in currentFrontFace.vertexIds) + { + originalVertexLocations[vertexId] = currentVolume.VertexPositionInMeshCoords(vertexId); + } } - } else { - audioLibrary.PlayClip(audioLibrary.shapeMenuEndSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - } - } - /// - /// Set the current checkpointing mode and toggle all the UI elements. - /// - /// Whether we should be manually checkpointing. - private void SetCheckpointingMode(bool shouldBeManuallyCheckpointing) { - // TODO: Toggle UI elements. - isManualCheckpointing = shouldBeManuallyCheckpointing; - } + /// + /// Changes the scale of the preview if the change is within the min and max scale index. + /// + /// Whether we should increase the scale. + private void ChangeScale(bool increase) + { + int change = increase ? 1 : -1; + if (IsLongTermScale()) + { + change *= LONG_TERM_SCALE_FACTOR; + } - /// - /// An event handler that listens for controller input and delegates accordingly. - /// - /// The sender of the controller event. - /// The controller event arguments. - private void ControllerEventHandler(object sender, ControllerEventArgs args) { - if (peltzerController.mode != ControllerMode.insertStroke) - return; - - if (IsBeginOperationEvent(args)) { - triggerUpToEnd = false; - waitingToDetermineReleaseType = true; - triggerDownTime = Time.time; - StartStroke(); - peltzerController.ChangeTouchpadOverlay(TouchpadOverlay.FREEFORM); - } else if (IsCompleteSingleClickEvent(args)) { - waitingToDetermineReleaseType = false; - triggerUpToEnd = false; - SetCheckpointingMode(/*isManualCheckpointing*/ true); - peltzerController.ChangeTouchpadOverlay(TouchpadOverlay.FREEFORM); - } else if (IsFinishStrokeEvent(args)) { - EndStrokeSegment(); - EndStroke(); - SetCheckpointingMode(/*isManualCheckpointing*/ false); - peltzerController.ChangeTouchpadOverlay(TouchpadOverlay.FREEFORM); - recordedSnapThisStroke = false; - } else if (IsScaleEvent(args)) { - if (scaleType == ScaleType.NONE) { - longTermScaleStartTime = Time.time + FAST_SCALE_THRESHOLD; - } - scaleType = (args.TouchpadLocation == TouchpadLocation.TOP) ? ScaleType.SCALE_UP : ScaleType.SCALE_DOWN; - if (!insertionInProgress) { - ChangeScale(args.TouchpadLocation == TouchpadLocation.TOP); - } else { - ScaleFrontFace(args.TouchpadLocation == TouchpadLocation.TOP); + if (insertScaleIndex + change >= MIN_SCALE_INDEX && insertScaleIndex + change <= MAX_SCALE_INDEX) + { + insertScaleIndex += change; + CreateNewVolumeAndHighlight(); + if (change > 0) + { + audioLibrary.PlayClip(audioLibrary.incrementSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + } + else + { + audioLibrary.PlayClip(audioLibrary.decrementSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + } + } } - } else if (IsChangeStrokeVertexCountEvent(args)) { - ChangeVertexCount(args.TouchpadLocation == TouchpadLocation.RIGHT); - } else if (IsInsertStrokeCheckpointEvent(args)) { - if (isManualCheckpointing) { - AddCheckpoint(); - AddSpine(); + + /// + /// Changes the number of vertices on the preview if the change is within the min and max vertex count. + /// + /// + private void ChangeVertexCount(bool increase) + { + int change = increase ? 1 : -1; + + if (vertexCount + change >= MIN_VERTEX_COUNT && vertexCount + change <= MAX_VERTEX_COUNT) + { + vertexCount += change; + CreateNewVolumeAndHighlight(); + if (change > 0) + { + audioLibrary.PlayClip(audioLibrary.swipeRightSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + } + else + { + audioLibrary.PlayClip(audioLibrary.swipeLeftSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + } + } + else + { + audioLibrary.PlayClip(audioLibrary.shapeMenuEndSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + } } - } else if (IsStopScalingEvent(args)) { - StopScaling(); - } else if (IsSetUpHoverTooltipEvent(args) - && PeltzerMain.Instance.restrictionManager.touchpadUpAllowed) { - SetHoverTooltip(peltzerController.controllerGeometry.freeformTooltipUp, TouchpadHoverState.UP); - } else if (IsSetDownHoverTooltipEvent(args) - && PeltzerMain.Instance.restrictionManager.touchpadDownAllowed) { - SetHoverTooltip(peltzerController.controllerGeometry.freeformTooltipDown, TouchpadHoverState.DOWN); - } else if (IsSetLeftHoverTooltipEvent(args) && !insertionInProgress - && PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed) { - SetHoverTooltip(peltzerController.controllerGeometry.freeformTooltipLeft, TouchpadHoverState.LEFT); - } else if (IsSetRightHoverTooltipEvent(args) && !insertionInProgress - && PeltzerMain.Instance.restrictionManager.touchpadRightAllowed) { - SetHoverTooltip(peltzerController.controllerGeometry.freeformTooltipRight, TouchpadHoverState.RIGHT); - } else if (IsSetCenterHoverTooltipEvent(args) && insertionInProgress) { - SetHoverTooltip(peltzerController.controllerGeometry.freeformTooltipCenter, TouchpadHoverState.NONE); - } else if (IsUnsetAllHoverTooltipsEvent(args)) { - UnsetAllHoverTooltips(); - } else if (IsStartSnapEvent(args)) { - isSnapping = true; - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.alignSound); - if (completedSnaps < SNAP_KNOW_HOW_COUNT) { - PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); + + /// + /// Set the current checkpointing mode and toggle all the UI elements. + /// + /// Whether we should be manually checkpointing. + private void SetCheckpointingMode(bool shouldBeManuallyCheckpointing) + { + // TODO: Toggle UI elements. + isManualCheckpointing = shouldBeManuallyCheckpointing; } - } else if (IsEndSnapEvent(args)) { - // Allow the user to create a stroke that has snapped and unsnapping segments. - if (strokeSpine.Count > 0) { - AddCheckpoint(); - AddSpine(); + + /// + /// An event handler that listens for controller input and delegates accordingly. + /// + /// The sender of the controller event. + /// The controller event arguments. + private void ControllerEventHandler(object sender, ControllerEventArgs args) + { + if (peltzerController.mode != ControllerMode.insertStroke) + return; + + if (IsBeginOperationEvent(args)) + { + triggerUpToEnd = false; + waitingToDetermineReleaseType = true; + triggerDownTime = Time.time; + StartStroke(); + peltzerController.ChangeTouchpadOverlay(TouchpadOverlay.FREEFORM); + } + else if (IsCompleteSingleClickEvent(args)) + { + waitingToDetermineReleaseType = false; + triggerUpToEnd = false; + SetCheckpointingMode(/*isManualCheckpointing*/ true); + peltzerController.ChangeTouchpadOverlay(TouchpadOverlay.FREEFORM); + } + else if (IsFinishStrokeEvent(args)) + { + EndStrokeSegment(); + EndStroke(); + SetCheckpointingMode(/*isManualCheckpointing*/ false); + peltzerController.ChangeTouchpadOverlay(TouchpadOverlay.FREEFORM); + recordedSnapThisStroke = false; + } + else if (IsScaleEvent(args)) + { + if (scaleType == ScaleType.NONE) + { + longTermScaleStartTime = Time.time + FAST_SCALE_THRESHOLD; + } + scaleType = (args.TouchpadLocation == TouchpadLocation.TOP) ? ScaleType.SCALE_UP : ScaleType.SCALE_DOWN; + if (!insertionInProgress) + { + ChangeScale(args.TouchpadLocation == TouchpadLocation.TOP); + } + else + { + ScaleFrontFace(args.TouchpadLocation == TouchpadLocation.TOP); + } + } + else if (IsChangeStrokeVertexCountEvent(args)) + { + ChangeVertexCount(args.TouchpadLocation == TouchpadLocation.RIGHT); + } + else if (IsInsertStrokeCheckpointEvent(args)) + { + if (isManualCheckpointing) + { + AddCheckpoint(); + AddSpine(); + } + } + else if (IsStopScalingEvent(args)) + { + StopScaling(); + } + else if (IsSetUpHoverTooltipEvent(args) + && PeltzerMain.Instance.restrictionManager.touchpadUpAllowed) + { + SetHoverTooltip(peltzerController.controllerGeometry.freeformTooltipUp, TouchpadHoverState.UP); + } + else if (IsSetDownHoverTooltipEvent(args) + && PeltzerMain.Instance.restrictionManager.touchpadDownAllowed) + { + SetHoverTooltip(peltzerController.controllerGeometry.freeformTooltipDown, TouchpadHoverState.DOWN); + } + else if (IsSetLeftHoverTooltipEvent(args) && !insertionInProgress + && PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed) + { + SetHoverTooltip(peltzerController.controllerGeometry.freeformTooltipLeft, TouchpadHoverState.LEFT); + } + else if (IsSetRightHoverTooltipEvent(args) && !insertionInProgress + && PeltzerMain.Instance.restrictionManager.touchpadRightAllowed) + { + SetHoverTooltip(peltzerController.controllerGeometry.freeformTooltipRight, TouchpadHoverState.RIGHT); + } + else if (IsSetCenterHoverTooltipEvent(args) && insertionInProgress) + { + SetHoverTooltip(peltzerController.controllerGeometry.freeformTooltipCenter, TouchpadHoverState.NONE); + } + else if (IsUnsetAllHoverTooltipsEvent(args)) + { + UnsetAllHoverTooltips(); + } + else if (IsStartSnapEvent(args)) + { + isSnapping = true; + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.alignSound); + if (completedSnaps < SNAP_KNOW_HOW_COUNT) + { + PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); + } + } + else if (IsEndSnapEvent(args)) + { + // Allow the user to create a stroke that has snapped and unsnapping segments. + if (strokeSpine.Count > 0) + { + AddCheckpoint(); + AddSpine(); + } + isSnapping = false; + PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); + // The definingAxis is only used for snapping. + definingAxis = Vector3.zero; + } } - isSnapping = false; - PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); - // The definingAxis is only used for snapping. - definingAxis = Vector3.zero; - } - } - /// - /// Whether this matches the pattern of a 'scale' event. - /// - /// The controller event arguments. - /// True if this is a scale event, false otherwise. - private static bool IsScaleEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN - && (args.TouchpadLocation == TouchpadLocation.BOTTOM || args.TouchpadLocation == TouchpadLocation.TOP) - && !PeltzerMain.Instance.Zoomer.Zooming; - } + /// + /// Whether this matches the pattern of a 'scale' event. + /// + /// The controller event arguments. + /// True if this is a scale event, false otherwise. + private static bool IsScaleEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN + && (args.TouchpadLocation == TouchpadLocation.BOTTOM || args.TouchpadLocation == TouchpadLocation.TOP) + && !PeltzerMain.Instance.Zoomer.Zooming; + } - /// - /// Whether this matches the pattern of the end of a 'scale' event. - /// - /// The controller event arguments. - /// True if this is a scale event, false otherwise. - private bool IsStopScalingEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.UP - && scaleType != ScaleType.NONE - && !PeltzerMain.Instance.Zoomer.Zooming; - } + /// + /// Whether this matches the pattern of the end of a 'scale' event. + /// + /// The controller event arguments. + /// True if this is a scale event, false otherwise. + private bool IsStopScalingEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.UP + && scaleType != ScaleType.NONE + && !PeltzerMain.Instance.Zoomer.Zooming; + } - /// - /// Stop scaling. - /// - private void StopScaling() { - scaleType = ScaleType.NONE; - longTermScaleStartTime = float.MaxValue; - } + /// + /// Stop scaling. + /// + private void StopScaling() + { + scaleType = ScaleType.NONE; + longTermScaleStartTime = float.MaxValue; + } - /// - /// Whether scaling has been happening continuously over the threshold set by FAST_SCALE_THRESHOLD. - /// - /// True if this is a long term scale event, false otherwise. - private bool IsLongTermScale() { - return Time.time > longTermScaleStartTime; - } + /// + /// Whether scaling has been happening continuously over the threshold set by FAST_SCALE_THRESHOLD. + /// + /// True if this is a long term scale event, false otherwise. + private bool IsLongTermScale() + { + return Time.time > longTermScaleStartTime; + } - /// - /// Whether this matches the pattern of an event which should change the number of stroke polygon vertices. - /// - /// The controller event arguments. - /// True if this is a 'change stroke vertex count' event, false otherwise. - private bool IsChangeStrokeVertexCountEvent(ControllerEventArgs args) { - return !insertionInProgress - && args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN - && (args.TouchpadLocation == TouchpadLocation.RIGHT || args.TouchpadLocation == TouchpadLocation.LEFT) - && !PeltzerMain.Instance.Zoomer.Zooming; - } + /// + /// Whether this matches the pattern of an event which should change the number of stroke polygon vertices. + /// + /// The controller event arguments. + /// True if this is a 'change stroke vertex count' event, false otherwise. + private bool IsChangeStrokeVertexCountEvent(ControllerEventArgs args) + { + return !insertionInProgress + && args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN + && (args.TouchpadLocation == TouchpadLocation.RIGHT || args.TouchpadLocation == TouchpadLocation.LEFT) + && !PeltzerMain.Instance.Zoomer.Zooming; + } - /// - /// Whether this matches the pattern of an event which should begin the creation of a stroke. - /// - /// The controller event arguments. - /// True if this is a 'start stroke' event, false otherwise. - private bool IsBeginOperationEvent(ControllerEventArgs args) { - return !insertionInProgress - && args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN; - } + /// + /// Whether this matches the pattern of an event which should begin the creation of a stroke. + /// + /// The controller event arguments. + /// True if this is a 'start stroke' event, false otherwise. + private bool IsBeginOperationEvent(ControllerEventArgs args) + { + return !insertionInProgress + && args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN; + } - private bool IsCompleteSingleClickEvent(ControllerEventArgs args) { - return waitingToDetermineReleaseType - && args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.UP; - } + private bool IsCompleteSingleClickEvent(ControllerEventArgs args) + { + return waitingToDetermineReleaseType + && args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.UP; + } - private bool IsFinishStrokeEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && ((args.Action == ButtonAction.UP && triggerUpToEnd) - || (args.Action == ButtonAction.DOWN && !triggerUpToEnd)) - && insertionInProgress; - } + private bool IsFinishStrokeEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && ((args.Action == ButtonAction.UP && triggerUpToEnd) + || (args.Action == ButtonAction.DOWN && !triggerUpToEnd)) + && insertionInProgress; + } - /// - /// Whether this matches the pattern of an event which should insert a checkpoint into the current stroke. - /// - /// The controller event arguments. - /// True if this is an 'insert stroke checkpoint' event, false otherwise. - private bool IsInsertStrokeCheckpointEvent(ControllerEventArgs args) { - // If the controller is a Rift, use the secondary button to signal a checkpoint; otherwise use the touchpad. - if (Config.Instance.VrHardware == VrHardware.Rift) { - return insertionInProgress - && args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.SecondaryButton - && args.Action == ButtonAction.UP - && !PeltzerMain.Instance.Zoomer.Zooming; - } - return insertionInProgress - && args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.UP - && (args.TouchpadLocation == TouchpadLocation.CENTER || args.TouchpadLocation == TouchpadLocation.LEFT - || args.TouchpadLocation == TouchpadLocation.RIGHT) - && !PeltzerMain.Instance.Zoomer.Zooming; - } + /// + /// Whether this matches the pattern of an event which should insert a checkpoint into the current stroke. + /// + /// The controller event arguments. + /// True if this is an 'insert stroke checkpoint' event, false otherwise. + private bool IsInsertStrokeCheckpointEvent(ControllerEventArgs args) + { + // If the controller is a Rift, use the secondary button to signal a checkpoint; otherwise use the touchpad. + if (Config.Instance.VrHardware == VrHardware.Rift) + { + return insertionInProgress + && args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.SecondaryButton + && args.Action == ButtonAction.UP + && !PeltzerMain.Instance.Zoomer.Zooming; + } + return insertionInProgress + && args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.UP + && (args.TouchpadLocation == TouchpadLocation.CENTER || args.TouchpadLocation == TouchpadLocation.LEFT + || args.TouchpadLocation == TouchpadLocation.RIGHT) + && !PeltzerMain.Instance.Zoomer.Zooming; + } - // Touchpad Hover Tests. - private static bool IsSetUpHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.TOP; - } + // Touchpad Hover Tests. + private static bool IsSetUpHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.TOP; + } - /// - /// Override logic for 'undo' for in-progress strokes. - /// - /// True if anything changed as a result of this operation. - public bool Undo() { - if (!insertionInProgress) { - return false; - } - SetCheckpointingMode(/*shouldBeManuallyCheckpointing*/ false); - ClearState(); - return true; - } + /// + /// Override logic for 'undo' for in-progress strokes. + /// + /// True if anything changed as a result of this operation. + public bool Undo() + { + if (!insertionInProgress) + { + return false; + } + SetCheckpointingMode(/*shouldBeManuallyCheckpointing*/ false); + ClearState(); + return true; + } - public void ClearState() { - insertionInProgress = false; - waitingToForceCheckpoint = false; - strokeSpine = new List(); - // TODO: Do we want to do this when you change tools? - // SetCheckpointingMode(/*shouldBeManuallyCheckpointing*/ false); - CreateNewVolumeAndHighlight(); - } + public void ClearState() + { + insertionInProgress = false; + waitingToForceCheckpoint = false; + strokeSpine = new List(); + // TODO: Do we want to do this when you change tools? + // SetCheckpointingMode(/*shouldBeManuallyCheckpointing*/ false); + CreateNewVolumeAndHighlight(); + } - private static bool IsSetDownHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.BOTTOM; - } + private static bool IsSetDownHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.BOTTOM; + } - private static bool IsSetLeftHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.LEFT; - } + private static bool IsSetLeftHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.LEFT; + } - private static bool IsSetRightHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.RIGHT; - } + private static bool IsSetRightHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.RIGHT; + } - private static bool IsSetCenterHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.CENTER; - } + private static bool IsSetCenterHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.CENTER; + } - private static bool IsUnsetAllHoverTooltipsEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.NONE; - } + private static bool IsUnsetAllHoverTooltipsEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.NONE; + } - private static bool IsToggleCheckpointingModeEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.ApplicationMenu - && args.Action == ButtonAction.DOWN; - } + private static bool IsToggleCheckpointingModeEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.ApplicationMenu + && args.Action == ButtonAction.DOWN; + } - public bool IsStroking() { - return insertionInProgress; - } + public bool IsStroking() + { + return insertionInProgress; + } - public bool IsManualStroking() { - return isManualCheckpointing; - } + public bool IsManualStroking() + { + return isManualCheckpointing; + } - /// - /// Whether this matches a start snapping event. - /// - /// The controller event arguments. - /// True if the palette trigger is down. - private static bool IsStartSnapEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN; - } + /// + /// Whether this matches a start snapping event. + /// + /// The controller event arguments. + /// True if the palette trigger is down. + private static bool IsStartSnapEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN; + } - /// - /// Whether this matches an end snapping event. - /// - /// The controller event arguments. - /// True if the palette trigger is up. - private static bool IsEndSnapEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.UP; - } + /// + /// Whether this matches an end snapping event. + /// + /// The controller event arguments. + /// True if the palette trigger is up. + private static bool IsEndSnapEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.UP; + } - private void ModeChangedHandler(ControllerMode oldMode, ControllerMode newMode) { - if (currentHighlight != null) { - currentHighlight.SetActive(newMode == ControllerMode.insertStroke); - } + private void ModeChangedHandler(ControllerMode oldMode, ControllerMode newMode) + { + if (currentHighlight != null) + { + currentHighlight.SetActive(newMode == ControllerMode.insertStroke); + } - // TODO: If in progress, do something to cancel it. - if (oldMode == ControllerMode.insertStroke) { - UnsetAllHoverTooltips(); - } - } + // TODO: If in progress, do something to cancel it. + if (oldMode == ControllerMode.insertStroke) + { + UnsetAllHoverTooltips(); + } + } - private void MaterialChangeHandler(int newMaterialId) { - MMesh.GeometryOperation matChangeOp = currentVolume.StartOperation(); - foreach (Face face in currentVolume.GetFaces()) { - matChangeOp.ModifyFace(face.id, face.vertexIds, new FaceProperties(newMaterialId)); - } - // Material change only - don't recalc normals. - matChangeOp.CommitWithoutRecalculation(); - MMesh.AttachMeshToGameObject(worldSpace, currentHighlight, currentVolume, /* updateOnly */ true, - MaterialRegistry.GetMaterialAndColorById(newMaterialId)); - } + private void MaterialChangeHandler(int newMaterialId) + { + MMesh.GeometryOperation matChangeOp = currentVolume.StartOperation(); + foreach (Face face in currentVolume.GetFaces()) + { + matChangeOp.ModifyFace(face.id, face.vertexIds, new FaceProperties(newMaterialId)); + } + // Material change only - don't recalc normals. + matChangeOp.CommitWithoutRecalculation(); + MMesh.AttachMeshToGameObject(worldSpace, currentHighlight, currentVolume, /* updateOnly */ true, + MaterialRegistry.GetMaterialAndColorById(newMaterialId)); + } - /// - /// Unset all of the touchpad hover text tooltips. - /// - private void UnsetAllHoverTooltips() { - peltzerController.controllerGeometry.freeformTooltipUp.SetActive(false); - peltzerController.controllerGeometry.freeformTooltipDown.SetActive(false); - peltzerController.controllerGeometry.freeformTooltipLeft.SetActive(false); - peltzerController.controllerGeometry.freeformTooltipRight.SetActive(false); - peltzerController.controllerGeometry.freeformTooltipCenter.SetActive(false); - peltzerController.SetTouchpadHoverTexture(TouchpadHoverState.NONE); - } + /// + /// Unset all of the touchpad hover text tooltips. + /// + private void UnsetAllHoverTooltips() + { + peltzerController.controllerGeometry.freeformTooltipUp.SetActive(false); + peltzerController.controllerGeometry.freeformTooltipDown.SetActive(false); + peltzerController.controllerGeometry.freeformTooltipLeft.SetActive(false); + peltzerController.controllerGeometry.freeformTooltipRight.SetActive(false); + peltzerController.controllerGeometry.freeformTooltipCenter.SetActive(false); + peltzerController.SetTouchpadHoverTexture(TouchpadHoverState.NONE); + } - /// - /// Makes only the supplied tooltip visible and ensures the others are off. - /// - /// The tooltip text to activate. - /// The hover state. - private void SetHoverTooltip(GameObject tooltip, TouchpadHoverState state) { - if (!tooltip.activeSelf) { - UnsetAllHoverTooltips(); - tooltip.SetActive(true); - peltzerController.SetTouchpadHoverTexture(state); - peltzerController.TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_1, - 0.003f, - 0.15f - ); - } + /// + /// Makes only the supplied tooltip visible and ensures the others are off. + /// + /// The tooltip text to activate. + /// The hover state. + private void SetHoverTooltip(GameObject tooltip, TouchpadHoverState state) + { + if (!tooltip.activeSelf) + { + UnsetAllHoverTooltips(); + tooltip.SetActive(true); + peltzerController.SetTouchpadHoverTexture(state); + peltzerController.TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_1, + 0.003f, + 0.15f + ); + } + } } - } } diff --git a/Assets/Scripts/tools/Fuser.cs b/Assets/Scripts/tools/Fuser.cs index 2d9668c6..d8c13896 100644 --- a/Assets/Scripts/tools/Fuser.cs +++ b/Assets/Scripts/tools/Fuser.cs @@ -18,67 +18,76 @@ using System.Linq; using UnityEngine; -namespace com.google.apps.peltzer.client.tools { - class Fuser { - /// - /// Fuses all the given meshes into a single mesh. This is different from grouping because this - /// does not preserve the original components and CAN'T BE UNDONE -- once the meshes are fused, - /// they can never be taken apart. - /// - /// NOTE: This is used for debug purposes only for now. We don't expose any tools that fuse meshes. - /// - /// The meshes to fuse together. - /// The ID of the fused mesh. - /// The fused mesh. - public static MMesh FuseMeshes(IEnumerable meshes, int fusedMeshId) { - // Set up the new MMesh's data. - Vector3 offset = Vector3.zero; - foreach (MMesh mesh in meshes) { - offset += mesh.offset; - } - offset /= meshes.Count(); - Quaternion rotation = Math3d.MostCommonRotation(meshes.Select(m => m.rotation)); - Dictionary verticesById = new Dictionary(); - Dictionary facesById = new Dictionary(); - HashSet allRemixIds = new HashSet(); +namespace com.google.apps.peltzer.client.tools +{ + class Fuser + { + /// + /// Fuses all the given meshes into a single mesh. This is different from grouping because this + /// does not preserve the original components and CAN'T BE UNDONE -- once the meshes are fused, + /// they can never be taken apart. + /// + /// NOTE: This is used for debug purposes only for now. We don't expose any tools that fuse meshes. + /// + /// The meshes to fuse together. + /// The ID of the fused mesh. + /// The fused mesh. + public static MMesh FuseMeshes(IEnumerable meshes, int fusedMeshId) + { + // Set up the new MMesh's data. + Vector3 offset = Vector3.zero; + foreach (MMesh mesh in meshes) + { + offset += mesh.offset; + } + offset /= meshes.Count(); + Quaternion rotation = Math3d.MostCommonRotation(meshes.Select(m => m.rotation)); + Dictionary verticesById = new Dictionary(); + Dictionary facesById = new Dictionary(); + HashSet allRemixIds = new HashSet(); - // Collapse each MMesh into the new data above. - int nextVertexId = 0; - int nextFaceId = 0; - foreach (MMesh mesh in meshes) { - if (mesh.remixIds != null) { - allRemixIds.UnionWith(mesh.remixIds); - } + // Collapse each MMesh into the new data above. + int nextVertexId = 0; + int nextFaceId = 0; + foreach (MMesh mesh in meshes) + { + if (mesh.remixIds != null) + { + allRemixIds.UnionWith(mesh.remixIds); + } - // Copy each vertex with a new id. - Dictionary originalVertexIdsToNewVertexIds = new Dictionary(); - foreach (Vertex originalVertex in mesh.GetVertices()) { - originalVertexIdsToNewVertexIds.Add(originalVertex.id, nextVertexId); - Vector3 newVertexLoc = Quaternion.Inverse(rotation) * - ((mesh.rotation * originalVertex.loc) + mesh.offset - offset); - Vertex newVertex = new Vertex(nextVertexId, newVertexLoc); - verticesById.Add(nextVertexId, newVertex); - nextVertexId++; - } + // Copy each vertex with a new id. + Dictionary originalVertexIdsToNewVertexIds = new Dictionary(); + foreach (Vertex originalVertex in mesh.GetVertices()) + { + originalVertexIdsToNewVertexIds.Add(originalVertex.id, nextVertexId); + Vector3 newVertexLoc = Quaternion.Inverse(rotation) * + ((mesh.rotation * originalVertex.loc) + mesh.offset - offset); + Vertex newVertex = new Vertex(nextVertexId, newVertexLoc); + verticesById.Add(nextVertexId, newVertex); + nextVertexId++; + } - // Copy each face with a new id, referencing into the new vertex ids. - foreach (Face originalFace in mesh.GetFaces()) { - List vertexIds = new List(); - foreach (int originalVertexId in originalFace.vertexIds) { - vertexIds.Add(originalVertexIdsToNewVertexIds[originalVertexId]); - } - - // Can't use original normal because vertices may have been rotated. - Face newFace = new Face(nextFaceId, vertexIds.AsReadOnly(), verticesById, - originalFace.properties); - facesById.Add(nextFaceId, newFace); - nextFaceId++; - } - } + // Copy each face with a new id, referencing into the new vertex ids. + foreach (Face originalFace in mesh.GetFaces()) + { + List vertexIds = new List(); + foreach (int originalVertexId in originalFace.vertexIds) + { + vertexIds.Add(originalVertexIdsToNewVertexIds[originalVertexId]); + } - // Create a new MMesh out of the collapsed data. - return new MMesh(fusedMeshId, offset, rotation, verticesById, facesById, MMesh.GROUP_NONE, - allRemixIds.Count > 0 ? allRemixIds : null); + // Can't use original normal because vertices may have been rotated. + Face newFace = new Face(nextFaceId, vertexIds.AsReadOnly(), verticesById, + originalFace.properties); + facesById.Add(nextFaceId, newFace); + nextFaceId++; + } + } + + // Create a new MMesh out of the collapsed data. + return new MMesh(fusedMeshId, offset, rotation, verticesById, facesById, MMesh.GROUP_NONE, + allRemixIds.Count > 0 ? allRemixIds : null); + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/tools/GifRecorder.cs b/Assets/Scripts/tools/GifRecorder.cs index 72eaf531..89bec74f 100644 --- a/Assets/Scripts/tools/GifRecorder.cs +++ b/Assets/Scripts/tools/GifRecorder.cs @@ -21,89 +21,103 @@ /// Records gifs. While active, collects frames from a camera, and when finished, /// it creates a task to spool those frames out to a gif file. /// -public class GifRecorder : MonoBehaviour { +public class GifRecorder : MonoBehaviour +{ - private static readonly float RECORD_TIME_S = 30f; - private static readonly int FPS = 33; - private static readonly float FRAME_INTERVAL_S = 1.0f / FPS; + private static readonly float RECORD_TIME_S = 30f; + private static readonly int FPS = 33; + private static readonly float FRAME_INTERVAL_S = 1.0f / FPS; - private GameObject eyeCamera; - private Camera gifRecordingCamera; - private GameObject camObj; - private RenderTexture gifRenderTexture; - private GifEncodeTask task = null; - private List capturedGifFrames; - private float intervalTimer = 0; + private GameObject eyeCamera; + private Camera gifRecordingCamera; + private GameObject camObj; + private RenderTexture gifRenderTexture; + private GifEncodeTask task = null; + private List capturedGifFrames; + private float intervalTimer = 0; - private bool recordGif = false; - private int recordFrames = 0; + private bool recordGif = false; + private int recordFrames = 0; - void Start() { - eyeCamera = GameObject.Find("ID_Camera (eye)"); - camObj = new GameObject(); - camObj.name = "GifRecorder"; - camObj.transform.position = Vector3.zero; - camObj.transform.rotation = Quaternion.identity; - gifRecordingCamera = camObj.AddComponent(); - gifRecordingCamera.nearClipPlane = .01f; - camObj.transform.SetParent(eyeCamera.transform); - camObj.transform.position = eyeCamera.transform.position; - camObj.transform.rotation = eyeCamera.transform.rotation; - gifRenderTexture = new RenderTexture(512, 512, 16, RenderTextureFormat.ARGB32); - gifRecordingCamera.targetTexture = gifRenderTexture; - camObj.SetActive(false); - } - - void Update() { - if (task != null && task.IsDone) { - if (task.Error != null && task.Error.Length > 0) { - Debug.Log("gif recording error " + task.Error); - } else { - Debug.Log("gif was recorded"); - } - task = null; + void Start() + { + eyeCamera = GameObject.Find("ID_Camera (eye)"); + camObj = new GameObject(); + camObj.name = "GifRecorder"; + camObj.transform.position = Vector3.zero; + camObj.transform.rotation = Quaternion.identity; + gifRecordingCamera = camObj.AddComponent(); + gifRecordingCamera.nearClipPlane = .01f; + camObj.transform.SetParent(eyeCamera.transform); + camObj.transform.position = eyeCamera.transform.position; + camObj.transform.rotation = eyeCamera.transform.rotation; + gifRenderTexture = new RenderTexture(512, 512, 16, RenderTextureFormat.ARGB32); + gifRecordingCamera.targetTexture = gifRenderTexture; + camObj.SetActive(false); } - if (recordGif) { - if (recordFrames > 0) { - intervalTimer += Time.deltaTime; - if (intervalTimer > FRAME_INTERVAL_S) { - intervalTimer = -FRAME_INTERVAL_S; - RenderTexture.active = gifRenderTexture; - Texture2D frameTexture = - new Texture2D(gifRenderTexture.width, gifRenderTexture.height, TextureFormat.RGB24, false); - frameTexture.ReadPixels(new Rect(0, 0, gifRenderTexture.width, gifRenderTexture.height), 0, 0); - frameTexture.Apply(); - RenderTexture.active = null; - capturedGifFrames.Add(frameTexture.GetPixels32()); - Destroy(frameTexture); - recordFrames--; + + void Update() + { + if (task != null && task.IsDone) + { + if (task.Error != null && task.Error.Length > 0) + { + Debug.Log("gif recording error " + task.Error); + } + else + { + Debug.Log("gif was recorded"); + } + task = null; + } + if (recordGif) + { + if (recordFrames > 0) + { + intervalTimer += Time.deltaTime; + if (intervalTimer > FRAME_INTERVAL_S) + { + intervalTimer = -FRAME_INTERVAL_S; + RenderTexture.active = gifRenderTexture; + Texture2D frameTexture = + new Texture2D(gifRenderTexture.width, gifRenderTexture.height, TextureFormat.RGB24, false); + frameTexture.ReadPixels(new Rect(0, 0, gifRenderTexture.width, gifRenderTexture.height), 0, 0); + frameTexture.Apply(); + RenderTexture.active = null; + capturedGifFrames.Add(frameTexture.GetPixels32()); + Destroy(frameTexture); + recordFrames--; + } + } + else + { + // Recording is finished + recordGif = false; + string filename = Application.persistentDataPath + "/poly_gif_" + Guid.NewGuid().ToString() + ".gif"; + Debug.Log("starting gif save " + filename + " with " + capturedGifFrames.Count + " frames"); + task = new GifEncodeTask( + capturedGifFrames, (int)(FRAME_INTERVAL_S * 1000.0f), + gifRenderTexture.width, gifRenderTexture.height, + filename, + 1f / 8, true); + capturedGifFrames = null; + task.Start(); + camObj.SetActive(false); + } } - } else { - // Recording is finished - recordGif = false; - string filename = Application.persistentDataPath + "/poly_gif_" + Guid.NewGuid().ToString() + ".gif"; - Debug.Log("starting gif save " + filename + " with " + capturedGifFrames.Count + " frames"); - task = new GifEncodeTask( - capturedGifFrames, (int)(FRAME_INTERVAL_S * 1000.0f), - gifRenderTexture.width, gifRenderTexture.height, - filename, - 1f / 8, true); - capturedGifFrames = null; - task.Start(); - camObj.SetActive(false); - } } - } - public void RecordGif() { - if (recordGif || task != null) { - Debug.Log("Can't start new recording while current recording is active."); - return; + public void RecordGif() + { + if (recordGif || task != null) + { + Debug.Log("Can't start new recording while current recording is active."); + return; + } + recordGif = true; + camObj.SetActive(true); + recordFrames = (int)(FPS * RECORD_TIME_S); + capturedGifFrames = new List(recordFrames); + Debug.Log("Starting gif record with " + recordFrames + "frames."); } - recordGif = true; - camObj.SetActive(true); - recordFrames = (int)(FPS * RECORD_TIME_S); - capturedGifFrames = new List(recordFrames); - Debug.Log("Starting gif record with " + recordFrames + "frames."); - } } diff --git a/Assets/Scripts/tools/MoveableObject.cs b/Assets/Scripts/tools/MoveableObject.cs index b539d510..3e915aac 100644 --- a/Assets/Scripts/tools/MoveableObject.cs +++ b/Assets/Scripts/tools/MoveableObject.cs @@ -20,327 +20,379 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.tools.utils; -namespace com.google.apps.peltzer.client.tools { - /// - /// Something outside of the actual model which can be moved with the grab tool. - /// This script expects to be attached to a GameObject representing the grabbable object, and to have a - /// Unity Mesh (as opposed to a Blocks MMesh) representing the object. - /// Supports: - /// - Hover behaviour - /// - Grabbing - /// - Releasing - /// - Deleting - /// - Throwing away - /// - Basic grid alignment - /// - Scaling - /// - public class MoveableObject : MonoBehaviour { +namespace com.google.apps.peltzer.client.tools +{ /// - /// If released with this or more lateral velocity, the object will be thrown away. + /// Something outside of the actual model which can be moved with the grab tool. + /// This script expects to be attached to a GameObject representing the grabbable object, and to have a + /// Unity Mesh (as opposed to a Blocks MMesh) representing the object. + /// Supports: + /// - Hover behaviour + /// - Grabbing + /// - Releasing + /// - Deleting + /// - Throwing away + /// - Basic grid alignment + /// - Scaling /// - private const float THROWING_VELOCITY_THRESHOLD = 2.5f; - - // How close to the quad the user must be to grab the object. - // In Unity units where 1.0 = 1m. - public const float HOVER_DISTANCE = 0.05f; - - // The Unity Mesh of the grabbable object, for render and collision detection. - internal Mesh mesh; - // The Unity Material of the grabbable object, for render. - internal Material material; - // The normal vector of the mesh, in model space. - private Vector3 meshNormalModelSpace; - // The position of the mesh vertices in model space. - private List meshVerticesModelSpace; - - // The position, rotation and scale of the object in model space. - internal Vector3 positionModelSpace = Vector3.zero; - internal Quaternion rotationModelSpace = Quaternion.identity; - internal Vector3 scaleModelSpace = Vector3.one; - - // The position, rotation and scale of the object in model space when it was most-recently grabbed. - internal Vector3 positionAtStartOfMove; - internal Quaternion rotationAtStartOfMove; - internal Vector3 scaleAtStartOfMove; - // The rotation of the controller in model space when the grab movement began. - internal Quaternion controllerRotationAtStartOfMove; - - // A basic shatter effect. - private ParticleSystem shatterPrefab; - // A basic highlight for a hover effect. - static Color BLUE_HIGHLIGHT = new Color(1f, 1f, 1.5f); - - private bool thrownAway = false; - internal bool grabbed = false; - private bool isSnapping = false; - private bool hovered = false; - - /// - /// Sets up the object, allowing it to be moved. Must be called for every object. - /// We do work here rather than in a constructor as this is a MonoBehavior - /// - public virtual void Setup() { - PeltzerMain.Instance.controllerMain.ControllerActionHandler += MoveDetector; - if (shatterPrefab == null) { - shatterPrefab = Resources.Load("Prefabs/Shatter"); - } - } - - /// - /// A basic, overridable shatter effect. - /// - internal virtual void Shatter() { - // Play the shatter effect and noise. - ParticleSystem shatterEffect = Instantiate(shatterPrefab); - shatterEffect.transform.position = gameObject.transform.position; - shatterEffect.startSize = gameObject.transform.localScale.magnitude * 0.3f; - PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.breakSound, /* pitch */ 0.85f); - - // Delete the object. - Delete(); - } - - /// - /// A basic, overridable hover effect. - /// - internal virtual void SetHovered() { - material.color = BLUE_HIGHLIGHT; - } - - /// - /// The reverse of the above hover effect. - /// - internal virtual void SetUnhovered() { - material.color = Color.white; - } - - /// - /// A basic, overridable deletion behavior. - /// - internal virtual void Delete() { - PeltzerMain.Instance.controllerMain.ControllerActionHandler -= MoveDetector; - } - - /// - /// Scale behavior, to be implemented by the specific object. - /// - /// True if scaling up, false if scaling down. - internal virtual void Scale(bool scaleUp) { - // Do nothing by default. - } + public class MoveableObject : MonoBehaviour + { + /// + /// If released with this or more lateral velocity, the object will be thrown away. + /// + private const float THROWING_VELOCITY_THRESHOLD = 2.5f; + + // How close to the quad the user must be to grab the object. + // In Unity units where 1.0 = 1m. + public const float HOVER_DISTANCE = 0.05f; + + // The Unity Mesh of the grabbable object, for render and collision detection. + internal Mesh mesh; + // The Unity Material of the grabbable object, for render. + internal Material material; + // The normal vector of the mesh, in model space. + private Vector3 meshNormalModelSpace; + // The position of the mesh vertices in model space. + private List meshVerticesModelSpace; + + // The position, rotation and scale of the object in model space. + internal Vector3 positionModelSpace = Vector3.zero; + internal Quaternion rotationModelSpace = Quaternion.identity; + internal Vector3 scaleModelSpace = Vector3.one; + + // The position, rotation and scale of the object in model space when it was most-recently grabbed. + internal Vector3 positionAtStartOfMove; + internal Quaternion rotationAtStartOfMove; + internal Vector3 scaleAtStartOfMove; + // The rotation of the controller in model space when the grab movement began. + internal Quaternion controllerRotationAtStartOfMove; + + // A basic shatter effect. + private ParticleSystem shatterPrefab; + // A basic highlight for a hover effect. + static Color BLUE_HIGHLIGHT = new Color(1f, 1f, 1.5f); + + private bool thrownAway = false; + internal bool grabbed = false; + private bool isSnapping = false; + private bool hovered = false; + + /// + /// Sets up the object, allowing it to be moved. Must be called for every object. + /// We do work here rather than in a constructor as this is a MonoBehavior + /// + public virtual void Setup() + { + PeltzerMain.Instance.controllerMain.ControllerActionHandler += MoveDetector; + if (shatterPrefab == null) + { + shatterPrefab = Resources.Load("Prefabs/Shatter"); + } + } - /// - /// A basic, overridable grab behaviour that attaches the moveable object to the controller. - /// - internal virtual void Grab() { - // Store the values when grab began, to allow us to calculate deltas later. - positionAtStartOfMove = positionModelSpace; - rotationAtStartOfMove = rotationModelSpace; - controllerRotationAtStartOfMove = PeltzerMain.Instance.peltzerController.LastRotationModel; - scaleAtStartOfMove = scaleModelSpace; + /// + /// A basic, overridable shatter effect. + /// + internal virtual void Shatter() + { + // Play the shatter effect and noise. + ParticleSystem shatterEffect = Instantiate(shatterPrefab); + shatterEffect.transform.position = gameObject.transform.position; + shatterEffect.startSize = gameObject.transform.localScale.magnitude * 0.3f; + PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.breakSound, /* pitch */ 0.85f); + + // Delete the object. + Delete(); + } - UpdatePosition(); + /// + /// A basic, overridable hover effect. + /// + internal virtual void SetHovered() + { + material.color = BLUE_HIGHLIGHT; + } - transform.SetParent(PeltzerMain.Instance.peltzerController.transform); - grabbed = true; + /// + /// The reverse of the above hover effect. + /// + internal virtual void SetUnhovered() + { + material.color = Color.white; + } - // Disable the menu and palette while something is grabbed. - PeltzerMain.Instance.restrictionManager.paletteAllowed = false; - PeltzerMain.Instance.restrictionManager.menuActionsAllowed = false; - } + /// + /// A basic, overridable deletion behavior. + /// + internal virtual void Delete() + { + PeltzerMain.Instance.controllerMain.ControllerActionHandler -= MoveDetector; + } - /// - /// A basic, overridable release behaviour that detaches the moveable object from the controller. - /// - internal virtual void Release() { - transform.SetParent(null); - grabbed = false; + /// + /// Scale behavior, to be implemented by the specific object. + /// + /// True if scaling up, false if scaling down. + internal virtual void Scale(bool scaleUp) + { + // Do nothing by default. + } - // Enable the menu and palette when something is no longer grabbed. - PeltzerMain.Instance.restrictionManager.paletteAllowed = true; - PeltzerMain.Instance.restrictionManager.menuActionsAllowed = true; - } + /// + /// A basic, overridable grab behaviour that attaches the moveable object to the controller. + /// + internal virtual void Grab() + { + // Store the values when grab began, to allow us to calculate deltas later. + positionAtStartOfMove = positionModelSpace; + rotationAtStartOfMove = rotationModelSpace; + controllerRotationAtStartOfMove = PeltzerMain.Instance.peltzerController.LastRotationModel; + scaleAtStartOfMove = scaleModelSpace; + + UpdatePosition(); + + transform.SetParent(PeltzerMain.Instance.peltzerController.transform); + grabbed = true; + + // Disable the menu and palette while something is grabbed. + PeltzerMain.Instance.restrictionManager.paletteAllowed = false; + PeltzerMain.Instance.restrictionManager.menuActionsAllowed = false; + } - /// - /// A basic, overridable behavior for throwing away the object. - /// - /// The velocity of the controller throwing the object. - internal virtual void ThrowAway(Vector3 velocity) { - // Set the object free. - transform.SetParent(null); - grabbed = false; - thrownAway = true; - - // Apply the force. - Rigidbody rigidbody = gameObject.GetComponent(); - if (rigidbody == null) { - rigidbody = gameObject.AddComponent(); - } - - rigidbody.isKinematic = false; - rigidbody.AddForce(velocity, ForceMode.VelocityChange); - } + /// + /// A basic, overridable release behaviour that detaches the moveable object from the controller. + /// + internal virtual void Release() + { + transform.SetParent(null); + grabbed = false; + + // Enable the menu and palette when something is no longer grabbed. + PeltzerMain.Instance.restrictionManager.paletteAllowed = true; + PeltzerMain.Instance.restrictionManager.menuActionsAllowed = true; + } - /// - /// A basic, overridable behaviour to destroy this object. - /// - internal void Destroy() { - PeltzerMain.Instance.controllerMain.ControllerActionHandler -= MoveDetector; - GameObject.Destroy(gameObject); - } + /// + /// A basic, overridable behavior for throwing away the object. + /// + /// The velocity of the controller throwing the object. + internal virtual void ThrowAway(Vector3 velocity) + { + // Set the object free. + transform.SetParent(null); + grabbed = false; + thrownAway = true; + + // Apply the force. + Rigidbody rigidbody = gameObject.GetComponent(); + if (rigidbody == null) + { + rigidbody = gameObject.AddComponent(); + } + + rigidbody.isKinematic = false; + rigidbody.AddForce(velocity, ForceMode.VelocityChange); + } - /// - /// Update's the object's position and rotation, aligning to the grid if needed. - /// - internal void UpdatePosition() { - WorldSpace worldSpace = PeltzerMain.Instance.worldSpace; - - // Calculate the new position/rotation in model space. - positionModelSpace = worldSpace.WorldToModel(transform.position); - rotationModelSpace = worldSpace.WorldOrientationToModel(transform.rotation); - - // The snap it to the grid and put it back into world-space if needed. - if (PeltzerMain.Instance.peltzerController.isBlockMode || isSnapping) { - positionModelSpace = GridUtils.SnapToGrid(positionModelSpace); - transform.position = PeltzerMain.Instance.worldSpace.ModelToWorld(positionModelSpace); - Quaternion rotDelta = Quaternion.Inverse(controllerRotationAtStartOfMove) - * PeltzerMain.Instance.peltzerController.LastRotationModel; - rotationModelSpace = rotationAtStartOfMove * GridUtils.SnapToNearest(rotDelta, Quaternion.identity, 90f); - transform.rotation = PeltzerMain.Instance.worldSpace.ModelOrientationToWorld(rotationModelSpace); - } - } + /// + /// A basic, overridable behaviour to destroy this object. + /// + internal void Destroy() + { + PeltzerMain.Instance.controllerMain.ControllerActionHandler -= MoveDetector; + GameObject.Destroy(gameObject); + } - /// - /// Recalculates the vertices and normal of the underlying mesh, to account for changes in position, - /// rotation and scale. - /// - internal void RecalculateVerticesAndNormal() { - Matrix4x4 mat = Matrix4x4.TRS(positionModelSpace, rotationModelSpace, scaleModelSpace); - meshVerticesModelSpace = new List(mesh.vertexCount); - foreach (Vector3 pos in mesh.vertices) { - meshVerticesModelSpace.Add(mat.MultiplyPoint(pos)); - } - meshNormalModelSpace = MeshMath.CalculateNormal(meshVerticesModelSpace); - } + /// + /// Update's the object's position and rotation, aligning to the grid if needed. + /// + internal void UpdatePosition() + { + WorldSpace worldSpace = PeltzerMain.Instance.worldSpace; + + // Calculate the new position/rotation in model space. + positionModelSpace = worldSpace.WorldToModel(transform.position); + rotationModelSpace = worldSpace.WorldOrientationToModel(transform.rotation); + + // The snap it to the grid and put it back into world-space if needed. + if (PeltzerMain.Instance.peltzerController.isBlockMode || isSnapping) + { + positionModelSpace = GridUtils.SnapToGrid(positionModelSpace); + transform.position = PeltzerMain.Instance.worldSpace.ModelToWorld(positionModelSpace); + Quaternion rotDelta = Quaternion.Inverse(controllerRotationAtStartOfMove) + * PeltzerMain.Instance.peltzerController.LastRotationModel; + rotationModelSpace = rotationAtStartOfMove * GridUtils.SnapToNearest(rotDelta, Quaternion.identity, 90f); + transform.rotation = PeltzerMain.Instance.worldSpace.ModelOrientationToWorld(rotationModelSpace); + } + } - /// - /// Set whether the object is currently being hovered by the controller. - /// - private void SetHoverStatus() { - PeltzerController controller = PeltzerMain.Instance.peltzerController; - - if ((controller.mode == ControllerMode.move || controller.mode == ControllerMode.delete) - && MeshMath.IsCloseToFaceInterior(controller.LastPositionModel, - meshNormalModelSpace, meshVerticesModelSpace, HOVER_DISTANCE, /* vertexDistanceThreshold */ 0)) { - if (!hovered) { - hovered = true; - SetHovered(); + /// + /// Recalculates the vertices and normal of the underlying mesh, to account for changes in position, + /// rotation and scale. + /// + internal void RecalculateVerticesAndNormal() + { + Matrix4x4 mat = Matrix4x4.TRS(positionModelSpace, rotationModelSpace, scaleModelSpace); + meshVerticesModelSpace = new List(mesh.vertexCount); + foreach (Vector3 pos in mesh.vertices) + { + meshVerticesModelSpace.Add(mat.MultiplyPoint(pos)); + } + meshNormalModelSpace = MeshMath.CalculateNormal(meshVerticesModelSpace); } - } else if (hovered) { - hovered = false; - SetUnhovered(); - } - } - /// - /// Returns whether or not the user just did a "throw" gesture with the controller when releasing. - /// - /// True if and only if the user just did a throw gesture. - private bool IsThrowing() { - return PeltzerMain.Instance.peltzerController.GetVelocity().magnitude > THROWING_VELOCITY_THRESHOLD; - } + /// + /// Set whether the object is currently being hovered by the controller. + /// + private void SetHoverStatus() + { + PeltzerController controller = PeltzerMain.Instance.peltzerController; + + if ((controller.mode == ControllerMode.move || controller.mode == ControllerMode.delete) + && MeshMath.IsCloseToFaceInterior(controller.LastPositionModel, + meshNormalModelSpace, meshVerticesModelSpace, HOVER_DISTANCE, /* vertexDistanceThreshold */ 0)) + { + if (!hovered) + { + hovered = true; + SetHovered(); + } + } + else if (hovered) + { + hovered = false; + SetUnhovered(); + } + } - /// - /// Offers basic interaction with the moveable object: grabbing, releasing, throwing, scaling and deleting. - /// - private void MoveDetector(object sender, ControllerEventArgs args) { - PeltzerController controller = PeltzerMain.Instance.peltzerController; - - if (args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN) { - isSnapping = true; - } else if (args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.UP) { - isSnapping = false; - } else if (controller.mode == ControllerMode.move - && args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN - && hovered) { - Grab(); - } else if (controller.mode == ControllerMode.delete - && args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN - && hovered) { - Delete(); - } else if (controller.mode == ControllerMode.move - && args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.UP - && grabbed) { - if (IsThrowing()) { - ThrowAway(controller.GetVelocity()); - } else { - Release(); + /// + /// Returns whether or not the user just did a "throw" gesture with the controller when releasing. + /// + /// True if and only if the user just did a throw gesture. + private bool IsThrowing() + { + return PeltzerMain.Instance.peltzerController.GetVelocity().magnitude > THROWING_VELOCITY_THRESHOLD; } - } else if (controller.mode == ControllerMode.move - && args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN - && grabbed || hovered) { - if (args.TouchpadLocation == TouchpadLocation.TOP) { - Scale(/* scaleUp */ true); - } else if (args.TouchpadLocation == TouchpadLocation.BOTTOM) { - Scale(/* scaleUp */ false); + + /// + /// Offers basic interaction with the moveable object: grabbing, releasing, throwing, scaling and deleting. + /// + private void MoveDetector(object sender, ControllerEventArgs args) + { + PeltzerController controller = PeltzerMain.Instance.peltzerController; + + if (args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN) + { + isSnapping = true; + } + else if (args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.UP) + { + isSnapping = false; + } + else if (controller.mode == ControllerMode.move + && args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN + && hovered) + { + Grab(); + } + else if (controller.mode == ControllerMode.delete + && args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN + && hovered) + { + Delete(); + } + else if (controller.mode == ControllerMode.move + && args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.UP + && grabbed) + { + if (IsThrowing()) + { + ThrowAway(controller.GetVelocity()); + } + else + { + Release(); + } + } + else if (controller.mode == ControllerMode.move + && args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN + && grabbed || hovered) + { + if (args.TouchpadLocation == TouchpadLocation.TOP) + { + Scale(/* scaleUp */ true); + } + else if (args.TouchpadLocation == TouchpadLocation.BOTTOM) + { + Scale(/* scaleUp */ false); + } + } } - } - } - /// - /// Updates the objects transform values in order to keep them consistent with any transformations - /// applied to the world. - /// - void UpdateTransform() { - transform.position = PeltzerMain.Instance.worldSpace.ModelToWorld(positionModelSpace); - transform.rotation = PeltzerMain.Instance.worldSpace.ModelOrientationToWorld(rotationModelSpace); - transform.localScale = PeltzerMain.Instance.worldSpace.scale * scaleModelSpace; - } + /// + /// Updates the objects transform values in order to keep them consistent with any transformations + /// applied to the world. + /// + void UpdateTransform() + { + transform.position = PeltzerMain.Instance.worldSpace.ModelToWorld(positionModelSpace); + transform.rotation = PeltzerMain.Instance.worldSpace.ModelOrientationToWorld(rotationModelSpace); + transform.localScale = PeltzerMain.Instance.worldSpace.scale * scaleModelSpace; + } - void Update() { - if (thrownAway) { - if (transform.position.y <= 0.125f) { - // If the object was thrown and has hit the ground, shatter it. - Shatter(); - } else { - // Else, keep updating its position for render. - UpdatePosition(); + void Update() + { + if (thrownAway) + { + if (transform.position.y <= 0.125f) + { + // If the object was thrown and has hit the ground, shatter it. + Shatter(); + } + else + { + // Else, keep updating its position for render. + UpdatePosition(); + } + return; + } + + if (grabbed) + { + // If this object is being grabbed, its position will update by virtue of it being a child of the controller. + // We do, however, need to update its position and orientation in model space for future operations. + UpdatePosition(); + } + else + { + // If this object is not grabbed, check to see if it is hovered. + SetHoverStatus(); + + // Update the object's transforms in case of any world movement while the object is not grabbed. + UpdateTransform(); + } } - return; - } - - if (grabbed) { - // If this object is being grabbed, its position will update by virtue of it being a child of the controller. - // We do, however, need to update its position and orientation in model space for future operations. - UpdatePosition(); - } else { - // If this object is not grabbed, check to see if it is hovered. - SetHoverStatus(); - - // Update the object's transforms in case of any world movement while the object is not grabbed. - UpdateTransform(); - } - } - void LateUpdate() { - // Render the object in the world. To do that, we transform our model space position/rotation/scale to - // world space to get an appropriate matrix. - Matrix4x4 mat = PeltzerMain.Instance.worldSpace.modelToWorld * - Matrix4x4.TRS(positionModelSpace, rotationModelSpace, scaleModelSpace); - // Draw in the PolyAssets layer -- won't show up in thumbnails. - Graphics.DrawMesh(mesh, mat, material, MeshWithMaterialRenderer.DEFAULT_LAYER); + void LateUpdate() + { + // Render the object in the world. To do that, we transform our model space position/rotation/scale to + // world space to get an appropriate matrix. + Matrix4x4 mat = PeltzerMain.Instance.worldSpace.modelToWorld * + Matrix4x4.TRS(positionModelSpace, rotationModelSpace, scaleModelSpace); + // Draw in the PolyAssets layer -- won't show up in thumbnails. + Graphics.DrawMesh(mesh, mat, material, MeshWithMaterialRenderer.DEFAULT_LAYER); + } } - } } diff --git a/Assets/Scripts/tools/Mover.cs b/Assets/Scripts/tools/Mover.cs index 139b32d2..8099c8f3 100644 --- a/Assets/Scripts/tools/Mover.cs +++ b/Assets/Scripts/tools/Mover.cs @@ -24,1312 +24,1554 @@ using com.google.apps.peltzer.client.model.render; using UnityEngine.UI; -namespace com.google.apps.peltzer.client.tools { - /// - /// Tool which handles moving an entire mesh. Since "cloning" is a very similar operation - /// to moving, we also support "insertVolume" commands where the ShapeType is COPY. - /// - public class Mover : MonoBehaviour, IMeshRenderOwner { - // Enums for Type of move. - // MOVE = hide and then move. - // CLONE = clone and then move. - // CREATE = create new and then move. This is used for Zandria models. - public enum MoveType { NONE, MOVE, CLONE, CREATE } - // Possible actions that the "group" button can perform, depending on the selected meshes. - enum GroupButtonAction { NONE, GROUP, UNGROUP }; - - // Colours for trackpad icons, to show whether they're enabled or disabled. - private readonly Color ENABLED_COLOR = new Color(1, 1, 1, 1); - private readonly Color DISABLED_COLOR = new Color(1, 1, 1, 70f / 255f); // aka 70/255. - - // Parameters for varying the shatter sound. - private const float SHATTER_PITCH_MAX = 1.3f; - private const float SHATTER_PITCH_MIN = 0.35f; - private const float SHATTER_STEP_MAX = 2f; - private const float SHATTER_STEP_MIN = 0.03f; - - /// - /// Whether or not the user has performed the GROUP action at least once. - /// - public bool userHasPerformedGroupAction = false; - - /// - /// The maximum size of any mesh's bounding box in Grid units, to provide a reasonable - /// upper bound for scale operations. Assuming a bounding box maximum of 15metres, and a grid - /// unit of 1cm, this is 500 aka 5metres or 1/3 of the bounding box. - /// - private const int MAX_MESH_SIZE_IN_GRID_UNITS = 500; - /// - /// How long an object should remain in the scene after being thrown away. - /// - private const float OBJECT_LIFETIME_AFTER_THROWING = 0.6f; - /// - /// Adjusts the size of the shatter particle effect. - /// - private const float SHATTER_SCALE = 0.8f; - /// - /// If released with this or more lateral velocity, an object will be thrown away. - /// - private const float THROWING_VELOCITY_THRESHOLD = 2.5f; - /// - /// The number of seconds the user scales an object continuously before we start increasing the rate of the - /// scaling process. - /// - private const float FAST_SCALE_THRESHOLD = 1f; - /// - /// If user is scaling for a what we consider a long time, we will increase the scaling rate by this amount. - /// - private const int LONG_TERM_SCALE_FACTOR = 1; - /// - /// Number of times to show the group tooltip. - /// - private const int SHOW_GROUP_TOOLTIP_COUNT = 2; - public ControllerMain controllerMain; - /// - /// A reference to a controller capable of issuing move commands. - /// - private PeltzerController peltzerController; - /// - /// A reference to the overall model being built. - /// - private Model model; - /// - /// The spatial index of the model. - /// - private SpatialIndex spatialIndex; - /// - /// Selector for detecting which item is hovered or selected. - /// - private Selector selector; - /// - /// Library for playing sounds. - /// - private AudioLibrary audioLibrary; - /// - /// A utility to transform from model space to world space. - /// - private WorldSpace worldSpace; - /// - /// The volume insertion tool. - /// - private VolumeInserter volumeInserter; - /// - /// The meshes currently being held as a part of a move or copy operation. - /// - private HeldMeshes heldMeshes; - /// - /// Whether, in the current move operation, we have modified any meshes. We need to know this in order to decide, - /// post-move, whether to just move them or replace them. - /// - private bool modifiedAnyMeshesDuringMove; - /// - /// Whether we are doing a 'move' a 'clone and move' or a 'create and move'. - /// - public MoveType currentMoveType; - /// - /// The user has pulled the trigger in 'copy' mode. - /// - private bool isCopyGrabbing; - /// - /// A queue of pairs that contain objects that have been thrown away and the size of the original mesh. - /// - private Queue> objectsToDelete = new Queue> (); - /// - /// The TextMesh component of the left tooltip (so we can modify the text at runtime). - /// - private TextMesh tooltipLeftTextMesh; - /// - /// The Particle System object used to imitate a shatter effect. - /// - private ParticleSystem shatterPrefab; - /// - /// Number of times the group tooltip has been shown. - /// - private int groupTooltipShownCount = 0; - /// - /// Number of times the ungroup tooltip has been shown. - /// - private int ungroupTooltipShownCount = 0; - - // Detection for trigger down & straight back up, vs trigger down and hold -- either of which - // begins a move or a copy. - private bool triggerUpToRelease; - private float triggerDownTime; - private bool waitingToDetermineReleaseType; - - // Detection for how long to show the tooltip that explains using the half trigger to preview snapping. - float timeStartedHalfTriggerDown = 0f; - private const float HALF_TRIGGER_TOOLTIP_DURATION = 2f; - private bool halfTriggerTooltipShown = false; - - // Whether the user is currently holding the scale button. - private ScaleType scaleType = ScaleType.NONE; - // The number of scale events received without the touchpad being released. - private int continuousScaleEvents = 0; - - /// - // The benchmark we set to determine when the user has been scaling for a long time. - /// - private float longTermScaleStartTime = float.MaxValue; - - private MeshRepresentationCache meshRepresentationCache; - +namespace com.google.apps.peltzer.client.tools +{ /// - /// Holds a cached reference to the initial material for the grab tool. + /// Tool which handles moving an entire mesh. Since "cloning" is a very similar operation + /// to moving, we also support "insertVolume" commands where the ShapeType is COPY. /// - private Material defaultGrabMaterial; - /// - /// A reference to the current instance of the toolhead's surface for changing the material. - /// - private GameObject copyToolheadSurface; - - /// - /// Whether the user has ever multi-selected in this session - /// - private bool userHasMultiSelected = false; - - /// - /// Used to determine if we should show the snap tooltip or not. Don't show the tooltip if the user already - /// showed enough knowledge of how to snap. - /// - private int completedSnaps = 0; - private const int SNAP_KNOW_HOW_COUNT = 3; - - /// - /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. - /// As such, this setup method must be called before the tool is used for it to have a valid state. - /// - public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, - PaletteController paletteController, Selector selector, VolumeInserter volumeInserter, AudioLibrary audioLibrary, - WorldSpace worldSpace, SpatialIndex spatialIndex, MeshRepresentationCache meshRepresentationCache) { - this.model = model; - this.controllerMain = controllerMain; - this.peltzerController = peltzerController; - this.selector = selector; - this.volumeInserter = volumeInserter; - this.audioLibrary = audioLibrary; - this.spatialIndex = spatialIndex; - this.worldSpace = worldSpace; - this.meshRepresentationCache = meshRepresentationCache; - this.shatterPrefab = Resources.Load("Prefabs/Shatter"); - controllerMain.ControllerActionHandler += ControllerEventHandler; - peltzerController.ModeChangedHandler += ModeChangeEventHandler; - peltzerController.shapesMenu.ShapeMenuItemChangedHandler += ShapeChangedEventHandler; - } - - /// - /// An event handler that listens for controller input and delegates accordingly. - /// - /// The sender of the controller event. - /// The controller event arguments. - private void ControllerEventHandler(object sender, ControllerEventArgs args) { - if (peltzerController.mode != ControllerMode.move) - return; - - // get state for tool mode. - UpdateTooltip(); - - if (IsBeginOperationEvent(args)) { - // If the user is clicking in free space, moving and grabbing is enabled, else we are in selection mode. - // Selection mode is only ever enabled if click to select is enabled. While selection is enabled, a click - // in free space will allow moving and cloning. - int? nearestMesh = null; - if (Features.clickToSelectEnabled && - spatialIndex.FindNearestMeshTo(peltzerController.LastPositionModel, 0.1f / worldSpace.scale, out nearestMesh)) { - // Check for a nearest mesh in the case that the user is clicking from the inside of a mesh's bounding box. - // 0.1f is the threshold above which a mesh is considered too far away to select. This is in Unity units, - // where 1.0f = 1 meter by default. - selector.SelectMeshAtPosition(peltzerController.LastPositionModel, Selector.MESHES_ONLY, /* forceSelection = */ true); - } else { - triggerUpToRelease = false; - waitingToDetermineReleaseType = true; - triggerDownTime = Time.time; - MaybeStartMoveOrClone(); - } - } else if (IsCompleteSingleClickEvent(args)) { - waitingToDetermineReleaseType = false; - triggerUpToRelease = false; - } else if (IsReleaseEvent(args)) { - heldMeshes.HideSnapGuides(); - CompleteMove(); - } else if (Features.useContinuousSnapDetection && IsStartSnapDetectionEvent(args) && heldMeshes != null) { - // Show the snap guides if the trigger is slightly pressed. - heldMeshes.DetectSnap(); - // Show half trigger down tooltip - if (timeStartedHalfTriggerDown == 0f && !halfTriggerTooltipShown) { - timeStartedHalfTriggerDown = Time.time; - PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); - PeltzerMain.Instance.paletteController.ShowHoldTriggerHalfwaySnapTooltip(); - } else if (Time.time - timeStartedHalfTriggerDown >= HALF_TRIGGER_TOOLTIP_DURATION) { - // The user has held down the half trigger for enough time to indicate they understand it. Stop - // showing the tooltip. - PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); - halfTriggerTooltipShown = true; + public class Mover : MonoBehaviour, IMeshRenderOwner + { + // Enums for Type of move. + // MOVE = hide and then move. + // CLONE = clone and then move. + // CREATE = create new and then move. This is used for Zandria models. + public enum MoveType { NONE, MOVE, CLONE, CREATE } + // Possible actions that the "group" button can perform, depending on the selected meshes. + enum GroupButtonAction { NONE, GROUP, UNGROUP }; + + // Colours for trackpad icons, to show whether they're enabled or disabled. + private readonly Color ENABLED_COLOR = new Color(1, 1, 1, 1); + private readonly Color DISABLED_COLOR = new Color(1, 1, 1, 70f / 255f); // aka 70/255. + + // Parameters for varying the shatter sound. + private const float SHATTER_PITCH_MAX = 1.3f; + private const float SHATTER_PITCH_MIN = 0.35f; + private const float SHATTER_STEP_MAX = 2f; + private const float SHATTER_STEP_MIN = 0.03f; + + /// + /// Whether or not the user has performed the GROUP action at least once. + /// + public bool userHasPerformedGroupAction = false; + + /// + /// The maximum size of any mesh's bounding box in Grid units, to provide a reasonable + /// upper bound for scale operations. Assuming a bounding box maximum of 15metres, and a grid + /// unit of 1cm, this is 500 aka 5metres or 1/3 of the bounding box. + /// + private const int MAX_MESH_SIZE_IN_GRID_UNITS = 500; + /// + /// How long an object should remain in the scene after being thrown away. + /// + private const float OBJECT_LIFETIME_AFTER_THROWING = 0.6f; + /// + /// Adjusts the size of the shatter particle effect. + /// + private const float SHATTER_SCALE = 0.8f; + /// + /// If released with this or more lateral velocity, an object will be thrown away. + /// + private const float THROWING_VELOCITY_THRESHOLD = 2.5f; + /// + /// The number of seconds the user scales an object continuously before we start increasing the rate of the + /// scaling process. + /// + private const float FAST_SCALE_THRESHOLD = 1f; + /// + /// If user is scaling for a what we consider a long time, we will increase the scaling rate by this amount. + /// + private const int LONG_TERM_SCALE_FACTOR = 1; + /// + /// Number of times to show the group tooltip. + /// + private const int SHOW_GROUP_TOOLTIP_COUNT = 2; + public ControllerMain controllerMain; + /// + /// A reference to a controller capable of issuing move commands. + /// + private PeltzerController peltzerController; + /// + /// A reference to the overall model being built. + /// + private Model model; + /// + /// The spatial index of the model. + /// + private SpatialIndex spatialIndex; + /// + /// Selector for detecting which item is hovered or selected. + /// + private Selector selector; + /// + /// Library for playing sounds. + /// + private AudioLibrary audioLibrary; + /// + /// A utility to transform from model space to world space. + /// + private WorldSpace worldSpace; + /// + /// The volume insertion tool. + /// + private VolumeInserter volumeInserter; + /// + /// The meshes currently being held as a part of a move or copy operation. + /// + private HeldMeshes heldMeshes; + /// + /// Whether, in the current move operation, we have modified any meshes. We need to know this in order to decide, + /// post-move, whether to just move them or replace them. + /// + private bool modifiedAnyMeshesDuringMove; + /// + /// Whether we are doing a 'move' a 'clone and move' or a 'create and move'. + /// + public MoveType currentMoveType; + /// + /// The user has pulled the trigger in 'copy' mode. + /// + private bool isCopyGrabbing; + /// + /// A queue of pairs that contain objects that have been thrown away and the size of the original mesh. + /// + private Queue> objectsToDelete = new Queue>(); + /// + /// The TextMesh component of the left tooltip (so we can modify the text at runtime). + /// + private TextMesh tooltipLeftTextMesh; + /// + /// The Particle System object used to imitate a shatter effect. + /// + private ParticleSystem shatterPrefab; + /// + /// Number of times the group tooltip has been shown. + /// + private int groupTooltipShownCount = 0; + /// + /// Number of times the ungroup tooltip has been shown. + /// + private int ungroupTooltipShownCount = 0; + + // Detection for trigger down & straight back up, vs trigger down and hold -- either of which + // begins a move or a copy. + private bool triggerUpToRelease; + private float triggerDownTime; + private bool waitingToDetermineReleaseType; + + // Detection for how long to show the tooltip that explains using the half trigger to preview snapping. + float timeStartedHalfTriggerDown = 0f; + private const float HALF_TRIGGER_TOOLTIP_DURATION = 2f; + private bool halfTriggerTooltipShown = false; + + // Whether the user is currently holding the scale button. + private ScaleType scaleType = ScaleType.NONE; + // The number of scale events received without the touchpad being released. + private int continuousScaleEvents = 0; + + /// + // The benchmark we set to determine when the user has been scaling for a long time. + /// + private float longTermScaleStartTime = float.MaxValue; + + private MeshRepresentationCache meshRepresentationCache; + + /// + /// Holds a cached reference to the initial material for the grab tool. + /// + private Material defaultGrabMaterial; + /// + /// A reference to the current instance of the toolhead's surface for changing the material. + /// + private GameObject copyToolheadSurface; + + /// + /// Whether the user has ever multi-selected in this session + /// + private bool userHasMultiSelected = false; + + /// + /// Used to determine if we should show the snap tooltip or not. Don't show the tooltip if the user already + /// showed enough knowledge of how to snap. + /// + private int completedSnaps = 0; + private const int SNAP_KNOW_HOW_COUNT = 3; + + /// + /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. + /// As such, this setup method must be called before the tool is used for it to have a valid state. + /// + public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, + PaletteController paletteController, Selector selector, VolumeInserter volumeInserter, AudioLibrary audioLibrary, + WorldSpace worldSpace, SpatialIndex spatialIndex, MeshRepresentationCache meshRepresentationCache) + { + this.model = model; + this.controllerMain = controllerMain; + this.peltzerController = peltzerController; + this.selector = selector; + this.volumeInserter = volumeInserter; + this.audioLibrary = audioLibrary; + this.spatialIndex = spatialIndex; + this.worldSpace = worldSpace; + this.meshRepresentationCache = meshRepresentationCache; + this.shatterPrefab = Resources.Load("Prefabs/Shatter"); + controllerMain.ControllerActionHandler += ControllerEventHandler; + peltzerController.ModeChangedHandler += ModeChangeEventHandler; + peltzerController.shapesMenu.ShapeMenuItemChangedHandler += ShapeChangedEventHandler; } - } else if (Features.useContinuousSnapDetection && IsStopSnapDetectionEvent(args) && heldMeshes != null) { - // If we are previewing the snap guide with a half trigger press and then release the trigger, - // hide the guide. - heldMeshes.HideSnapGuides(); - if (!halfTriggerTooltipShown) { - // Restart the timer tracking half trigger use. - timeStartedHalfTriggerDown = 0f; - } - } else if (IsStartSnapEvent(args) && heldMeshes != null) { - heldMeshes.DetectSnap(); - heldMeshes.StartSnapping(model, spatialIndex); - if (!halfTriggerTooltipShown) { - // Restart the timer tracking half trigger use. - timeStartedHalfTriggerDown = 0f; - } - if (completedSnaps < SNAP_KNOW_HOW_COUNT) { - PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); - } - } else if (IsEndSnapEvent(args) && heldMeshes != null) { - heldMeshes.StopSnapping(); - heldMeshes.HideSnapGuides(); - completedSnaps++; - PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); - } else if (IsScaleEvent(args)) { - if (scaleType == ScaleType.NONE) { - longTermScaleStartTime = Time.time + FAST_SCALE_THRESHOLD; - } - continuousScaleEvents++; - ScaleMeshes(args.TouchpadLocation == TouchpadLocation.TOP, continuousScaleEvents); - } else if (IsStopScalingEvent(args)) { - StopScaling(); - } else if (IsToggleCopyEvent(args) && PeltzerMain.Instance.restrictionManager.copyingAllowed) { - if (PeltzerMain.Instance.restrictionManager.copyingAllowed && currentMoveType != MoveType.CREATE) { - PeltzerMain.Instance.restrictionManager.movingMeshesAllowed = true; - } - if (currentMoveType != MoveType.CLONE && heldMeshes == null) { - // If the user was previously not in clone mode, toggle to clone mode and try cloning immediately. - currentMoveType = MoveType.CLONE; - MaybeStartMoveOrClone(); - } else if (heldMeshes == null) { - // If the user was previously in clone mode and is not in the middle of an operation, toggle to move mode. - currentMoveType = MoveType.MOVE; - } - } else if (IsGroupEvent(args)) { - // Perform the appropriate group action (contextual, depending on the selected meshes). - PerformGroupButtonAction(); - } else if (IsFlipEvent(args)) { - FlipMeshes(); - } else if (IsSetUpHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadUpAllowed) { - SetHoverTooltip(peltzerController.controllerGeometry.moverTooltipUp, TouchpadHoverState.UP); - } else if (IsSetDownHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadDownAllowed) { - SetHoverTooltip(peltzerController.controllerGeometry.moverTooltipDown, TouchpadHoverState.DOWN); - } else if (IsSetLeftHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed - && currentMoveType != MoveType.CREATE) { - // We don't show the 'copy' tooltip when a user is importing from the PolyMenu, per bug - SetHoverTooltip(peltzerController.controllerGeometry.moverTooltipLeft, TouchpadHoverState.LEFT); - } else if (IsSetRightHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadRightAllowed) { - SetHoverTooltip(peltzerController.controllerGeometry.moverTooltipRight, TouchpadHoverState.RIGHT); - } else if (IsUnsetAllHoverTooltipsEvent(args)) { - UnsetAllHoverTooltips(); - } - } - private bool IsBeginOperationEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN - && heldMeshes == null; - } - - private bool IsCompleteSingleClickEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.UP - && waitingToDetermineReleaseType; - } - - private bool IsReleaseEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && ((args.Action == ButtonAction.UP && triggerUpToRelease) - || (args.Action == ButtonAction.DOWN && !triggerUpToRelease)) - && heldMeshes != null; - } - - private static bool IsStartSnapEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN; - } - - private static bool IsStartSnapDetectionEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.LIGHT_DOWN; - } - - private static bool IsStopSnapDetectionEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.LIGHT_UP; - } - - private static bool IsEndSnapEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.UP; - } - - private static bool IsGroupEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.ApplicationMenu - && args.Action == ButtonAction.DOWN; - } - - private static bool IsFlipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN && args.TouchpadLocation == TouchpadLocation.RIGHT - && !PeltzerMain.Instance.Zoomer.Zooming; - } - - private bool IsScaleEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN - && (args.TouchpadLocation == TouchpadLocation.BOTTOM || args.TouchpadLocation == TouchpadLocation.TOP) - && (args.TouchpadOverlay == TouchpadOverlay.MOVE) - && !PeltzerMain.Instance.Zoomer.Zooming; - } - - /// - /// Whether this matches the pattern of a 'stop scaling' event. - /// - /// The controller event arguments. - /// True if this is a scale event, false otherwise. - private bool IsStopScalingEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.UP - && scaleType != ScaleType.NONE; - } - - private static bool IsToggleCopyEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN && args.TouchpadLocation == TouchpadLocation.LEFT - && !PeltzerMain.Instance.Zoomer.Zooming; - } - - // Touchpad Hover Tests - private static bool IsSetUpHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.TOP; - } - - private static bool IsSetDownHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.BOTTOM; - } - - private static bool IsSetLeftHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.LEFT; - } - - private static bool IsSetRightHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.RIGHT; - } - - private static bool IsUnsetAllHoverTooltipsEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.NONE; - } - - /// - /// Called when the controller mode changes to allow for any setup that may be necessary. - /// - /// The current mode of the controller. - private void ModeChangeEventHandler(ControllerMode oldMode, ControllerMode newMode) { - if (oldMode == ControllerMode.move) { - ClearState(); - } - - if (newMode == ControllerMode.move && completedSnaps < SNAP_KNOW_HOW_COUNT) { - PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); - } - } - - private void ShapeChangedEventHandler(int newMenuItemId) { - ClearState(); - } - - public void InvalidateCachedMaterial() { - defaultGrabMaterial = null; - } - - // Start a move or clone operation if any meshes were hovered or selected when the trigger went down. - private bool MaybeStartMoveOrClone() { - if (currentMoveType != MoveType.CREATE) { - // There might be some selected/hovered meshes, if so, set up a move/clone, else do nothing. - IEnumerable meshes = selector.SelectedOrHoveredMeshes(); - if (meshes.Count() > 0) { - IEnumerable selectedMeshes = meshes.Select(m => model.GetMesh(m)); - StartMove(selectedMeshes); - return true; + /// + /// An event handler that listens for controller input and delegates accordingly. + /// + /// The sender of the controller event. + /// The controller event arguments. + private void ControllerEventHandler(object sender, ControllerEventArgs args) + { + if (peltzerController.mode != ControllerMode.move) + return; + + // get state for tool mode. + UpdateTooltip(); + + if (IsBeginOperationEvent(args)) + { + // If the user is clicking in free space, moving and grabbing is enabled, else we are in selection mode. + // Selection mode is only ever enabled if click to select is enabled. While selection is enabled, a click + // in free space will allow moving and cloning. + int? nearestMesh = null; + if (Features.clickToSelectEnabled && + spatialIndex.FindNearestMeshTo(peltzerController.LastPositionModel, 0.1f / worldSpace.scale, out nearestMesh)) + { + // Check for a nearest mesh in the case that the user is clicking from the inside of a mesh's bounding box. + // 0.1f is the threshold above which a mesh is considered too far away to select. This is in Unity units, + // where 1.0f = 1 meter by default. + selector.SelectMeshAtPosition(peltzerController.LastPositionModel, Selector.MESHES_ONLY, /* forceSelection = */ true); + } + else + { + triggerUpToRelease = false; + waitingToDetermineReleaseType = true; + triggerDownTime = Time.time; + MaybeStartMoveOrClone(); + } + } + else if (IsCompleteSingleClickEvent(args)) + { + waitingToDetermineReleaseType = false; + triggerUpToRelease = false; + } + else if (IsReleaseEvent(args)) + { + heldMeshes.HideSnapGuides(); + CompleteMove(); + } + else if (Features.useContinuousSnapDetection && IsStartSnapDetectionEvent(args) && heldMeshes != null) + { + // Show the snap guides if the trigger is slightly pressed. + heldMeshes.DetectSnap(); + // Show half trigger down tooltip + if (timeStartedHalfTriggerDown == 0f && !halfTriggerTooltipShown) + { + timeStartedHalfTriggerDown = Time.time; + PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); + PeltzerMain.Instance.paletteController.ShowHoldTriggerHalfwaySnapTooltip(); + } + else if (Time.time - timeStartedHalfTriggerDown >= HALF_TRIGGER_TOOLTIP_DURATION) + { + // The user has held down the half trigger for enough time to indicate they understand it. Stop + // showing the tooltip. + PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); + halfTriggerTooltipShown = true; + } + } + else if (Features.useContinuousSnapDetection && IsStopSnapDetectionEvent(args) && heldMeshes != null) + { + // If we are previewing the snap guide with a half trigger press and then release the trigger, + // hide the guide. + heldMeshes.HideSnapGuides(); + if (!halfTriggerTooltipShown) + { + // Restart the timer tracking half trigger use. + timeStartedHalfTriggerDown = 0f; + } + } + else if (IsStartSnapEvent(args) && heldMeshes != null) + { + heldMeshes.DetectSnap(); + heldMeshes.StartSnapping(model, spatialIndex); + if (!halfTriggerTooltipShown) + { + // Restart the timer tracking half trigger use. + timeStartedHalfTriggerDown = 0f; + } + if (completedSnaps < SNAP_KNOW_HOW_COUNT) + { + PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); + } + } + else if (IsEndSnapEvent(args) && heldMeshes != null) + { + heldMeshes.StopSnapping(); + heldMeshes.HideSnapGuides(); + completedSnaps++; + PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); + } + else if (IsScaleEvent(args)) + { + if (scaleType == ScaleType.NONE) + { + longTermScaleStartTime = Time.time + FAST_SCALE_THRESHOLD; + } + continuousScaleEvents++; + ScaleMeshes(args.TouchpadLocation == TouchpadLocation.TOP, continuousScaleEvents); + } + else if (IsStopScalingEvent(args)) + { + StopScaling(); + } + else if (IsToggleCopyEvent(args) && PeltzerMain.Instance.restrictionManager.copyingAllowed) + { + if (PeltzerMain.Instance.restrictionManager.copyingAllowed && currentMoveType != MoveType.CREATE) + { + PeltzerMain.Instance.restrictionManager.movingMeshesAllowed = true; + } + if (currentMoveType != MoveType.CLONE && heldMeshes == null) + { + // If the user was previously not in clone mode, toggle to clone mode and try cloning immediately. + currentMoveType = MoveType.CLONE; + MaybeStartMoveOrClone(); + } + else if (heldMeshes == null) + { + // If the user was previously in clone mode and is not in the middle of an operation, toggle to move mode. + currentMoveType = MoveType.MOVE; + } + } + else if (IsGroupEvent(args)) + { + // Perform the appropriate group action (contextual, depending on the selected meshes). + PerformGroupButtonAction(); + } + else if (IsFlipEvent(args)) + { + FlipMeshes(); + } + else if (IsSetUpHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadUpAllowed) + { + SetHoverTooltip(peltzerController.controllerGeometry.moverTooltipUp, TouchpadHoverState.UP); + } + else if (IsSetDownHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadDownAllowed) + { + SetHoverTooltip(peltzerController.controllerGeometry.moverTooltipDown, TouchpadHoverState.DOWN); + } + else if (IsSetLeftHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed + && currentMoveType != MoveType.CREATE) + { + // We don't show the 'copy' tooltip when a user is importing from the PolyMenu, per bug + SetHoverTooltip(peltzerController.controllerGeometry.moverTooltipLeft, TouchpadHoverState.LEFT); + } + else if (IsSetRightHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadRightAllowed) + { + SetHoverTooltip(peltzerController.controllerGeometry.moverTooltipRight, TouchpadHoverState.RIGHT); + } + else if (IsUnsetAllHoverTooltipsEvent(args)) + { + UnsetAllHoverTooltips(); + } } - } - return false; - } - /// - /// Updates the grab/copy tooltip - /// - public void UpdateTooltip() { - peltzerController.controllerGeometry.grabTooltips.SetActive(peltzerController.mode == ControllerMode.move); - - // Set the grab hand to be green wireframe or regular, depending if we're in clone mode or not. - if (copyToolheadSurface != null) { - if (defaultGrabMaterial == null) { - defaultGrabMaterial = copyToolheadSurface.GetComponent().material; - } - if (PeltzerMain.Instance.restrictionManager.IsControllerModeAllowed(ControllerMode.move)) { - if (currentMoveType == MoveType.CLONE) { - copyToolheadSurface.GetComponent().material = - MaterialRegistry.GetMaterialAndColorById(MaterialRegistry.GREEN_WIREFRAME_ID).material; - } else { - copyToolheadSurface.GetComponent().material = defaultGrabMaterial; - } - } - } else { - copyToolheadSurface = peltzerController.attachedToolHead.transform.Find( - "grabTool_Geo/GrabHandFBX3/Hand_object").gameObject; - } - - // Find the current tool state. - bool inProgress = IsMoving(); - bool hoveringOrSelected = - selector.selectedMeshes.Count > 0 || selector.hoverMeshes.Count > 0; - - if (!PeltzerMain.Instance.restrictionManager.touchpadHighlightingAllowed) { - Overlay moveOverlay = peltzerController.controllerGeometry.moveOverlay.GetComponent(); - - // Set resize & flip active if we're hovering, selected, or in progress. - moveOverlay.upIcon.color = inProgress || hoveringOrSelected ? ENABLED_COLOR : DISABLED_COLOR; - moveOverlay.downIcon.color = inProgress || hoveringOrSelected ? ENABLED_COLOR : DISABLED_COLOR; - - // flip - moveOverlay.rightIcon.color = inProgress || hoveringOrSelected ? ENABLED_COLOR : DISABLED_COLOR; - - // Set copy active if we're hovering, selected, or neither of those but are in copy-mode. - moveOverlay.leftIcon.color = - hoveringOrSelected || (!hoveringOrSelected && currentMoveType == MoveType.CLONE) ? - ENABLED_COLOR : DISABLED_COLOR; - } - - // Figure out what the "group" button should do and update UI to reflect the action. - GroupButtonAction groupButtonAction = GetGroupButtonAction(GetSelectedGrabbedOrHoveredMeshes()); - - if (groupButtonAction != GroupButtonAction.NONE) { - peltzerController.SetApplicationButtonOverlay(ButtonMode.ACTIVE); - } else { - peltzerController.SetApplicationButtonOverlay(ButtonMode.WAITING); - } - - Overlay overlay = peltzerController.controllerGeometry.moveOverlay.GetComponent(); - // We want the group icon on if the user can group or when the button is waiting to show the user - // what the button would do. - overlay.onIcon.gameObject.SetActive(groupButtonAction != GroupButtonAction.UNGROUP); - overlay.offIcon.gameObject.SetActive(groupButtonAction == GroupButtonAction.UNGROUP); - - GameObject groupTooltip = peltzerController.handedness == Handedness.RIGHT ? - peltzerController.controllerGeometry.groupLeftTooltip : peltzerController.controllerGeometry.groupRightTooltip; - GameObject ungroupTooltip = peltzerController.handedness == Handedness.RIGHT ? - peltzerController.controllerGeometry.ungroupLeftTooltip : peltzerController.controllerGeometry.ungroupRightTooltip; - - if (groupButtonAction == GroupButtonAction.GROUP - && groupTooltipShownCount < SHOW_GROUP_TOOLTIP_COUNT - && !PeltzerMain.Instance.tutorialManager.TutorialOccurring() - && !PeltzerMain.Instance.HasDisabledTooltips) { - peltzerController.controllerGeometry.groupTooltipRoot.SetActive(true); - groupTooltip.SetActive(true); - ungroupTooltip.SetActive(false); - } else if (groupButtonAction == GroupButtonAction.UNGROUP - && ungroupTooltipShownCount < SHOW_GROUP_TOOLTIP_COUNT - && !PeltzerMain.Instance.tutorialManager.TutorialOccurring() - && !PeltzerMain.Instance.HasDisabledTooltips) { - peltzerController.controllerGeometry.groupTooltipRoot.SetActive(true); - ungroupTooltip.SetActive(true); - groupTooltip.SetActive(false); - } else { - peltzerController.controllerGeometry.groupTooltipRoot.SetActive(false); - // Only increment the 'times shown' count when deselected, or else may count as separate times if the tooltip - // that was still active was "set" again. - if (groupTooltip.activeSelf) { - groupTooltip.SetActive(false); - groupTooltipShownCount++; - } else if (ungroupTooltip.activeSelf) { - ungroupTooltip.SetActive(false); - ungroupTooltipShownCount++; + private bool IsBeginOperationEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN + && heldMeshes == null; } - } - - // Set the state of the Grab Toolhead tooltip which happens to be the first child - index 0. - // We wish to show this tooltip if: - // - There are at least two meshes in the scene. - // - The user has not multi-selected in this session. - if (Features.showMultiselectTooltip) { - userHasMultiSelected |= PeltzerMain.Instance.GetSelector().selectedMeshes.Count > 1; - bool showTooltip = PeltzerMain.Instance.peltzerController.mode == ControllerMode.move - && !userHasMultiSelected - && !PeltzerMain.Instance.HasDisabledTooltips - && PeltzerMain.Instance.model.GetNumberOfMeshes() > 1; - peltzerController.attachedToolHead.transform.GetChild(0).gameObject.SetActive(showTooltip); - } - } - /// - /// Makes only the supplied tooltip visible and ensures the others are off. - /// - /// The tooltip text to activate. - /// The hover state. - private void SetHoverTooltip(GameObject tooltip, TouchpadHoverState state) { - if (IsMoving() || selector.selectedMeshes.Count > 0 || selector.hoverMeshes.Count > 0) { - if (!tooltip.activeSelf) { - UnsetAllHoverTooltips(); - tooltip.SetActive(true); - peltzerController.SetTouchpadHoverTexture(state); - peltzerController.TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_1, - 0.003f, - 0.15f - ); + private bool IsCompleteSingleClickEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.UP + && waitingToDetermineReleaseType; } - } else { - UnsetAllHoverTooltips(); - } - } - - /// - /// Unset all of the touchpad hover text tooltips. - /// - private void UnsetAllHoverTooltips() { - peltzerController.controllerGeometry.moverTooltipUp.SetActive(false); - peltzerController.controllerGeometry.moverTooltipDown.SetActive(false); - peltzerController.controllerGeometry.moverTooltipLeft.SetActive(false); - peltzerController.controllerGeometry.moverTooltipRight.SetActive(false); - peltzerController.SetTouchpadHoverTexture(TouchpadHoverState.NONE); - } - /// - /// Add a shatter effect to thrown objects. - /// - /// The GameObject to shatter. - /// The magnitude of the GameObject's meshes bounds size. - private void ShatterObject(GameObject objectToDelete, float scaleFactor) { - ParticleSystem shatterEffect = Instantiate(shatterPrefab); - shatterEffect.transform.position = objectToDelete.transform.position; - - ParticleSystem.MainModule mainModule = shatterEffect.main; - mainModule.startSize = scaleFactor * SHATTER_SCALE; - - float step = Mathf.Clamp(Mathf.SmoothStep(SHATTER_STEP_MIN, SHATTER_STEP_MAX, scaleFactor), 0f, 1f); - float pitch = Mathf.Lerp(SHATTER_PITCH_MAX, SHATTER_PITCH_MIN, step); - audioLibrary.PlayClip(audioLibrary.breakSound, pitch); - - int materialId = objectToDelete.GetComponent().meshes[0].materialAndColor.matId; - shatterEffect.GetComponent().material = - MaterialRegistry.GetMaterialWithAlbedoById(materialId); - } - - /// - /// Each frame, if a mesh is currently held, update its position in world-space relative - /// to its original position, and the delta between the controller's position at world-start - /// and the controller's current position. - /// - private void Update() { - // We need to clean up any 'thrown away' objects when their time comes (even if the user has changed mode). - while (objectsToDelete.Count > 0 && (objectsToDelete.Peek().Key.transform.position.y <= 0.125f)) { - KeyValuePair pairToDelete = objectsToDelete.Dequeue(); - - ShatterObject(pairToDelete.Key, pairToDelete.Value); - DestroyImmediate(pairToDelete.Key); - } - - if (!PeltzerController.AcquireIfNecessary(ref peltzerController) - || peltzerController.mode != ControllerMode.move) { - return; - } - - UpdateTooltip(); - - // If we have not grabbed any meshes let the selector find meshes to grab. - if (heldMeshes == null) { - selector.SelectMeshAtPosition(peltzerController.LastPositionModel, Selector.MESHES_ONLY); - } else { - // If a move is in progress, and the trigger has been down for longer than WAIT_THRESHOLD, then this is - // a hold-trigger-and-drag operation which can be completed by raising the trigger. - if (waitingToDetermineReleaseType && Time.time - triggerDownTime > PeltzerController.SINGLE_CLICK_THRESHOLD) { - waitingToDetermineReleaseType = false; - triggerUpToRelease = true; + private bool IsReleaseEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && ((args.Action == ButtonAction.UP && triggerUpToRelease) + || (args.Action == ButtonAction.DOWN && !triggerUpToRelease)) + && heldMeshes != null; } - heldMeshes.UpdatePositions(); - } - - peltzerController.heldMeshes = heldMeshes; - } - - public void StartMove(IEnumerable selectedMeshes) { - if (!PeltzerMain.Instance.restrictionManager.movingMeshesAllowed) { - return; - } - - selector.EndMultiSelection(); - - // Hide the overlay. - peltzerController.HideTooltips(); - - // A move is being started, inform the user. - peltzerController.TriggerHapticFeedback(); - audioLibrary.PlayClip(currentMoveType == MoveType.MOVE ? - audioLibrary.grabMeshSound : audioLibrary.copySound); - - // We'll generate a preview and a copy of the original mesh, for each selected mesh. - heldMeshes = gameObject.AddComponent(); - heldMeshes.Setup(selectedMeshes, peltzerController.LastPositionModel, peltzerController.LastRotationModel, - peltzerController, worldSpace, meshRepresentationCache); - - // If we are copying, then we unhide the meshes now, as the original meshes will not be affected. - // If we are moving, we don't bother unhiding the original meshes, as Mover will be using previews. - if (currentMoveType == MoveType.MOVE) { - foreach (HeldMeshes.HeldMesh heldMesh in heldMeshes.heldMeshes) { - model.ClaimMesh(heldMesh.Mesh.id, this); + private static bool IsStartSnapEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN; } - } else { - selector.DeselectAll(); - } - } - - public bool IsMoving() { - return heldMeshes != null; - } - - /// - /// Throw an object away and mark it for deletion. - /// - /// The object to delete. - /// The velocity with which to release it. - /// The magnitude of the mesh's bounds, used to scale the shatter size. - private void ThrowObjectAway(GameObject unlovedObject, Vector3 velocity, float meshMagnitude) { - MeshWithMaterialRenderer mwmRenderer = unlovedObject.GetComponent(); - unlovedObject.transform.position = mwmRenderer.GetPositionInWorldSpace(); - mwmRenderer.UseGameObjectPosition = true; - - // Apply the force. - Rigidbody rigidbody = unlovedObject.GetComponent(); - if (rigidbody == null) { - rigidbody = unlovedObject.AddComponent(); - } - rigidbody.isKinematic = false; - rigidbody.AddForce(velocity, ForceMode.VelocityChange); - - // Schedule for deletion. - objectsToDelete.Enqueue(new KeyValuePair(unlovedObject, meshMagnitude)); - } - /// - /// Returns whether or not the user just did a "throw" gesture with the controller, thus indicating their disdain - /// for the currently held meshes and a corresponding desire to be rid of them. - /// - /// True if and only if the user just did a throw gesture. - private bool IsThrowing() { - return peltzerController.GetVelocity().magnitude > THROWING_VELOCITY_THRESHOLD - && PeltzerMain.Instance.restrictionManager.throwAwayAllowed; - } - - /// - /// Throws away the currently grabbed meshes, applying a force to them and scheduling them for deletion. - /// - private void ThrowGrabbedMeshes(out List commands) { - commands = new List(heldMeshes.heldMeshes.Count); - foreach (HeldMeshes.HeldMesh heldMesh in heldMeshes.heldMeshes) { - ThrowObjectAway(heldMesh.Preview, peltzerController.GetVelocity(), heldMesh.Mesh.bounds.size.magnitude); - if (currentMoveType == MoveType.MOVE) { - commands.Add(new DeleteMeshCommand(heldMesh.Mesh.id)); - model.RelinquishMesh(heldMesh.Mesh.id, this); + private static bool IsStartSnapDetectionEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.LIGHT_DOWN; } - } - heldMeshes.heldMeshes.Clear(); - } - /// - /// Moves the currently grabbed meshes. - /// - /// Out param. Returns the list of model commands that resulted from the move. - /// True if there were any invalid meshes, false if all meshes were valid. - private bool MoveGrabbedMeshes(out List commands) { - bool anyInvalidMeshes = false; - commands = new List(heldMeshes.heldMeshes.Count); - - foreach (HeldMeshes.HeldMesh heldMesh in heldMeshes.heldMeshes) { - MMesh mesh = heldMesh.Mesh; - GameObject preview = heldMesh.Preview; - - if (modifiedAnyMeshesDuringMove) { - - MeshWithMaterialRenderer meshRenderer = preview.GetComponent(); - // If we modified any meshes, we delete and re-add any affected meshes, but first, we need to update their - // position/rotation to match the preview. - mesh.offset = meshRenderer.GetPositionInModelSpace(); - mesh.rotation = meshRenderer.GetOrientationInModelSpace(); - if (model.CanAddMesh(mesh)) { - commands.Add(new ReplaceMeshCommand(mesh.id, mesh)); - } else { - anyInvalidMeshes = true; - } - } else { - // If we didn't modify any meshes, we issue a command to move any affected meshes, by their respective - // positional and rotational deltas. - MMesh originalMesh = model.GetMesh(mesh.id); - MeshWithMaterialRenderer meshRenderer = preview.GetComponent(); - Quaternion rotationDelta = Quaternion.Inverse(originalMesh.rotation) - * meshRenderer.GetOrientationInModelSpace(); - Vector3 positionDelta = meshRenderer.GetPositionInModelSpace() - originalMesh.offset; - if (model.CanMoveMesh(originalMesh, positionDelta, rotationDelta)) { - commands.Add(new MoveMeshCommand(originalMesh.id, positionDelta, rotationDelta)); - } else { - anyInvalidMeshes = true; - } + private static bool IsStopSnapDetectionEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.LIGHT_UP; } - model.RelinquishMesh(mesh.id, this); - } - - DestroyImmediate(heldMeshes); - return anyInvalidMeshes; - } - - /// - /// Creates (inserts into the model for the first time) the currently grabbed meshes. - /// - /// Out param. Returns the list of model commands that resulted from the create. - /// True if there were any invalid meshes, false if all meshes were valid. - private bool CreateGrabbedMeshes(out List commands) { - bool anyInvalidMeshes = false; - commands = new List(heldMeshes.heldMeshes.Count); - - foreach (HeldMeshes.HeldMesh heldMesh in heldMeshes.heldMeshes) { - MMesh mesh = heldMesh.Mesh; - GameObject preview = heldMesh.Preview; - - MeshWithMaterialRenderer renderer = preview.GetComponent(); - mesh.offset = renderer.GetPositionInModelSpace(); - mesh.rotation = renderer.GetOrientationInModelSpace(); - if (model.CanAddMesh(mesh)) { - commands.Add(new AddMeshCommand(mesh)); - } else { - anyInvalidMeshes = true; + private static bool IsEndSnapEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.UP; } - DestroyImmediate(preview); - } - - - return anyInvalidMeshes; - } - /// - /// Copies the currently grabbed meshes. - /// - /// Out param. Returns the list of model commands that resulted from the copy. - /// True if there were any invalid meshes, false if all meshes were valid. - private bool CopyGrabbedMeshes(out List commands) { - bool anyInvalidMeshes = false; - commands = new List(heldMeshes.heldMeshes.Count); - - // To clone groups correctly, we must generate a new group ID for each group ID in the meshes to copy, and use - // the new group IDs for the copy. That way, a copy of a group will be a new group. This dictionary stores a - // mapping from old group ID to new group ID. We populate it lazily, generating a new group ID for each - // old group ID that we find. - Dictionary groupMapping = new Dictionary(); - - // GROUP_NONE just maps to GROUP_NONE. - groupMapping[MMesh.GROUP_NONE] = MMesh.GROUP_NONE; - - foreach (HeldMeshes.HeldMesh heldMesh in heldMeshes.heldMeshes) { - MMesh mesh = heldMesh.Mesh; - GameObject preview = heldMesh.Preview; - - // Generate a new group ID if we haven't seen this group before. - if (mesh.groupId != MMesh.GROUP_NONE && !groupMapping.ContainsKey(mesh.groupId)) { - groupMapping[mesh.groupId] = model.GenerateGroupId(); + private static bool IsGroupEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.ApplicationMenu + && args.Action == ButtonAction.DOWN; } - MMesh copy = mesh.CloneWithNewIdAndGroup(model.GenerateMeshId(), groupMapping[mesh.groupId]); - MeshWithMaterialRenderer renderer = preview.GetComponent(); - copy.offset = renderer.GetPositionInModelSpace(); - copy.rotation = renderer.GetOrientationInModelSpace(); - if (model.CanAddMesh(copy)) { - commands.Add(new CopyMeshCommand(mesh.id, copy)); - } else { - anyInvalidMeshes = true; + private static bool IsFlipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN && args.TouchpadLocation == TouchpadLocation.RIGHT + && !PeltzerMain.Instance.Zoomer.Zooming; } - DestroyImmediate(preview); - } - - return anyInvalidMeshes; - } - - private void CompleteMove() { - // If we weren't moving anything, calling this function is redundant, return. - if (heldMeshes == null) { - return; - } - bool throwAway = IsThrowing(); - - // Get the list of commands for move/copy and find out if this would be an invalid operation. - List commands = new List(); - bool anyInvalidMeshes = false; - if (throwAway) { - ThrowGrabbedMeshes(out commands); - } else if (currentMoveType == MoveType.MOVE) { - anyInvalidMeshes = MoveGrabbedMeshes(out commands); - } else if (currentMoveType == MoveType.CREATE) { - anyInvalidMeshes = CreateGrabbedMeshes(out commands); - } else { - anyInvalidMeshes = CopyGrabbedMeshes(out commands); - } - - if (!anyInvalidMeshes) { - if (commands.Count > 0) { - model.ApplyCommand(new CompositeCommand(commands)); + private bool IsScaleEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN + && (args.TouchpadLocation == TouchpadLocation.BOTTOM || args.TouchpadLocation == TouchpadLocation.TOP) + && (args.TouchpadOverlay == TouchpadOverlay.MOVE) + && !PeltzerMain.Instance.Zoomer.Zooming; } - // TODO(bug): Add audio for throwing away. - audioLibrary.PlayClip(currentMoveType == MoveType.MOVE ? - audioLibrary.releaseMeshSound : audioLibrary.pasteMeshSound); - peltzerController.TriggerHapticFeedback(); - PeltzerMain.Instance.movesCompleted++; - } else { - audioLibrary.PlayClip(audioLibrary.errorSound); - peltzerController.TriggerHapticFeedback(); - } - - if (!throwAway) { - heldMeshes.DestroyPreviews(); - } - - if (currentMoveType == MoveType.CREATE) { - // Close the details menu after a user imports. - // This is a potentially useful UX action for the user, but more importantly it allows - // us to avoid making a copy of all meshes here, as we're actually grabbing the meshes from the details panel - // to import into the scene. If the user (for some reason) wants to import the same mesh again, they'll have to - // re-open the details panel, thereby re-generating the meshes. - PeltzerMain.Instance.GetPolyMenuMain().SetActiveMenu(menu.PolyMenuMain.Menu.TOOLS_MENU); - } - ClearState(); - } - // Reset everything to a clean, default state. - public void ClearState() { - isCopyGrabbing = false; - modifiedAnyMeshesDuringMove = false; - waitingToDetermineReleaseType = false; - - if (heldMeshes != null) { - if (currentMoveType == MoveType.MOVE) { - foreach (int meshId in heldMeshes.GetMeshIds()) { - model.RelinquishMesh(meshId, this); - } + /// + /// Whether this matches the pattern of a 'stop scaling' event. + /// + /// The controller event arguments. + /// True if this is a scale event, false otherwise. + private bool IsStopScalingEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.UP + && scaleType != ScaleType.NONE; } - } - DestroyImmediate(heldMeshes); - peltzerController.ShowTooltips(); + private static bool IsToggleCopyEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN && args.TouchpadLocation == TouchpadLocation.LEFT + && !PeltzerMain.Instance.Zoomer.Zooming; + } - selector.DeselectAll(); - currentMoveType = MoveType.MOVE; - } + // Touchpad Hover Tests + private static bool IsSetUpHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.TOP; + } - /// - /// Claim responsibility for rendering a mesh from this class. - /// This should only be called by Model, as otherwise Model's knowledge of current ownership will be incorrect. - /// - public int ClaimMesh(int meshId, IMeshRenderOwner fosterRenderer) { - for (int i = 0; i < heldMeshes.heldMeshes.Count; i++) { - if (heldMeshes.heldMeshes[i].Mesh.id == meshId) { - if (heldMeshes.heldMeshes[i].Preview != null) { - DestroyImmediate(heldMeshes.heldMeshes[i].Preview); - } - heldMeshes.heldMeshes.RemoveAt(i); - return meshId; + private static bool IsSetDownHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.BOTTOM; } - } - return -1; - } - private void FlipMeshes() { - if (heldMeshes != null && heldMeshes.IsFilling) { - return; - } - - List meshesToFlip = GetMeshesToFlipOrScale(); - List originallySelectedMeshIds = new List(selector.selectedMeshes); - - if (meshesToFlip.Count() == 0) - return; - - // Now compute the flipped the meshes. This doesn't alter the model, it just returns a new set of - // meshes that represents the result of the flipping. - List flippedMeshes; - - if (!Flipper.FlipMeshes(meshesToFlip, PeltzerMain.Instance.peltzerController.LastRotationModel, out flippedMeshes)) { - return; - } - - if (heldMeshes != null) { - // If we are currently in the middle of a move/create operation, then we don't update the model. - // Instead, we just update the previews and continue the grab operation. The model will be updated later when - // the grab ends. - if (currentMoveType == MoveType.MOVE) { - heldMeshes.DestroyPreviews(); - heldMeshes.SetupWithNoCloneOrCache(flippedMeshes, peltzerController.LastPositionModel, peltzerController, - worldSpace); - } else { - heldMeshes.DestroyPreviews(); - heldMeshes.SetupWithNoCloneOrCache(flippedMeshes, peltzerController.LastPositionModel, peltzerController, worldSpace); + private static bool IsSetLeftHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.LEFT; } - // Keep track of the fact that meshes were modified during the move (so we know to replace them when the - // move is complete). - modifiedAnyMeshesDuringMove = true; - } else { - // Not grabbing, so we can update the model directly, replacing the original meshes with their corrresponding - // flipped mesh. - List commands = new List(); - foreach (MMesh mesh in flippedMeshes) { - // Claim to stop any other tool from previewing - model.ClaimMesh(mesh.id, this); - commands.Add(new ReplaceMeshCommand(mesh.id, mesh)); - model.RelinquishMesh(mesh.id, this); + private static bool IsSetRightHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.RIGHT; } - model.ApplyCommand(new CompositeCommand(commands)); - // Restore the original selection, if there was one. - foreach (MMesh mesh in meshesToFlip) { - if (originallySelectedMeshIds.Contains(mesh.id)) { - selector.SelectMesh(mesh.id); - } + private static bool IsUnsetAllHoverTooltipsEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.NONE; } - } + /// + /// Called when the controller mode changes to allow for any setup that may be necessary. + /// + /// The current mode of the controller. + private void ModeChangeEventHandler(ControllerMode oldMode, ControllerMode newMode) + { + if (oldMode == ControllerMode.move) + { + ClearState(); + } + + if (newMode == ControllerMode.move && completedSnaps < SNAP_KNOW_HOW_COUNT) + { + PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); + } + } - // Make some noise. - // TODO(bug): replace with flip sound. - audioLibrary.PlayClip(audioLibrary.groupSound); - peltzerController.TriggerHapticFeedback(); - } + private void ShapeChangedEventHandler(int newMenuItemId) + { + ClearState(); + } - private IEnumerable GetSelectedGrabbedOrHoveredMeshes() { - if (heldMeshes != null) { - return heldMeshes.heldMeshes.Select(hm => hm.Mesh.id); - } else if (selector.selectedMeshes.Count > 0) { - return selector.selectedMeshes; - } else { - List meshList = new List(); - return selector.hoverMeshes; - } - } + public void InvalidateCachedMaterial() + { + defaultGrabMaterial = null; + } - // Performs the correct action for the "group" button. The action is decided - // contextually depending on the selection. - private void PerformGroupButtonAction() { - IEnumerable meshes = GetSelectedGrabbedOrHoveredMeshes(); - - // Figure out what the button should do. - GroupButtonAction action = GetGroupButtonAction(meshes); - - SetMeshGroupsCommand command; - string operationName; - AudioClip audioClip; - - switch (action) { - case GroupButtonAction.GROUP: - // Group the meshes together. - operationName = "groupMeshes"; - command = SetMeshGroupsCommand.CreateGroupMeshesCommand(model, meshes); - audioClip = audioLibrary.groupSound; - userHasPerformedGroupAction = true; - break; - case GroupButtonAction.UNGROUP: - // Ungroup the meshes. - operationName = "ungroupMeshes"; - command = SetMeshGroupsCommand.CreateUngroupMeshesCommand(model, meshes); - audioClip = audioLibrary.ungroupSound; - break; - default: - // Nothing to do. - return; - } - - // Apply the command and log to Google Analytics. - model.ApplyCommand(command); - - if (heldMeshes != null) { - // Since the held meshes are clones of the model's meshes, we need to update the group IDs of - // the held meshes to reflect the changes in the model. - foreach (HeldMeshes.HeldMesh heldMesh in heldMeshes.heldMeshes) { - heldMesh.Mesh.groupId = model.GetMesh(heldMesh.Mesh.id).groupId; + // Start a move or clone operation if any meshes were hovered or selected when the trigger went down. + private bool MaybeStartMoveOrClone() + { + if (currentMoveType != MoveType.CREATE) + { + // There might be some selected/hovered meshes, if so, set up a move/clone, else do nothing. + IEnumerable meshes = selector.SelectedOrHoveredMeshes(); + if (meshes.Count() > 0) + { + IEnumerable selectedMeshes = meshes.Select(m => model.GetMesh(m)); + StartMove(selectedMeshes); + return true; + } + } + return false; } - } else { - // Unselect everything (because it's unintuitive/inconvenient for things to remain selected after - // grouping or ungrouping). - selector.DeselectAll(); - } - - // Make some noise. - audioLibrary.PlayClip(audioClip); - peltzerController.TriggerHapticFeedback(); - } - /// - /// Returns the action that the "group" button should perform, based on which meshes are - /// selected. - /// - /// The list of meshes currently selected or grabbed. - /// - /// The action that the "group" button should perform. - private GroupButtonAction GetGroupButtonAction(IEnumerable selectedOrGrabbedMeshes) { - // If there are fewer than 2 meshes selected, there is no group action. - if (selectedOrGrabbedMeshes.Count() < 2) { - return GroupButtonAction.NONE; - } - // If all the grabbed meshes belong to the same group, the relevant action is "ungroup". - if (model.AreMeshesInSameGroup(selectedOrGrabbedMeshes)) { - return GroupButtonAction.UNGROUP; - } - // Meshes are in different groups or are ungrouped. So the action is "group". - return GroupButtonAction.GROUP; - } + /// + /// Updates the grab/copy tooltip + /// + public void UpdateTooltip() + { + peltzerController.controllerGeometry.grabTooltips.SetActive(peltzerController.mode == ControllerMode.move); + + // Set the grab hand to be green wireframe or regular, depending if we're in clone mode or not. + if (copyToolheadSurface != null) + { + if (defaultGrabMaterial == null) + { + defaultGrabMaterial = copyToolheadSurface.GetComponent().material; + } + if (PeltzerMain.Instance.restrictionManager.IsControllerModeAllowed(ControllerMode.move)) + { + if (currentMoveType == MoveType.CLONE) + { + copyToolheadSurface.GetComponent().material = + MaterialRegistry.GetMaterialAndColorById(MaterialRegistry.GREEN_WIREFRAME_ID).material; + } + else + { + copyToolheadSurface.GetComponent().material = defaultGrabMaterial; + } + } + } + else + { + copyToolheadSurface = peltzerController.attachedToolHead.transform.Find( + "grabTool_Geo/GrabHandFBX3/Hand_object").gameObject; + } + + // Find the current tool state. + bool inProgress = IsMoving(); + bool hoveringOrSelected = + selector.selectedMeshes.Count > 0 || selector.hoverMeshes.Count > 0; + + if (!PeltzerMain.Instance.restrictionManager.touchpadHighlightingAllowed) + { + Overlay moveOverlay = peltzerController.controllerGeometry.moveOverlay.GetComponent(); + + // Set resize & flip active if we're hovering, selected, or in progress. + moveOverlay.upIcon.color = inProgress || hoveringOrSelected ? ENABLED_COLOR : DISABLED_COLOR; + moveOverlay.downIcon.color = inProgress || hoveringOrSelected ? ENABLED_COLOR : DISABLED_COLOR; + + // flip + moveOverlay.rightIcon.color = inProgress || hoveringOrSelected ? ENABLED_COLOR : DISABLED_COLOR; + + // Set copy active if we're hovering, selected, or neither of those but are in copy-mode. + moveOverlay.leftIcon.color = + hoveringOrSelected || (!hoveringOrSelected && currentMoveType == MoveType.CLONE) ? + ENABLED_COLOR : DISABLED_COLOR; + } + + // Figure out what the "group" button should do and update UI to reflect the action. + GroupButtonAction groupButtonAction = GetGroupButtonAction(GetSelectedGrabbedOrHoveredMeshes()); + + if (groupButtonAction != GroupButtonAction.NONE) + { + peltzerController.SetApplicationButtonOverlay(ButtonMode.ACTIVE); + } + else + { + peltzerController.SetApplicationButtonOverlay(ButtonMode.WAITING); + } + + Overlay overlay = peltzerController.controllerGeometry.moveOverlay.GetComponent(); + // We want the group icon on if the user can group or when the button is waiting to show the user + // what the button would do. + overlay.onIcon.gameObject.SetActive(groupButtonAction != GroupButtonAction.UNGROUP); + overlay.offIcon.gameObject.SetActive(groupButtonAction == GroupButtonAction.UNGROUP); + + GameObject groupTooltip = peltzerController.handedness == Handedness.RIGHT ? + peltzerController.controllerGeometry.groupLeftTooltip : peltzerController.controllerGeometry.groupRightTooltip; + GameObject ungroupTooltip = peltzerController.handedness == Handedness.RIGHT ? + peltzerController.controllerGeometry.ungroupLeftTooltip : peltzerController.controllerGeometry.ungroupRightTooltip; + + if (groupButtonAction == GroupButtonAction.GROUP + && groupTooltipShownCount < SHOW_GROUP_TOOLTIP_COUNT + && !PeltzerMain.Instance.tutorialManager.TutorialOccurring() + && !PeltzerMain.Instance.HasDisabledTooltips) + { + peltzerController.controllerGeometry.groupTooltipRoot.SetActive(true); + groupTooltip.SetActive(true); + ungroupTooltip.SetActive(false); + } + else if (groupButtonAction == GroupButtonAction.UNGROUP + && ungroupTooltipShownCount < SHOW_GROUP_TOOLTIP_COUNT + && !PeltzerMain.Instance.tutorialManager.TutorialOccurring() + && !PeltzerMain.Instance.HasDisabledTooltips) + { + peltzerController.controllerGeometry.groupTooltipRoot.SetActive(true); + ungroupTooltip.SetActive(true); + groupTooltip.SetActive(false); + } + else + { + peltzerController.controllerGeometry.groupTooltipRoot.SetActive(false); + // Only increment the 'times shown' count when deselected, or else may count as separate times if the tooltip + // that was still active was "set" again. + if (groupTooltip.activeSelf) + { + groupTooltip.SetActive(false); + groupTooltipShownCount++; + } + else if (ungroupTooltip.activeSelf) + { + ungroupTooltip.SetActive(false); + ungroupTooltipShownCount++; + } + } + + // Set the state of the Grab Toolhead tooltip which happens to be the first child - index 0. + // We wish to show this tooltip if: + // - There are at least two meshes in the scene. + // - The user has not multi-selected in this session. + if (Features.showMultiselectTooltip) + { + userHasMultiSelected |= PeltzerMain.Instance.GetSelector().selectedMeshes.Count > 1; + bool showTooltip = PeltzerMain.Instance.peltzerController.mode == ControllerMode.move + && !userHasMultiSelected + && !PeltzerMain.Instance.HasDisabledTooltips + && PeltzerMain.Instance.model.GetNumberOfMeshes() > 1; + peltzerController.attachedToolHead.transform.GetChild(0).gameObject.SetActive(showTooltip); + } + } - /// - /// Calculates the scaling factor by which the meshes should be scaled in order to make them one grid unit - /// bigger or smaller. This helps ensure that meshes line up with the grid when scaled up/down. - /// - /// The meshes to be scaled. - /// If true, scale up (increase size). If false, scale down. - /// The number of steps: 2 steps means scale twice as much as 1 step. - /// Out param that indicates the scale factor to use. Only defined if this method - /// returns true. If the method returns false, the result is undefined. - /// True if the scaling factor was calculated successfully, false on failure. - private bool CalculateGridFriendlyScaleFactor(IEnumerable meshes, bool scaleUp, int numSteps, - out float result) { - result = 1.0f; - - // Figure out the bounding box of all the meshes. - Bounds? boundsOfAll = null; - foreach (MMesh mesh in meshes) { - if (null != boundsOfAll) { - boundsOfAll.Value.Encapsulate(mesh.bounds); - } else { - boundsOfAll = mesh.bounds; + /// + /// Makes only the supplied tooltip visible and ensures the others are off. + /// + /// The tooltip text to activate. + /// The hover state. + private void SetHoverTooltip(GameObject tooltip, TouchpadHoverState state) + { + if (IsMoving() || selector.selectedMeshes.Count > 0 || selector.hoverMeshes.Count > 0) + { + if (!tooltip.activeSelf) + { + UnsetAllHoverTooltips(); + tooltip.SetActive(true); + peltzerController.SetTouchpadHoverTexture(state); + peltzerController.TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_1, + 0.003f, + 0.15f + ); + } + } + else + { + UnsetAllHoverTooltips(); + } } - } - - // If there were no meshes, give up. - if (null == boundsOfAll) { - return false; - } - - if (IsLongTermScale()) { - numSteps += LONG_TERM_SCALE_FACTOR; - } - - // The logic we use here is: we take the bounding box of the selected meshes and figure out how large that - // box is in grid units. Then we compute the scale factor such that this box will increase/decrease in size - // by 1 grid unit (or, more precisely, such that the longest edge of the bounding box will increase/decrease - // by 1 grid unit). So, for example, if the longest edge currently measures 4 grid units and we are scaling - // up, then we want it to be 5 grid units long, so the scale factor should be 1.25 (since 4 * 1.25 = 5). - - // How many grid units does the largest side measure? - float largestSide = Mathf.Max(boundsOfAll.Value.size.x, boundsOfAll.Value.size.y, boundsOfAll.Value.size.z); - int gridUnits = Mathf.RoundToInt(largestSide / GridUtils.GRID_SIZE); - - if (largestSide < 0.001f) { - // We can't operate on meshes that have zero or negligible size. - // The result would be undefined. - return false; - } - - // Add some min/max checks: - if (!scaleUp) { - if (gridUnits <= 2) { - // Do not scale down further. - return false; + + /// + /// Unset all of the touchpad hover text tooltips. + /// + private void UnsetAllHoverTooltips() + { + peltzerController.controllerGeometry.moverTooltipUp.SetActive(false); + peltzerController.controllerGeometry.moverTooltipDown.SetActive(false); + peltzerController.controllerGeometry.moverTooltipLeft.SetActive(false); + peltzerController.controllerGeometry.moverTooltipRight.SetActive(false); + peltzerController.SetTouchpadHoverTexture(TouchpadHoverState.NONE); } - if (gridUnits - numSteps < 2) { - // Enforce a minimum size. - gridUnits = 2; - } else { - gridUnits -= numSteps; + + /// + /// Add a shatter effect to thrown objects. + /// + /// The GameObject to shatter. + /// The magnitude of the GameObject's meshes bounds size. + private void ShatterObject(GameObject objectToDelete, float scaleFactor) + { + ParticleSystem shatterEffect = Instantiate(shatterPrefab); + shatterEffect.transform.position = objectToDelete.transform.position; + + ParticleSystem.MainModule mainModule = shatterEffect.main; + mainModule.startSize = scaleFactor * SHATTER_SCALE; + + float step = Mathf.Clamp(Mathf.SmoothStep(SHATTER_STEP_MIN, SHATTER_STEP_MAX, scaleFactor), 0f, 1f); + float pitch = Mathf.Lerp(SHATTER_PITCH_MAX, SHATTER_PITCH_MIN, step); + audioLibrary.PlayClip(audioLibrary.breakSound, pitch); + + int materialId = objectToDelete.GetComponent().meshes[0].materialAndColor.matId; + shatterEffect.GetComponent().material = + MaterialRegistry.GetMaterialWithAlbedoById(materialId); } - } else { - if (gridUnits + numSteps > MAX_MESH_SIZE_IN_GRID_UNITS) { - gridUnits = MAX_MESH_SIZE_IN_GRID_UNITS; - } else { - gridUnits += numSteps; + + /// + /// Each frame, if a mesh is currently held, update its position in world-space relative + /// to its original position, and the delta between the controller's position at world-start + /// and the controller's current position. + /// + private void Update() + { + // We need to clean up any 'thrown away' objects when their time comes (even if the user has changed mode). + while (objectsToDelete.Count > 0 && (objectsToDelete.Peek().Key.transform.position.y <= 0.125f)) + { + KeyValuePair pairToDelete = objectsToDelete.Dequeue(); + + ShatterObject(pairToDelete.Key, pairToDelete.Value); + DestroyImmediate(pairToDelete.Key); + } + + if (!PeltzerController.AcquireIfNecessary(ref peltzerController) + || peltzerController.mode != ControllerMode.move) + { + return; + } + + UpdateTooltip(); + + // If we have not grabbed any meshes let the selector find meshes to grab. + if (heldMeshes == null) + { + selector.SelectMeshAtPosition(peltzerController.LastPositionModel, Selector.MESHES_ONLY); + } + else + { + // If a move is in progress, and the trigger has been down for longer than WAIT_THRESHOLD, then this is + // a hold-trigger-and-drag operation which can be completed by raising the trigger. + if (waitingToDetermineReleaseType && Time.time - triggerDownTime > PeltzerController.SINGLE_CLICK_THRESHOLD) + { + waitingToDetermineReleaseType = false; + triggerUpToRelease = true; + } + + heldMeshes.UpdatePositions(); + } + + peltzerController.heldMeshes = heldMeshes; } - } - // Now we want to compute the scale factor such that the largest size is that many grid units. - float desiredLargestSide = GridUtils.GRID_SIZE * gridUnits; - result = desiredLargestSide / largestSide; - return true; - } + public void StartMove(IEnumerable selectedMeshes) + { + if (!PeltzerMain.Instance.restrictionManager.movingMeshesAllowed) + { + return; + } + + selector.EndMultiSelection(); + + // Hide the overlay. + peltzerController.HideTooltips(); + + // A move is being started, inform the user. + peltzerController.TriggerHapticFeedback(); + audioLibrary.PlayClip(currentMoveType == MoveType.MOVE ? + audioLibrary.grabMeshSound : audioLibrary.copySound); + + // We'll generate a preview and a copy of the original mesh, for each selected mesh. + heldMeshes = gameObject.AddComponent(); + heldMeshes.Setup(selectedMeshes, peltzerController.LastPositionModel, peltzerController.LastRotationModel, + peltzerController, worldSpace, meshRepresentationCache); + + // If we are copying, then we unhide the meshes now, as the original meshes will not be affected. + // If we are moving, we don't bother unhiding the original meshes, as Mover will be using previews. + if (currentMoveType == MoveType.MOVE) + { + foreach (HeldMeshes.HeldMesh heldMesh in heldMeshes.heldMeshes) + { + model.ClaimMesh(heldMesh.Mesh.id, this); + } + } + else + { + selector.DeselectAll(); + } + } - /// - /// Retrieve a list of meshes to flip or scale. Returns grabbed, selected, or hovered meshes. - /// - /// - private List GetMeshesToFlipOrScale() { - List meshes; - List originallySelectedMeshIds = new List(selector.selectedMeshes); - if (heldMeshes != null) { - // Get the meshes that are being held. - Dictionary heldMeshesAndPreviews = heldMeshes.GetHeldMeshesAndPreviews(); - meshes = new List(heldMeshesAndPreviews.Count()); - foreach (KeyValuePair pair in heldMeshesAndPreviews) { - // Until now, the GameObject has held the state of the mesh's offset and rotation. Update it here - // such that we can pass the right information to Flipper/Scaler. - MMesh mesh = pair.Key; - MeshWithMaterialRenderer renderer = pair.Value.GetComponent(); - mesh.offset = renderer.GetPositionInModelSpace(); - mesh.rotation = renderer.GetOrientationInModelSpace(); - meshes.Add(mesh); + public bool IsMoving() + { + return heldMeshes != null; } - } else { - // Get the selected or hovered meshes. - IEnumerable meshIds = selector.SelectedOrHoveredMeshes(); - meshes = new List(meshIds.Count()); - foreach (int meshId in meshIds) { - MMesh mesh = model.GetMesh(meshId); - meshes.Add(mesh); + + /// + /// Throw an object away and mark it for deletion. + /// + /// The object to delete. + /// The velocity with which to release it. + /// The magnitude of the mesh's bounds, used to scale the shatter size. + private void ThrowObjectAway(GameObject unlovedObject, Vector3 velocity, float meshMagnitude) + { + MeshWithMaterialRenderer mwmRenderer = unlovedObject.GetComponent(); + unlovedObject.transform.position = mwmRenderer.GetPositionInWorldSpace(); + mwmRenderer.UseGameObjectPosition = true; + + // Apply the force. + Rigidbody rigidbody = unlovedObject.GetComponent(); + if (rigidbody == null) + { + rigidbody = unlovedObject.AddComponent(); + } + rigidbody.isKinematic = false; + rigidbody.AddForce(velocity, ForceMode.VelocityChange); + + // Schedule for deletion. + objectsToDelete.Enqueue(new KeyValuePair(unlovedObject, meshMagnitude)); } - } - return meshes; - } + /// + /// Returns whether or not the user just did a "throw" gesture with the controller, thus indicating their disdain + /// for the currently held meshes and a corresponding desire to be rid of them. + /// + /// True if and only if the user just did a throw gesture. + private bool IsThrowing() + { + return peltzerController.GetVelocity().magnitude > THROWING_VELOCITY_THRESHOLD + && PeltzerMain.Instance.restrictionManager.throwAwayAllowed; + } - /// - /// Scales all held or hovered meshes by expanding or contracting their vertices relative to the center of the - /// mesh. - /// - private void ScaleMeshes(bool scaleUp, int numSteps) { - if (heldMeshes != null && heldMeshes.IsFilling) { - return; - } + /// + /// Throws away the currently grabbed meshes, applying a force to them and scheduling them for deletion. + /// + private void ThrowGrabbedMeshes(out List commands) + { + commands = new List(heldMeshes.heldMeshes.Count); + foreach (HeldMeshes.HeldMesh heldMesh in heldMeshes.heldMeshes) + { + ThrowObjectAway(heldMesh.Preview, peltzerController.GetVelocity(), heldMesh.Mesh.bounds.size.magnitude); + if (currentMoveType == MoveType.MOVE) + { + commands.Add(new DeleteMeshCommand(heldMesh.Mesh.id)); + model.RelinquishMesh(heldMesh.Mesh.id, this); + } + } + heldMeshes.heldMeshes.Clear(); + } - if ((scaleUp && !PeltzerMain.Instance.restrictionManager.touchpadUpAllowed) - || (!scaleUp && !PeltzerMain.Instance.restrictionManager.touchpadDownAllowed)) { - return; - } + /// + /// Moves the currently grabbed meshes. + /// + /// Out param. Returns the list of model commands that resulted from the move. + /// True if there were any invalid meshes, false if all meshes were valid. + private bool MoveGrabbedMeshes(out List commands) + { + bool anyInvalidMeshes = false; + commands = new List(heldMeshes.heldMeshes.Count); + + foreach (HeldMeshes.HeldMesh heldMesh in heldMeshes.heldMeshes) + { + MMesh mesh = heldMesh.Mesh; + GameObject preview = heldMesh.Preview; + + if (modifiedAnyMeshesDuringMove) + { + + MeshWithMaterialRenderer meshRenderer = preview.GetComponent(); + // If we modified any meshes, we delete and re-add any affected meshes, but first, we need to update their + // position/rotation to match the preview. + mesh.offset = meshRenderer.GetPositionInModelSpace(); + mesh.rotation = meshRenderer.GetOrientationInModelSpace(); + if (model.CanAddMesh(mesh)) + { + commands.Add(new ReplaceMeshCommand(mesh.id, mesh)); + } + else + { + anyInvalidMeshes = true; + } + } + else + { + // If we didn't modify any meshes, we issue a command to move any affected meshes, by their respective + // positional and rotational deltas. + MMesh originalMesh = model.GetMesh(mesh.id); + MeshWithMaterialRenderer meshRenderer = preview.GetComponent(); + Quaternion rotationDelta = Quaternion.Inverse(originalMesh.rotation) + * meshRenderer.GetOrientationInModelSpace(); + Vector3 positionDelta = meshRenderer.GetPositionInModelSpace() - originalMesh.offset; + if (model.CanMoveMesh(originalMesh, positionDelta, rotationDelta)) + { + commands.Add(new MoveMeshCommand(originalMesh.id, positionDelta, rotationDelta)); + } + else + { + anyInvalidMeshes = true; + } + } + model.RelinquishMesh(mesh.id, this); + } + + DestroyImmediate(heldMeshes); + + return anyInvalidMeshes; + } - scaleType = scaleUp ? ScaleType.SCALE_UP : ScaleType.SCALE_DOWN; + /// + /// Creates (inserts into the model for the first time) the currently grabbed meshes. + /// + /// Out param. Returns the list of model commands that resulted from the create. + /// True if there were any invalid meshes, false if all meshes were valid. + private bool CreateGrabbedMeshes(out List commands) + { + bool anyInvalidMeshes = false; + commands = new List(heldMeshes.heldMeshes.Count); + + foreach (HeldMeshes.HeldMesh heldMesh in heldMeshes.heldMeshes) + { + MMesh mesh = heldMesh.Mesh; + GameObject preview = heldMesh.Preview; + + MeshWithMaterialRenderer renderer = preview.GetComponent(); + mesh.offset = renderer.GetPositionInModelSpace(); + mesh.rotation = renderer.GetOrientationInModelSpace(); + if (model.CanAddMesh(mesh)) + { + commands.Add(new AddMeshCommand(mesh)); + } + else + { + anyInvalidMeshes = true; + } + DestroyImmediate(preview); + } + + + return anyInvalidMeshes; + } - List meshesToScale = GetMeshesToFlipOrScale(); - List originallySelectedMeshIds = new List(selector.selectedMeshes); + /// + /// Copies the currently grabbed meshes. + /// + /// Out param. Returns the list of model commands that resulted from the copy. + /// True if there were any invalid meshes, false if all meshes were valid. + private bool CopyGrabbedMeshes(out List commands) + { + bool anyInvalidMeshes = false; + commands = new List(heldMeshes.heldMeshes.Count); + + // To clone groups correctly, we must generate a new group ID for each group ID in the meshes to copy, and use + // the new group IDs for the copy. That way, a copy of a group will be a new group. This dictionary stores a + // mapping from old group ID to new group ID. We populate it lazily, generating a new group ID for each + // old group ID that we find. + Dictionary groupMapping = new Dictionary(); + + // GROUP_NONE just maps to GROUP_NONE. + groupMapping[MMesh.GROUP_NONE] = MMesh.GROUP_NONE; + + foreach (HeldMeshes.HeldMesh heldMesh in heldMeshes.heldMeshes) + { + MMesh mesh = heldMesh.Mesh; + GameObject preview = heldMesh.Preview; + + // Generate a new group ID if we haven't seen this group before. + if (mesh.groupId != MMesh.GROUP_NONE && !groupMapping.ContainsKey(mesh.groupId)) + { + groupMapping[mesh.groupId] = model.GenerateGroupId(); + } + + MMesh copy = mesh.CloneWithNewIdAndGroup(model.GenerateMeshId(), groupMapping[mesh.groupId]); + MeshWithMaterialRenderer renderer = preview.GetComponent(); + copy.offset = renderer.GetPositionInModelSpace(); + copy.rotation = renderer.GetOrientationInModelSpace(); + if (model.CanAddMesh(copy)) + { + commands.Add(new CopyMeshCommand(mesh.id, copy)); + } + else + { + anyInvalidMeshes = true; + } + DestroyImmediate(preview); + } + + + return anyInvalidMeshes; + } - float scaleFactor; + private void CompleteMove() + { + // If we weren't moving anything, calling this function is redundant, return. + if (heldMeshes == null) + { + return; + } + bool throwAway = IsThrowing(); + + // Get the list of commands for move/copy and find out if this would be an invalid operation. + List commands = new List(); + bool anyInvalidMeshes = false; + if (throwAway) + { + ThrowGrabbedMeshes(out commands); + } + else if (currentMoveType == MoveType.MOVE) + { + anyInvalidMeshes = MoveGrabbedMeshes(out commands); + } + else if (currentMoveType == MoveType.CREATE) + { + anyInvalidMeshes = CreateGrabbedMeshes(out commands); + } + else + { + anyInvalidMeshes = CopyGrabbedMeshes(out commands); + } + + if (!anyInvalidMeshes) + { + if (commands.Count > 0) + { + model.ApplyCommand(new CompositeCommand(commands)); + } + // TODO(bug): Add audio for throwing away. + audioLibrary.PlayClip(currentMoveType == MoveType.MOVE ? + audioLibrary.releaseMeshSound : audioLibrary.pasteMeshSound); + peltzerController.TriggerHapticFeedback(); + PeltzerMain.Instance.movesCompleted++; + } + else + { + audioLibrary.PlayClip(audioLibrary.errorSound); + peltzerController.TriggerHapticFeedback(); + } + + if (!throwAway) + { + heldMeshes.DestroyPreviews(); + } + + if (currentMoveType == MoveType.CREATE) + { + // Close the details menu after a user imports. + // This is a potentially useful UX action for the user, but more importantly it allows + // us to avoid making a copy of all meshes here, as we're actually grabbing the meshes from the details panel + // to import into the scene. If the user (for some reason) wants to import the same mesh again, they'll have to + // re-open the details panel, thereby re-generating the meshes. + PeltzerMain.Instance.GetPolyMenuMain().SetActiveMenu(menu.PolyMenuMain.Menu.TOOLS_MENU); + } + ClearState(); + } - // Abort if we have no meshes to scale or if we failed to compute the scale factor. - if (meshesToScale.Count == 0 || - !CalculateGridFriendlyScaleFactor(meshesToScale, scaleUp, numSteps, out scaleFactor)) { - return; - } + // Reset everything to a clean, default state. + public void ClearState() + { + isCopyGrabbing = false; + modifiedAnyMeshesDuringMove = false; + waitingToDetermineReleaseType = false; + + if (heldMeshes != null) + { + if (currentMoveType == MoveType.MOVE) + { + foreach (int meshId in heldMeshes.GetMeshIds()) + { + model.RelinquishMesh(meshId, this); + } + } + } + + DestroyImmediate(heldMeshes); + peltzerController.ShowTooltips(); + + selector.DeselectAll(); + currentMoveType = MoveType.MOVE; + } - // Keep track of the fact that this move operation has scaled meshes. - modifiedAnyMeshesDuringMove = true; + /// + /// Claim responsibility for rendering a mesh from this class. + /// This should only be called by Model, as otherwise Model's knowledge of current ownership will be incorrect. + /// + public int ClaimMesh(int meshId, IMeshRenderOwner fosterRenderer) + { + for (int i = 0; i < heldMeshes.heldMeshes.Count; i++) + { + if (heldMeshes.heldMeshes[i].Mesh.id == meshId) + { + if (heldMeshes.heldMeshes[i].Preview != null) + { + DestroyImmediate(heldMeshes.heldMeshes[i].Preview); + } + heldMeshes.heldMeshes.RemoveAt(i); + return meshId; + } + } + return -1; + } - if (heldMeshes != null) { - // Try and scale the meshes as they are. - if (!Scaler.TryScalingMeshes(meshesToScale, scaleFactor)) { - audioLibrary.PlayClip(audioLibrary.errorSound); - return; + private void FlipMeshes() + { + if (heldMeshes != null && heldMeshes.IsFilling) + { + return; + } + + List meshesToFlip = GetMeshesToFlipOrScale(); + List originallySelectedMeshIds = new List(selector.selectedMeshes); + + if (meshesToFlip.Count() == 0) + return; + + // Now compute the flipped the meshes. This doesn't alter the model, it just returns a new set of + // meshes that represents the result of the flipping. + List flippedMeshes; + + if (!Flipper.FlipMeshes(meshesToFlip, PeltzerMain.Instance.peltzerController.LastRotationModel, out flippedMeshes)) + { + return; + } + + if (heldMeshes != null) + { + // If we are currently in the middle of a move/create operation, then we don't update the model. + // Instead, we just update the previews and continue the grab operation. The model will be updated later when + // the grab ends. + if (currentMoveType == MoveType.MOVE) + { + heldMeshes.DestroyPreviews(); + heldMeshes.SetupWithNoCloneOrCache(flippedMeshes, peltzerController.LastPositionModel, peltzerController, + worldSpace); + } + else + { + heldMeshes.DestroyPreviews(); + heldMeshes.SetupWithNoCloneOrCache(flippedMeshes, peltzerController.LastPositionModel, peltzerController, worldSpace); + } + + // Keep track of the fact that meshes were modified during the move (so we know to replace them when the + // move is complete). + modifiedAnyMeshesDuringMove = true; + } + else + { + // Not grabbing, so we can update the model directly, replacing the original meshes with their corrresponding + // flipped mesh. + List commands = new List(); + foreach (MMesh mesh in flippedMeshes) + { + // Claim to stop any other tool from previewing + model.ClaimMesh(mesh.id, this); + commands.Add(new ReplaceMeshCommand(mesh.id, mesh)); + model.RelinquishMesh(mesh.id, this); + } + model.ApplyCommand(new CompositeCommand(commands)); + + // Restore the original selection, if there was one. + foreach (MMesh mesh in meshesToFlip) + { + if (originallySelectedMeshIds.Contains(mesh.id)) + { + selector.SelectMesh(mesh.id); + } + } + } + + + // Make some noise. + // TODO(bug): replace with flip sound. + audioLibrary.PlayClip(audioLibrary.groupSound); + peltzerController.TriggerHapticFeedback(); } - // If we were moving before, we should still be moving now. We take the simplest approach of destroying old - // previews and generating new ones, rather than trying to move and scale the previews in addition to the - // meshes. - if (currentMoveType == MoveType.MOVE) { - heldMeshes.DestroyPreviews(); - heldMeshes.SetupWithNoCloneOrCache(meshesToScale, peltzerController.LastPositionModel, - peltzerController, worldSpace); - foreach (HeldMeshes.HeldMesh heldMesh in heldMeshes.heldMeshes) { - model.ClaimMesh(heldMesh.Mesh.id, this); - } - } else { - heldMeshes.DestroyPreviews(); - heldMeshes.SetupWithNoCloneOrCache(meshesToScale, peltzerController.LastPositionModel, - peltzerController, worldSpace); + private IEnumerable GetSelectedGrabbedOrHoveredMeshes() + { + if (heldMeshes != null) + { + return heldMeshes.heldMeshes.Select(hm => hm.Mesh.id); + } + else if (selector.selectedMeshes.Count > 0) + { + return selector.selectedMeshes; + } + else + { + List meshList = new List(); + return selector.hoverMeshes; + } } - audioLibrary.PlayClip(scaleUp ? audioLibrary.incrementSound : audioLibrary.decrementSound); - } else { - // If we are not currently moving, we'll actually manipulate the model. - - // Try and scale clones of the meshes, abort on error. - List clonedMeshes = new List(meshesToScale.Count); - foreach (MMesh mesh in meshesToScale) { - clonedMeshes.Add(mesh.Clone()); + + // Performs the correct action for the "group" button. The action is decided + // contextually depending on the selection. + private void PerformGroupButtonAction() + { + IEnumerable meshes = GetSelectedGrabbedOrHoveredMeshes(); + + // Figure out what the button should do. + GroupButtonAction action = GetGroupButtonAction(meshes); + + SetMeshGroupsCommand command; + string operationName; + AudioClip audioClip; + + switch (action) + { + case GroupButtonAction.GROUP: + // Group the meshes together. + operationName = "groupMeshes"; + command = SetMeshGroupsCommand.CreateGroupMeshesCommand(model, meshes); + audioClip = audioLibrary.groupSound; + userHasPerformedGroupAction = true; + break; + case GroupButtonAction.UNGROUP: + // Ungroup the meshes. + operationName = "ungroupMeshes"; + command = SetMeshGroupsCommand.CreateUngroupMeshesCommand(model, meshes); + audioClip = audioLibrary.ungroupSound; + break; + default: + // Nothing to do. + return; + } + + // Apply the command and log to Google Analytics. + model.ApplyCommand(command); + + if (heldMeshes != null) + { + // Since the held meshes are clones of the model's meshes, we need to update the group IDs of + // the held meshes to reflect the changes in the model. + foreach (HeldMeshes.HeldMesh heldMesh in heldMeshes.heldMeshes) + { + heldMesh.Mesh.groupId = model.GetMesh(heldMesh.Mesh.id).groupId; + } + } + else + { + // Unselect everything (because it's unintuitive/inconvenient for things to remain selected after + // grouping or ungrouping). + selector.DeselectAll(); + } + + // Make some noise. + audioLibrary.PlayClip(audioClip); + peltzerController.TriggerHapticFeedback(); } - if (!Scaler.TryScalingMeshes(clonedMeshes, scaleFactor)) { - audioLibrary.PlayClip(audioLibrary.errorSound); - return; + /// + /// Returns the action that the "group" button should perform, based on which meshes are + /// selected. + /// + /// The list of meshes currently selected or grabbed. + /// + /// The action that the "group" button should perform. + private GroupButtonAction GetGroupButtonAction(IEnumerable selectedOrGrabbedMeshes) + { + // If there are fewer than 2 meshes selected, there is no group action. + if (selectedOrGrabbedMeshes.Count() < 2) + { + return GroupButtonAction.NONE; + } + // If all the grabbed meshes belong to the same group, the relevant action is "ungroup". + if (model.AreMeshesInSameGroup(selectedOrGrabbedMeshes)) + { + return GroupButtonAction.UNGROUP; + } + // Meshes are in different groups or are ungrouped. So the action is "group". + return GroupButtonAction.GROUP; } - // First check that every operation is valid. - foreach (MMesh mesh in clonedMeshes) { - if (!model.CanAddMesh(mesh)) { - audioLibrary.PlayClip(audioLibrary.errorSound); - return; - } + /// + /// Calculates the scaling factor by which the meshes should be scaled in order to make them one grid unit + /// bigger or smaller. This helps ensure that meshes line up with the grid when scaled up/down. + /// + /// The meshes to be scaled. + /// If true, scale up (increase size). If false, scale down. + /// The number of steps: 2 steps means scale twice as much as 1 step. + /// Out param that indicates the scale factor to use. Only defined if this method + /// returns true. If the method returns false, the result is undefined. + /// True if the scaling factor was calculated successfully, false on failure. + private bool CalculateGridFriendlyScaleFactor(IEnumerable meshes, bool scaleUp, int numSteps, + out float result) + { + result = 1.0f; + + // Figure out the bounding box of all the meshes. + Bounds? boundsOfAll = null; + foreach (MMesh mesh in meshes) + { + if (null != boundsOfAll) + { + boundsOfAll.Value.Encapsulate(mesh.bounds); + } + else + { + boundsOfAll = mesh.bounds; + } + } + + // If there were no meshes, give up. + if (null == boundsOfAll) + { + return false; + } + + if (IsLongTermScale()) + { + numSteps += LONG_TERM_SCALE_FACTOR; + } + + // The logic we use here is: we take the bounding box of the selected meshes and figure out how large that + // box is in grid units. Then we compute the scale factor such that this box will increase/decrease in size + // by 1 grid unit (or, more precisely, such that the longest edge of the bounding box will increase/decrease + // by 1 grid unit). So, for example, if the longest edge currently measures 4 grid units and we are scaling + // up, then we want it to be 5 grid units long, so the scale factor should be 1.25 (since 4 * 1.25 = 5). + + // How many grid units does the largest side measure? + float largestSide = Mathf.Max(boundsOfAll.Value.size.x, boundsOfAll.Value.size.y, boundsOfAll.Value.size.z); + int gridUnits = Mathf.RoundToInt(largestSide / GridUtils.GRID_SIZE); + + if (largestSide < 0.001f) + { + // We can't operate on meshes that have zero or negligible size. + // The result would be undefined. + return false; + } + + // Add some min/max checks: + if (!scaleUp) + { + if (gridUnits <= 2) + { + // Do not scale down further. + return false; + } + if (gridUnits - numSteps < 2) + { + // Enforce a minimum size. + gridUnits = 2; + } + else + { + gridUnits -= numSteps; + } + } + else + { + if (gridUnits + numSteps > MAX_MESH_SIZE_IN_GRID_UNITS) + { + gridUnits = MAX_MESH_SIZE_IN_GRID_UNITS; + } + else + { + gridUnits += numSteps; + } + } + + // Now we want to compute the scale factor such that the largest size is that many grid units. + float desiredLargestSide = GridUtils.GRID_SIZE * gridUnits; + result = desiredLargestSide / largestSide; + return true; } - List commands = new List(); - foreach (MMesh mesh in clonedMeshes) { - // Claim to stop any other tool from previewing - model.ClaimMesh(mesh.id, this); - commands.Add(new ReplaceMeshCommand(mesh.id, mesh)); - model.RelinquishMesh(mesh.id, this); + /// + /// Retrieve a list of meshes to flip or scale. Returns grabbed, selected, or hovered meshes. + /// + /// + private List GetMeshesToFlipOrScale() + { + List meshes; + List originallySelectedMeshIds = new List(selector.selectedMeshes); + if (heldMeshes != null) + { + // Get the meshes that are being held. + Dictionary heldMeshesAndPreviews = heldMeshes.GetHeldMeshesAndPreviews(); + meshes = new List(heldMeshesAndPreviews.Count()); + foreach (KeyValuePair pair in heldMeshesAndPreviews) + { + // Until now, the GameObject has held the state of the mesh's offset and rotation. Update it here + // such that we can pass the right information to Flipper/Scaler. + MMesh mesh = pair.Key; + MeshWithMaterialRenderer renderer = pair.Value.GetComponent(); + mesh.offset = renderer.GetPositionInModelSpace(); + mesh.rotation = renderer.GetOrientationInModelSpace(); + meshes.Add(mesh); + } + } + else + { + // Get the selected or hovered meshes. + IEnumerable meshIds = selector.SelectedOrHoveredMeshes(); + meshes = new List(meshIds.Count()); + foreach (int meshId in meshIds) + { + MMesh mesh = model.GetMesh(meshId); + meshes.Add(mesh); + } + } + + return meshes; } - model.ApplyCommand(new CompositeCommand(commands)); - peltzerController.TriggerHapticFeedback(); - - // Restore the original selection, if there was one. - foreach (MMesh mesh in clonedMeshes) { - if (originallySelectedMeshIds.Contains(mesh.id)) { - selector.SelectMesh(mesh.id); - } + + /// + /// Scales all held or hovered meshes by expanding or contracting their vertices relative to the center of the + /// mesh. + /// + private void ScaleMeshes(bool scaleUp, int numSteps) + { + if (heldMeshes != null && heldMeshes.IsFilling) + { + return; + } + + if ((scaleUp && !PeltzerMain.Instance.restrictionManager.touchpadUpAllowed) + || (!scaleUp && !PeltzerMain.Instance.restrictionManager.touchpadDownAllowed)) + { + return; + } + + scaleType = scaleUp ? ScaleType.SCALE_UP : ScaleType.SCALE_DOWN; + + List meshesToScale = GetMeshesToFlipOrScale(); + List originallySelectedMeshIds = new List(selector.selectedMeshes); + + float scaleFactor; + + // Abort if we have no meshes to scale or if we failed to compute the scale factor. + if (meshesToScale.Count == 0 || + !CalculateGridFriendlyScaleFactor(meshesToScale, scaleUp, numSteps, out scaleFactor)) + { + return; + } + + // Keep track of the fact that this move operation has scaled meshes. + modifiedAnyMeshesDuringMove = true; + + if (heldMeshes != null) + { + // Try and scale the meshes as they are. + if (!Scaler.TryScalingMeshes(meshesToScale, scaleFactor)) + { + audioLibrary.PlayClip(audioLibrary.errorSound); + return; + } + + // If we were moving before, we should still be moving now. We take the simplest approach of destroying old + // previews and generating new ones, rather than trying to move and scale the previews in addition to the + // meshes. + if (currentMoveType == MoveType.MOVE) + { + heldMeshes.DestroyPreviews(); + heldMeshes.SetupWithNoCloneOrCache(meshesToScale, peltzerController.LastPositionModel, + peltzerController, worldSpace); + foreach (HeldMeshes.HeldMesh heldMesh in heldMeshes.heldMeshes) + { + model.ClaimMesh(heldMesh.Mesh.id, this); + } + } + else + { + heldMeshes.DestroyPreviews(); + heldMeshes.SetupWithNoCloneOrCache(meshesToScale, peltzerController.LastPositionModel, + peltzerController, worldSpace); + } + audioLibrary.PlayClip(scaleUp ? audioLibrary.incrementSound : audioLibrary.decrementSound); + } + else + { + // If we are not currently moving, we'll actually manipulate the model. + + // Try and scale clones of the meshes, abort on error. + List clonedMeshes = new List(meshesToScale.Count); + foreach (MMesh mesh in meshesToScale) + { + clonedMeshes.Add(mesh.Clone()); + } + + if (!Scaler.TryScalingMeshes(clonedMeshes, scaleFactor)) + { + audioLibrary.PlayClip(audioLibrary.errorSound); + return; + } + + // First check that every operation is valid. + foreach (MMesh mesh in clonedMeshes) + { + if (!model.CanAddMesh(mesh)) + { + audioLibrary.PlayClip(audioLibrary.errorSound); + return; + } + } + + List commands = new List(); + foreach (MMesh mesh in clonedMeshes) + { + // Claim to stop any other tool from previewing + model.ClaimMesh(mesh.id, this); + commands.Add(new ReplaceMeshCommand(mesh.id, mesh)); + model.RelinquishMesh(mesh.id, this); + } + model.ApplyCommand(new CompositeCommand(commands)); + peltzerController.TriggerHapticFeedback(); + + // Restore the original selection, if there was one. + foreach (MMesh mesh in clonedMeshes) + { + if (originallySelectedMeshIds.Contains(mesh.id)) + { + selector.SelectMesh(mesh.id); + } + } + } } - } - } - /// - /// Stop scaling. - /// - private void StopScaling() { - scaleType = ScaleType.NONE; - continuousScaleEvents = 0; - longTermScaleStartTime = float.MaxValue; - } + /// + /// Stop scaling. + /// + private void StopScaling() + { + scaleType = ScaleType.NONE; + continuousScaleEvents = 0; + longTermScaleStartTime = float.MaxValue; + } - /// - /// Whether scaling has been happening continuously over the threshold set by FAST_SCALE_THRESHOLD. - /// - /// True if this is a long term scale event, false otherwise. - private bool IsLongTermScale() { - return Time.time > longTermScaleStartTime; + /// + /// Whether scaling has been happening continuously over the threshold set by FAST_SCALE_THRESHOLD. + /// + /// True if this is a long term scale event, false otherwise. + private bool IsLongTermScale() + { + return Time.time > longTermScaleStartTime; + } } - } } diff --git a/Assets/Scripts/tools/Painter.cs b/Assets/Scripts/tools/Painter.cs index c5e3d046..8fa4fc4d 100644 --- a/Assets/Scripts/tools/Painter.cs +++ b/Assets/Scripts/tools/Painter.cs @@ -22,335 +22,394 @@ using com.google.apps.peltzer.client.model.render; using com.google.apps.peltzer.client.tools.utils; -namespace com.google.apps.peltzer.client.tools { - /// - /// Tool for painting meshes and faces. - /// - public class Painter : MonoBehaviour { - public ControllerMain controllerMain; +namespace com.google.apps.peltzer.client.tools +{ /// - /// A reference to a controller capable of issuing paint commands. + /// Tool for painting meshes and faces. /// - private PeltzerController peltzerController; - /// - /// A reference to the overall model being built. - /// - private Model model; - /// - /// Selector for detecting which item is hovered. - /// - private Selector selector; - /// - /// Library for playing sounds. - /// - private AudioLibrary audioLibrary; - /// - /// Whether we are currently painting all hovered objects. - /// - private bool isPainting; - /// - /// When we last made a noise and buzzed because of a paint. - /// - private float timeLastPaintFeedbackPlayed; - /// - /// Leave some time between playing paint feedback. - /// - private const float INTERVAL_BETWEEN_PAINT_FEEDBACKS = 0.5f; - /// - /// Already-painted meshes, never paint the same one the same colour twice in the same operation. - /// - private HashSet seenMeshes = new HashSet(); - /// - /// Already-painted meshes and faces, never paint the same one the same colour twice in the same operation. - /// - private Dictionary> seenMeshesAndFaces = new Dictionary>(); - /// - /// A pre-allocated dictionary of properties by face, used to avoid constructor overhead, initialized to a - /// - private Dictionary propsByFace = new Dictionary(MMesh.MAX_FACES); - /// - /// The FaceProperties for any face painted by the current tool. - /// - private FaceProperties paintedFaceProperties; - /// - /// A list of commands to send. 100 commands should be enough for any single update. - /// - private List paintCommands = new List(100); - /// - /// Keep track of material changes. - /// - private int lastMaterial = -1; - /// - /// Whether we have shown the snap tooltip for this tool yet. (Show only once because there are no direct - /// snapping behaviors for Painter and Deleter). - /// - private bool snapTooltipShown = false; - - // All swatches on the colour palette, such that we can play an animation when Painter is first selected. - private ChangeMaterialMenuItem[] allColourSwatches; - - public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, Selector selector, - AudioLibrary audioLibrary) { - this.model = model; - this.controllerMain = controllerMain; - this.peltzerController = peltzerController; - this.selector = selector; - this.audioLibrary = audioLibrary; - - controllerMain.ControllerActionHandler += ControllerEventHandler; - peltzerController.ModeChangedHandler += ModeChangedHandler; - - allColourSwatches = PeltzerMain.Instance.paletteController.transform.GetComponentsInChildren(true); - } - - public void Update() { - if (!PeltzerController.AcquireIfNecessary(ref peltzerController) || - (peltzerController.mode != ControllerMode.paintFace && - peltzerController.mode != ControllerMode.paintMesh)) { - return; - } - - // Update the material if the user switched colors. - if (lastMaterial != peltzerController.currentMaterial) { - seenMeshes.Clear(); - seenMeshesAndFaces.Clear(); - lastMaterial = peltzerController.currentMaterial; - paintedFaceProperties = new FaceProperties(peltzerController.currentMaterial); - } - - // Update the position of the selector. - // Note that we use MESHES_ONLY_IGNORE_GROUPS because, specifically for the Paint tool, we don't want mesh - // groups to be honored (so the user can paint individual meshes in groups without ungrouping). - if (peltzerController.mode == ControllerMode.paintMesh) { - selector.SelectMeshAtPosition(peltzerController.LastPositionModel, Selector.MESHES_ONLY_IGNORE_GROUPS); - } - else { - selector.SelectAtPosition(peltzerController.LastPositionModel, Selector.FACES_ONLY); - } - - if (isPainting) { - if (peltzerController.mode == ControllerMode.paintFace && selector.hoverFace != null) { - PaintSelectedFace(selector.hoverFace); - } - else if (peltzerController.mode == ControllerMode.paintMesh && selector.hoverMeshes.Count > 0) { - PaintSelectedMeshes(); + public class Painter : MonoBehaviour + { + public ControllerMain controllerMain; + /// + /// A reference to a controller capable of issuing paint commands. + /// + private PeltzerController peltzerController; + /// + /// A reference to the overall model being built. + /// + private Model model; + /// + /// Selector for detecting which item is hovered. + /// + private Selector selector; + /// + /// Library for playing sounds. + /// + private AudioLibrary audioLibrary; + /// + /// Whether we are currently painting all hovered objects. + /// + private bool isPainting; + /// + /// When we last made a noise and buzzed because of a paint. + /// + private float timeLastPaintFeedbackPlayed; + /// + /// Leave some time between playing paint feedback. + /// + private const float INTERVAL_BETWEEN_PAINT_FEEDBACKS = 0.5f; + /// + /// Already-painted meshes, never paint the same one the same colour twice in the same operation. + /// + private HashSet seenMeshes = new HashSet(); + /// + /// Already-painted meshes and faces, never paint the same one the same colour twice in the same operation. + /// + private Dictionary> seenMeshesAndFaces = new Dictionary>(); + /// + /// A pre-allocated dictionary of properties by face, used to avoid constructor overhead, initialized to a + /// + private Dictionary propsByFace = new Dictionary(MMesh.MAX_FACES); + /// + /// The FaceProperties for any face painted by the current tool. + /// + private FaceProperties paintedFaceProperties; + /// + /// A list of commands to send. 100 commands should be enough for any single update. + /// + private List paintCommands = new List(100); + /// + /// Keep track of material changes. + /// + private int lastMaterial = -1; + /// + /// Whether we have shown the snap tooltip for this tool yet. (Show only once because there are no direct + /// snapping behaviors for Painter and Deleter). + /// + private bool snapTooltipShown = false; + + // All swatches on the colour palette, such that we can play an animation when Painter is first selected. + private ChangeMaterialMenuItem[] allColourSwatches; + + public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, Selector selector, + AudioLibrary audioLibrary) + { + this.model = model; + this.controllerMain = controllerMain; + this.peltzerController = peltzerController; + this.selector = selector; + this.audioLibrary = audioLibrary; + + controllerMain.ControllerActionHandler += ControllerEventHandler; + peltzerController.ModeChangedHandler += ModeChangedHandler; + + allColourSwatches = PeltzerMain.Instance.paletteController.transform.GetComponentsInChildren(true); } - } else { - if (peltzerController.mode == ControllerMode.paintFace && selector.hoverFace != null) { - PeltzerMain.Instance.highlightUtils.SetFaceStyleToPaint(selector.hoverFace, selector.selectorPosition, - MaterialRegistry.GetMaterialAndColorById(peltzerController.currentMaterial).color); - } else if (peltzerController.mode == ControllerMode.paintMesh && selector.hoverMeshes != null) { - foreach (int meshId in selector.hoverMeshes) { - PeltzerMain.Instance.highlightUtils.SetMeshStyleToPaint(meshId); - } - } - } - } - - private void ModeChangedHandler(ControllerMode oldMode, ControllerMode newMode) { - if (oldMode == ControllerMode.paintFace || oldMode == ControllerMode.paintMesh) { - UnsetAllHoverTooltips(); - } - if ((newMode == ControllerMode.paintFace && oldMode != ControllerMode.paintMesh) || - (newMode == ControllerMode.paintMesh && oldMode != ControllerMode.paintFace)) { - if (!PeltzerMain.Instance.HasEverChangedColor || PeltzerMain.Instance.tutorialManager.TutorialOccurring()) { - StartFullRipple(); + public void Update() + { + if (!PeltzerController.AcquireIfNecessary(ref peltzerController) || + (peltzerController.mode != ControllerMode.paintFace && + peltzerController.mode != ControllerMode.paintMesh)) + { + return; + } + + // Update the material if the user switched colors. + if (lastMaterial != peltzerController.currentMaterial) + { + seenMeshes.Clear(); + seenMeshesAndFaces.Clear(); + lastMaterial = peltzerController.currentMaterial; + paintedFaceProperties = new FaceProperties(peltzerController.currentMaterial); + } + + // Update the position of the selector. + // Note that we use MESHES_ONLY_IGNORE_GROUPS because, specifically for the Paint tool, we don't want mesh + // groups to be honored (so the user can paint individual meshes in groups without ungrouping). + if (peltzerController.mode == ControllerMode.paintMesh) + { + selector.SelectMeshAtPosition(peltzerController.LastPositionModel, Selector.MESHES_ONLY_IGNORE_GROUPS); + } + else + { + selector.SelectAtPosition(peltzerController.LastPositionModel, Selector.FACES_ONLY); + } + + if (isPainting) + { + if (peltzerController.mode == ControllerMode.paintFace && selector.hoverFace != null) + { + PaintSelectedFace(selector.hoverFace); + } + else if (peltzerController.mode == ControllerMode.paintMesh && selector.hoverMeshes.Count > 0) + { + PaintSelectedMeshes(); + } + } + else + { + if (peltzerController.mode == ControllerMode.paintFace && selector.hoverFace != null) + { + PeltzerMain.Instance.highlightUtils.SetFaceStyleToPaint(selector.hoverFace, selector.selectorPosition, + MaterialRegistry.GetMaterialAndColorById(peltzerController.currentMaterial).color); + } + else if (peltzerController.mode == ControllerMode.paintMesh && selector.hoverMeshes != null) + { + foreach (int meshId in selector.hoverMeshes) + { + PeltzerMain.Instance.highlightUtils.SetMeshStyleToPaint(meshId); + } + } + } } - lastMaterial = peltzerController.currentMaterial; - } - } - - /// - /// Generates the full ripple effect by triggering the ripple for each individual color swatch. - /// - public void StartFullRipple() { - foreach (ChangeMaterialMenuItem changeMaterialMenuItem in allColourSwatches) { - changeMaterialMenuItem.StartRipple(); - } - } - /// - /// Makes only the supplied tooltip visible and ensures the others are off. - /// - /// The tooltip text to activate. - /// The hover state. - private void SetHoverTooltip(GameObject tooltip, TouchpadHoverState state, TouchpadOverlay currentOverlay) { - if (!tooltip.activeSelf) { - UnsetAllHoverTooltips(); - if (currentOverlay != TouchpadOverlay.PAINT) { - return; + private void ModeChangedHandler(ControllerMode oldMode, ControllerMode newMode) + { + if (oldMode == ControllerMode.paintFace || oldMode == ControllerMode.paintMesh) + { + UnsetAllHoverTooltips(); + } + + if ((newMode == ControllerMode.paintFace && oldMode != ControllerMode.paintMesh) || + (newMode == ControllerMode.paintMesh && oldMode != ControllerMode.paintFace)) + { + if (!PeltzerMain.Instance.HasEverChangedColor || PeltzerMain.Instance.tutorialManager.TutorialOccurring()) + { + StartFullRipple(); + } + lastMaterial = peltzerController.currentMaterial; + } } - tooltip.SetActive(true); - peltzerController.SetTouchpadHoverTexture(state); - peltzerController.TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_1, - 0.003f, - 0.15f - ); - } - } - - /// - /// Unset all of the touchpad hover text tooltips. - /// - private void UnsetAllHoverTooltips() { - peltzerController.controllerGeometry.paintTooltipLeft.SetActive(false); - peltzerController.controllerGeometry.paintTooltipRight.SetActive(false); - peltzerController.SetTouchpadHoverTexture(TouchpadHoverState.NONE); - } - public void ControllerEventHandler(object sender, ControllerEventArgs args) { - if (peltzerController.mode != ControllerMode.paintFace && peltzerController.mode != ControllerMode.paintMesh) - return; - - if (IsStartPaintingEvent(args)) { - StartPainting(); - } else if (IsFinishPaintingEvent(args)) { - ClearState(); - } else if (IsSetLeftHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed) { - SetHoverTooltip( - peltzerController.controllerGeometry.paintTooltipLeft, TouchpadHoverState.LEFT, args.TouchpadOverlay); - } else if (IsSetRightHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadRightAllowed) { - SetHoverTooltip( - peltzerController.controllerGeometry.paintTooltipRight, TouchpadHoverState.RIGHT, args.TouchpadOverlay); - } else if (IsSetSnapTriggerTooltipEvent(args) && !snapTooltipShown) { - // Show tool tip about the snap trigger. - PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); - snapTooltipShown = true; - } else if (IsUnsetAllHoverTooltipsEvent(args)) { - UnsetAllHoverTooltips(); - } - } + /// + /// Generates the full ripple effect by triggering the ripple for each individual color swatch. + /// + public void StartFullRipple() + { + foreach (ChangeMaterialMenuItem changeMaterialMenuItem in allColourSwatches) + { + changeMaterialMenuItem.StartRipple(); + } + } - /// - /// Whether this matches the pattern of a 'start painting' event. - /// - /// The controller event arguments. - /// True if this is a start painting event, false otherwise. - private bool IsStartPaintingEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN; - } + /// + /// Makes only the supplied tooltip visible and ensures the others are off. + /// + /// The tooltip text to activate. + /// The hover state. + private void SetHoverTooltip(GameObject tooltip, TouchpadHoverState state, TouchpadOverlay currentOverlay) + { + if (!tooltip.activeSelf) + { + UnsetAllHoverTooltips(); + if (currentOverlay != TouchpadOverlay.PAINT) + { + return; + } + tooltip.SetActive(true); + peltzerController.SetTouchpadHoverTexture(state); + peltzerController.TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_1, + 0.003f, + 0.15f + ); + } + } - /// - /// Whether this matches the pattern of a 'stop painting' event. - /// - /// The controller event arguments. - /// True if this is a stop painting event, false otherwise. - private bool IsFinishPaintingEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.UP; - } + /// + /// Unset all of the touchpad hover text tooltips. + /// + private void UnsetAllHoverTooltips() + { + peltzerController.controllerGeometry.paintTooltipLeft.SetActive(false); + peltzerController.controllerGeometry.paintTooltipRight.SetActive(false); + peltzerController.SetTouchpadHoverTexture(TouchpadHoverState.NONE); + } - public bool IsPainting() { - return isPainting; - } + public void ControllerEventHandler(object sender, ControllerEventArgs args) + { + if (peltzerController.mode != ControllerMode.paintFace && peltzerController.mode != ControllerMode.paintMesh) + return; + + if (IsStartPaintingEvent(args)) + { + StartPainting(); + } + else if (IsFinishPaintingEvent(args)) + { + ClearState(); + } + else if (IsSetLeftHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed) + { + SetHoverTooltip( + peltzerController.controllerGeometry.paintTooltipLeft, TouchpadHoverState.LEFT, args.TouchpadOverlay); + } + else if (IsSetRightHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadRightAllowed) + { + SetHoverTooltip( + peltzerController.controllerGeometry.paintTooltipRight, TouchpadHoverState.RIGHT, args.TouchpadOverlay); + } + else if (IsSetSnapTriggerTooltipEvent(args) && !snapTooltipShown) + { + // Show tool tip about the snap trigger. + PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); + snapTooltipShown = true; + } + else if (IsUnsetAllHoverTooltipsEvent(args)) + { + UnsetAllHoverTooltips(); + } + } - private static bool IsSetSnapTriggerTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.LIGHT_DOWN; - } + /// + /// Whether this matches the pattern of a 'start painting' event. + /// + /// The controller event arguments. + /// True if this is a start painting event, false otherwise. + private bool IsStartPaintingEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN; + } - private static bool IsSetDownHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.BOTTOM; - } + /// + /// Whether this matches the pattern of a 'stop painting' event. + /// + /// The controller event arguments. + /// True if this is a stop painting event, false otherwise. + private bool IsFinishPaintingEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.UP; + } - private static bool IsSetLeftHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.LEFT; - } + public bool IsPainting() + { + return isPainting; + } - private static bool IsSetRightHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.RIGHT; - } + private static bool IsSetSnapTriggerTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.LIGHT_DOWN; + } - private static bool IsUnsetAllHoverTooltipsEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.NONE; - } + private static bool IsSetDownHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.BOTTOM; + } - private void StartPainting() { - // Just create faceProperties once: technically this means its shared between all faces that - // get painted but given that it's immutable, we're safe. - paintedFaceProperties = new FaceProperties(peltzerController.currentMaterial); + private static bool IsSetLeftHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.LEFT; + } - isPainting = true; - } + private static bool IsSetRightHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.RIGHT; + } - public void ClearState() { - selector.DeselectAll(); + private static bool IsUnsetAllHoverTooltipsEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.NONE; + } - // Forget the list of 'already-painted' faces. - seenMeshes.Clear(); - seenMeshesAndFaces.Clear(); + private void StartPainting() + { + // Just create faceProperties once: technically this means its shared between all faces that + // get painted but given that it's immutable, we're safe. + paintedFaceProperties = new FaceProperties(peltzerController.currentMaterial); - isPainting = false; - } + isPainting = true; + } - // Test method. - public void TriggerUpdateForTest() { - Update(); - } + public void ClearState() + { + selector.DeselectAll(); - private void PaintSelectedMeshes() { - // Get all the hovered meshes. - IEnumerable hoveredMeshes = selector.hoverMeshes; + // Forget the list of 'already-painted' faces. + seenMeshes.Clear(); + seenMeshesAndFaces.Clear(); - // Set the same face property for each face. - foreach (int meshId in hoveredMeshes) { - // Never paint the same mesh the same colour twice in one operation. - if (seenMeshes.Add(meshId)) { - paintCommands.Add(new ChangeFacePropertiesCommand(meshId, paintedFaceProperties)); + isPainting = false; } - } - if (paintCommands.Count > 0) { - Command compositeCommand = new CompositeCommand(paintCommands); - model.ApplyCommand(compositeCommand); - paintCommands.Clear(); - PlayFeedback(); + // Test method. + public void TriggerUpdateForTest() + { + Update(); + } - } - } + private void PaintSelectedMeshes() + { + // Get all the hovered meshes. + IEnumerable hoveredMeshes = selector.hoverMeshes; + + // Set the same face property for each face. + foreach (int meshId in hoveredMeshes) + { + // Never paint the same mesh the same colour twice in one operation. + if (seenMeshes.Add(meshId)) + { + paintCommands.Add(new ChangeFacePropertiesCommand(meshId, paintedFaceProperties)); + } + } + + if (paintCommands.Count > 0) + { + Command compositeCommand = new CompositeCommand(paintCommands); + model.ApplyCommand(compositeCommand); + paintCommands.Clear(); + PlayFeedback(); + + } + } - private void PaintSelectedFace(FaceKey faceKey) { - int meshId = faceKey.meshId; - int faceId = faceKey.faceId; - - HashSet seenFaces; - if (!seenMeshesAndFaces.TryGetValue(meshId, out seenFaces)) { - seenFaces = new HashSet(); - seenMeshesAndFaces[meshId] = seenFaces; - } - // Never paint the same face the same colour twice in the same operation. - if (seenFaces.Add(faceId)) { - model.ApplyCommand(new ChangeFacePropertiesCommand(meshId, - new Dictionary() { { faceId, paintedFaceProperties } })); - PlayFeedback(); - - } - } + private void PaintSelectedFace(FaceKey faceKey) + { + int meshId = faceKey.meshId; + int faceId = faceKey.faceId; + + HashSet seenFaces; + if (!seenMeshesAndFaces.TryGetValue(meshId, out seenFaces)) + { + seenFaces = new HashSet(); + seenMeshesAndFaces[meshId] = seenFaces; + } + // Never paint the same face the same colour twice in the same operation. + if (seenFaces.Add(faceId)) + { + model.ApplyCommand(new ChangeFacePropertiesCommand(meshId, + new Dictionary() { { faceId, paintedFaceProperties } })); + PlayFeedback(); + + } + } - /// - /// Give haptic and audio feedback to the user about their paint operation, but only if it's been a reasonable - /// amount of time since feedback was last given. - /// - private void PlayFeedback() { - if (Time.time - timeLastPaintFeedbackPlayed > INTERVAL_BETWEEN_PAINT_FEEDBACKS) { - timeLastPaintFeedbackPlayed = Time.time; - audioLibrary.PlayClip(audioLibrary.paintSound); - peltzerController.TriggerHapticFeedback(); - } + /// + /// Give haptic and audio feedback to the user about their paint operation, but only if it's been a reasonable + /// amount of time since feedback was last given. + /// + private void PlayFeedback() + { + if (Time.time - timeLastPaintFeedbackPlayed > INTERVAL_BETWEEN_PAINT_FEEDBACKS) + { + timeLastPaintFeedbackPlayed = Time.time; + audioLibrary.PlayClip(audioLibrary.paintSound); + peltzerController.TriggerHapticFeedback(); + } + } } - } } diff --git a/Assets/Scripts/tools/Reshaper.cs b/Assets/Scripts/tools/Reshaper.cs index c55efd26..c0680bce 100644 --- a/Assets/Scripts/tools/Reshaper.cs +++ b/Assets/Scripts/tools/Reshaper.cs @@ -24,824 +24,948 @@ using com.google.apps.peltzer.client.model.render; using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.tools { - /// - /// A tool responsible for reshaping meshes in the scene. Implemented as MonoBehaviour - /// so we can have an Update() loop. - /// - /// A reshape consists of moving vertices, edges or faces of a mesh. Reshape operations are always done in such - /// a way that the resulting mesh remains valid. If the user attempts to reshape the mesh in an invalid way, we - /// retain the last valid state of the mesh, but show the outline of the invalid mesh to the user to help them - /// understand that they are being geometrically unreasonable and help guide them back to mathematical sanity. - /// - public class Reshaper : MonoBehaviour, IMeshRenderOwner { +namespace com.google.apps.peltzer.client.tools +{ /// - /// The factor by which meshes should be scaled. - /// - private const float SCALE_FACTOR = 1.5f; - public ControllerMain controllerMain; - /// - /// A reference to a controller capable of issuing move commands. - /// - private PeltzerController peltzerController; - /// - /// A reference to the overall model being built. - /// - private Model model; - /// - /// The spatial index to the model. - /// - private SpatialIndex spatialIndex; - /// - /// Selector for detecting which item is hovered or selected. - /// - private Selector selector; - /// - /// Library for playing sounds. - /// - private AudioLibrary audioLibrary; - /// - /// A cache of Mesh representations. - /// - private MeshRepresentationCache meshRepresentationCache; - /// - /// The controller position in model space when the vertices or mesh(es) was (were) grabbed. - /// - private Vector3 moveStartPosition; - /// - /// The controller orientation in model space when the vertices or mesh(es) was (were) grabbed. - /// - private Quaternion reshapeBeginOrientation; - /// - /// The previews of the meshes that the reshaper tool has grabbed and is reshaping, and - /// the Unity GameObjects used to render them to the scene. - /// - private Dictionary grabbedMeshesAndPreviews = new Dictionary(); - /// - /// Meshes that the reshaper tool has grabbed and is reshaping, with all of their static faces removed, - /// and GameObject previews of such. - /// - private Dictionary badMeshesAndPreviews = new Dictionary(); - /// - /// Maintained so we can send some signals to analytics for how many faces people are moving. - /// - private List grabbedFaces = new List(); - /// - /// Maintained so we can send some signals to analytics for how many edges people are moving. - /// - private List grabbedEdges = new List(); - /// - /// All unique vertices that are parts of selected edges. - /// - private HashSet allVertices = new HashSet(); - /// - /// Stores a relation from a vertex back to a face, so we can determine the normal of the face. - /// - private Dictionary vertexToFace = new Dictionary(); - - private WorldSpace worldSpace; - - private bool isReshaping = false; - private bool startedReshapingThisFrame = false; - - // Detection for trigger down & straight back up, vs trigger down and hold -- either of which - // begins an extrusion. - private bool triggerUpToRelease; - private float triggerDownTime; - private bool waitingToDetermineReleaseType; - - /// - /// Used to determine if we should show the snap tooltip or not. Don't show the tooltip if the user already - /// showed enough knowledge of how to snap. - /// - private int completedSnaps = 0; - private const int SNAP_KNOW_HOW_COUNT = 3; - - /// - /// These meshes are the result of mutating the original meshes in the most naive way possible to reflect - /// what the user did (just moving the vertices to where the user said they wanted them). - /// As a result, these will often be invalid, have non-coplanar faces and many other such embarassing - /// defects. + /// A tool responsible for reshaping meshes in the scene. Implemented as MonoBehaviour + /// so we can have an Update() loop. /// - /// We use these to represent what the user is trying to do, but this won't ultimately be the final result of the - /// operation. Instead, these naively mutated meshes are fed to the BackgroundMeshValidator, which will - /// do the hard work (in a separate thread) to figure out how to convert our naive mutation into a valid mesh - /// with all the necessary triangulations, vertex deduping, and other niceties of civilized geometries. - /// - /// We may BRIEFLY use these meshes for display while BackgroundMeshValidator hasn't given us our first - /// "known good state". But as soon as we have a "known valid" state, that's that we use for display - /// instead of this. + /// A reshape consists of moving vertices, edges or faces of a mesh. Reshape operations are always done in such + /// a way that the resulting mesh remains valid. If the user attempts to reshape the mesh in an invalid way, we + /// retain the last valid state of the mesh, but show the outline of the invalid mesh to the user to help them + /// understand that they are being geometrically unreasonable and help guide them back to mathematical sanity. /// - private Dictionary naivelyMutatedMeshes = new Dictionary(); - - private Dictionary> movesByMesh = new Dictionary>(); - - // Background validator. During a reshape operation, it evaluates the validity of our meshes in the background. - private BackgroundMeshValidator backgroundValidator; + public class Reshaper : MonoBehaviour, IMeshRenderOwner + { + /// + /// The factor by which meshes should be scaled. + /// + private const float SCALE_FACTOR = 1.5f; + public ControllerMain controllerMain; + /// + /// A reference to a controller capable of issuing move commands. + /// + private PeltzerController peltzerController; + /// + /// A reference to the overall model being built. + /// + private Model model; + /// + /// The spatial index to the model. + /// + private SpatialIndex spatialIndex; + /// + /// Selector for detecting which item is hovered or selected. + /// + private Selector selector; + /// + /// Library for playing sounds. + /// + private AudioLibrary audioLibrary; + /// + /// A cache of Mesh representations. + /// + private MeshRepresentationCache meshRepresentationCache; + /// + /// The controller position in model space when the vertices or mesh(es) was (were) grabbed. + /// + private Vector3 moveStartPosition; + /// + /// The controller orientation in model space when the vertices or mesh(es) was (were) grabbed. + /// + private Quaternion reshapeBeginOrientation; + /// + /// The previews of the meshes that the reshaper tool has grabbed and is reshaping, and + /// the Unity GameObjects used to render them to the scene. + /// + private Dictionary grabbedMeshesAndPreviews = new Dictionary(); + /// + /// Meshes that the reshaper tool has grabbed and is reshaping, with all of their static faces removed, + /// and GameObject previews of such. + /// + private Dictionary badMeshesAndPreviews = new Dictionary(); + /// + /// Maintained so we can send some signals to analytics for how many faces people are moving. + /// + private List grabbedFaces = new List(); + /// + /// Maintained so we can send some signals to analytics for how many edges people are moving. + /// + private List grabbedEdges = new List(); + /// + /// All unique vertices that are parts of selected edges. + /// + private HashSet allVertices = new HashSet(); + /// + /// Stores a relation from a vertex back to a face, so we can determine the normal of the face. + /// + private Dictionary vertexToFace = new Dictionary(); + + private WorldSpace worldSpace; + + private bool isReshaping = false; + private bool startedReshapingThisFrame = false; + + // Detection for trigger down & straight back up, vs trigger down and hold -- either of which + // begins an extrusion. + private bool triggerUpToRelease; + private float triggerDownTime; + private bool waitingToDetermineReleaseType; + + /// + /// Used to determine if we should show the snap tooltip or not. Don't show the tooltip if the user already + /// showed enough knowledge of how to snap. + /// + private int completedSnaps = 0; + private const int SNAP_KNOW_HOW_COUNT = 3; + + /// + /// These meshes are the result of mutating the original meshes in the most naive way possible to reflect + /// what the user did (just moving the vertices to where the user said they wanted them). + /// As a result, these will often be invalid, have non-coplanar faces and many other such embarassing + /// defects. + /// + /// We use these to represent what the user is trying to do, but this won't ultimately be the final result of the + /// operation. Instead, these naively mutated meshes are fed to the BackgroundMeshValidator, which will + /// do the hard work (in a separate thread) to figure out how to convert our naive mutation into a valid mesh + /// with all the necessary triangulations, vertex deduping, and other niceties of civilized geometries. + /// + /// We may BRIEFLY use these meshes for display while BackgroundMeshValidator hasn't given us our first + /// "known good state". But as soon as we have a "known valid" state, that's that we use for display + /// instead of this. + /// + private Dictionary naivelyMutatedMeshes = new Dictionary(); + + private Dictionary> movesByMesh = new Dictionary>(); + + // Background validator. During a reshape operation, it evaluates the validity of our meshes in the background. + private BackgroundMeshValidator backgroundValidator; + + // State of current operation for real-time validation. + private bool errorFeedbackGivenForCurrentOperation = false; + + /// + /// If we are snapping face moves to normals. + /// + private bool isSnapping = false; + + /// + /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. + /// As such, this setup method must be called before the tool is used for it to have a valid state. + /// + public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, + PaletteController paletteController, Selector selector, AudioLibrary audioLibrary, WorldSpace worldSpace, + SpatialIndex spatialIndex, MeshRepresentationCache meshRepresentationCache) + { + this.model = model; + this.controllerMain = controllerMain; + this.peltzerController = peltzerController; + this.selector = selector; + this.audioLibrary = audioLibrary; + this.worldSpace = worldSpace; + this.spatialIndex = spatialIndex; + this.meshRepresentationCache = meshRepresentationCache; + controllerMain.ControllerActionHandler += ControllerEventHandler; + peltzerController.ModeChangedHandler += ModeChangeEventHandler; + + backgroundValidator = new BackgroundMeshValidator(model); + selector.TurnOnSelectIndicator(); + } - // State of current operation for real-time validation. - private bool errorFeedbackGivenForCurrentOperation = false; + internal struct FaceAndVertexCount + { + internal int faceCount; + internal int vertexCount; - /// - /// If we are snapping face moves to normals. - /// - private bool isSnapping = false; + internal FaceAndVertexCount(int faceCount, int vertexCount) + { + this.faceCount = faceCount; + this.vertexCount = vertexCount; + } + } + Dictionary meshIdToLastKnownFaceAndVertexCounts = new Dictionary(); + + /// + /// Each frame, if a mesh is currently held, update its position in world-space relative + /// to its original position, and the delta between the controller's position at world-start + /// and the controller's current position. + /// + private void Update() + { + if (!PeltzerController.AcquireIfNecessary(ref peltzerController) || peltzerController.mode != ControllerMode.reshape) + { + return; + } - /// - /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. - /// As such, this setup method must be called before the tool is used for it to have a valid state. - /// - public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, - PaletteController paletteController, Selector selector, AudioLibrary audioLibrary, WorldSpace worldSpace, - SpatialIndex spatialIndex, MeshRepresentationCache meshRepresentationCache) { - this.model = model; - this.controllerMain = controllerMain; - this.peltzerController = peltzerController; - this.selector = selector; - this.audioLibrary = audioLibrary; - this.worldSpace = worldSpace; - this.spatialIndex = spatialIndex; - this.meshRepresentationCache = meshRepresentationCache; - controllerMain.ControllerActionHandler += ControllerEventHandler; - peltzerController.ModeChangedHandler += ModeChangeEventHandler; - - backgroundValidator = new BackgroundMeshValidator(model); - selector.TurnOnSelectIndicator(); - } + if (isReshaping) + { + // Wait one frame before performing the first update cycle for an in-progress reshape operation. + // This helps avoid frame drops, given that a lot of work happens in order begin a reshape operation. + if (startedReshapingThisFrame) + { + startedReshapingThisFrame = false; + return; + } + + if (waitingToDetermineReleaseType && Time.time - triggerDownTime > PeltzerController.SINGLE_CLICK_THRESHOLD) + { + waitingToDetermineReleaseType = false; + triggerUpToRelease = true; + } + + // Use the position of the controller to update the "naive" version of the meshes, which reflect what the + // user is trying to do. + UpdateNaivelyMutatedMeshes(); + + // We feed the naive meshes to the background validator. The validator will try to clean it up and produce + // a valid mesh, making it available asynchronously to us later. + backgroundValidator.UpdateMeshes(naivelyMutatedMeshes, allVertices); + + // Note that the background validator is asynchronous, so the validity state that it reports is + // not an immediate answer to the meshes we just provided with UpdateMeshes(). Instead, the validity + // state is likely to reflect the state of the meshes a few frames in the past. + BackgroundMeshValidator.Validity validity = backgroundValidator.ValidityState; + + if (validity == BackgroundMeshValidator.Validity.INVALID) + { + if (!errorFeedbackGivenForCurrentOperation) + { + // The current operation is invalid, play error... + audioLibrary.PlayClip(audioLibrary.errorSound); + peltzerController.TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_2, /* durationSeconds */ 0.25f, /* strength */ 0.3f); + errorFeedbackGivenForCurrentOperation = true; + } + + // Show an error outline of the mesh in its current, invalid state. + foreach (KeyValuePair pair in badMeshesAndPreviews) + { + // Update the bad mesh vertices to match the naive mesh vertices. This is a neat trick: the visuals of + // the error overlay don't need to respect the newly-split faces, so all we're effectively doing is + // updating the positions of the already-known faces that are affected by this reshape operation. + MMesh badMesh = pair.Key; + GameObject preview = pair.Value; + MMesh.GeometryOperation operation = badMesh.StartOperation(); + operation.ModifyVertices(naivelyMutatedMeshes[badMesh.id].GetVertices()); + operation.CommitWithoutRecalculation(); + + // Update the preview of the bad mesh and set it to show. + preview.SetActive(true); + MMesh.AttachMeshToGameObject( + worldSpace, preview, badMesh, /* updateOnly */ true, MaterialRegistry.GetReshaperErrorMaterial()); + } + } + else if (validity == BackgroundMeshValidator.Validity.VALID) + { + // The current move is valid, remove the error outlines for each mesh. + foreach (GameObject badMeshPreview in badMeshesAndPreviews.Values) + { + badMeshPreview.SetActive(false); + } + errorFeedbackGivenForCurrentOperation = false; + } + else + { + // If the validity state is not VALID or INVALID, it means the validator hasn't decided yet. + // In that case, we don't do anything. + } + + // Update the preview of the mesh. For each mesh, if we have last good state for it (by courtesy of the + // background validator), then we use it for display. If we don't, we use our naive meshes. + foreach (KeyValuePair pair in grabbedMeshesAndPreviews) + { + MMesh snappyPreview; + if (validity != BackgroundMeshValidator.Validity.NOT_YET_KNOWN) + { + // Display the "last known good" state. + Dictionary lastGoodState = backgroundValidator.GetLastValidState(); + MMesh lastGoodStateForMesh; + lastGoodState.TryGetValue(pair.Key.id, out lastGoodStateForMesh); + snappyPreview = lastGoodStateForMesh != null ? lastGoodStateForMesh : naivelyMutatedMeshes[pair.Key.id]; + } + else + { + // Display the naive preview, as that's the best thing we have for now. + // Hopefully BackgroundMeshValidator will catch up in a few frames and we'll have something + // better to show. + snappyPreview = naivelyMutatedMeshes[pair.Key.id]; + } + + // We wish to avoid re-triangulating the entire mesh to generate a preview as often as possible, which is + // controlled by the updateOnly flag below. + // Our current heuristic, which we expect to work in the vast majority of cases, is to check whether the face + // and vertex count has not changed as a result of the most-recent mesh fixing operation. If they have + // remained constant then it's likely we haven't merged any verts, split any faces, or un-done either of + // those things (or anything else that can affect the geometry beyond vertex location). + FaceAndVertexCount lastKnownFaceAndVertexCount = meshIdToLastKnownFaceAndVertexCounts[pair.Key.id]; + FaceAndVertexCount currentFaceAndVertexCount = + new FaceAndVertexCount(snappyPreview.faceCount, snappyPreview.vertexCount); + bool updateOnly = lastKnownFaceAndVertexCount.faceCount == currentFaceAndVertexCount.faceCount + && lastKnownFaceAndVertexCount.vertexCount == currentFaceAndVertexCount.vertexCount; + + MMesh.AttachMeshToGameObject(worldSpace, pair.Value, snappyPreview, updateOnly); + meshIdToLastKnownFaceAndVertexCounts[pair.Key.id] = currentFaceAndVertexCount; + } + } + else + { + // Update the position of the selector. + if (selector == null) + { + selector.TurnOnSelectIndicator(); + } + selector.SelectAtPosition(peltzerController.LastPositionModel, Selector.FACES_EDGES_AND_VERTICES); + selector.UpdateInactive(Selector.FACES_EDGES_AND_VERTICES); + // Play the selection animation for newly-hovered or -selected faces. + if (selector.hoverFace != null) + { + PeltzerMain.Instance.highlightUtils.SetFaceStyleToSelect(selector.hoverFace, selector.selectorPosition); + } + if (selector.selectedFaces != null) + { + foreach (FaceKey faceKey in selector.selectedFaces) + { + PeltzerMain.Instance.highlightUtils.SetFaceStyleToSelect(faceKey, selector.selectorPosition); + } + } + } + } - internal struct FaceAndVertexCount { - internal int faceCount; - internal int vertexCount; + /// + /// Makes only the supplied tooltip visible and ensures the others are off. + /// + /// The tooltip text to activate. + /// The hover state. + private void SetHoverTooltip(GameObject tooltip, TouchpadHoverState state, TouchpadOverlay currentOverlay) + { + if (!tooltip.activeSelf) + { + UnsetAllHoverTooltips(); + if (currentOverlay == TouchpadOverlay.RESET_ZOOM) + { + return; + } + tooltip.SetActive(true); + peltzerController.SetTouchpadHoverTexture(state); + peltzerController.TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_1, + 0.003f, + 0.15f + ); + } + } - internal FaceAndVertexCount(int faceCount, int vertexCount) { - this.faceCount = faceCount; - this.vertexCount = vertexCount; - } - } - Dictionary meshIdToLastKnownFaceAndVertexCounts = new Dictionary(); + /// + /// Unset all of the touchpad hover text tooltips. + /// + private void UnsetAllHoverTooltips() + { + peltzerController.controllerGeometry.modifyTooltipUp.SetActive(false); + peltzerController.controllerGeometry.modifyTooltipLeft.SetActive(false); + peltzerController.controllerGeometry.modifyTooltipRight.SetActive(false); + peltzerController.controllerGeometry.resizeUpTooltip.SetActive(false); + peltzerController.controllerGeometry.resizeDownTooltip.SetActive(false); + peltzerController.SetTouchpadHoverTexture(TouchpadHoverState.NONE); + } - /// - /// Each frame, if a mesh is currently held, update its position in world-space relative - /// to its original position, and the delta between the controller's position at world-start - /// and the controller's current position. - /// - private void Update() { - if (!PeltzerController.AcquireIfNecessary(ref peltzerController) || peltzerController.mode != ControllerMode.reshape) { - return; - } - - if (isReshaping) { - // Wait one frame before performing the first update cycle for an in-progress reshape operation. - // This helps avoid frame drops, given that a lot of work happens in order begin a reshape operation. - if (startedReshapingThisFrame) { - startedReshapingThisFrame = false; - return; + private bool IsBeginOperationEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN + && !isReshaping; } - if (waitingToDetermineReleaseType && Time.time - triggerDownTime > PeltzerController.SINGLE_CLICK_THRESHOLD) { - waitingToDetermineReleaseType = false; - triggerUpToRelease = true; + private bool IsCompleteSingleClickEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.UP + && waitingToDetermineReleaseType; } - // Use the position of the controller to update the "naive" version of the meshes, which reflect what the - // user is trying to do. - UpdateNaivelyMutatedMeshes(); - - // We feed the naive meshes to the background validator. The validator will try to clean it up and produce - // a valid mesh, making it available asynchronously to us later. - backgroundValidator.UpdateMeshes(naivelyMutatedMeshes, allVertices); - - // Note that the background validator is asynchronous, so the validity state that it reports is - // not an immediate answer to the meshes we just provided with UpdateMeshes(). Instead, the validity - // state is likely to reflect the state of the meshes a few frames in the past. - BackgroundMeshValidator.Validity validity = backgroundValidator.ValidityState; - - if (validity == BackgroundMeshValidator.Validity.INVALID) { - if (!errorFeedbackGivenForCurrentOperation) { - // The current operation is invalid, play error... - audioLibrary.PlayClip(audioLibrary.errorSound); - peltzerController.TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_2, /* durationSeconds */ 0.25f, /* strength */ 0.3f); - errorFeedbackGivenForCurrentOperation = true; - } - - // Show an error outline of the mesh in its current, invalid state. - foreach (KeyValuePair pair in badMeshesAndPreviews) { - // Update the bad mesh vertices to match the naive mesh vertices. This is a neat trick: the visuals of - // the error overlay don't need to respect the newly-split faces, so all we're effectively doing is - // updating the positions of the already-known faces that are affected by this reshape operation. - MMesh badMesh = pair.Key; - GameObject preview = pair.Value; - MMesh.GeometryOperation operation = badMesh.StartOperation(); - operation.ModifyVertices(naivelyMutatedMeshes[badMesh.id].GetVertices()); - operation.CommitWithoutRecalculation(); - - // Update the preview of the bad mesh and set it to show. - preview.SetActive(true); - MMesh.AttachMeshToGameObject( - worldSpace, preview, badMesh, /* updateOnly */ true, MaterialRegistry.GetReshaperErrorMaterial()); - } - } else if (validity == BackgroundMeshValidator.Validity.VALID) { - // The current move is valid, remove the error outlines for each mesh. - foreach (GameObject badMeshPreview in badMeshesAndPreviews.Values) { - badMeshPreview.SetActive(false); - } - errorFeedbackGivenForCurrentOperation = false; - } else { - // If the validity state is not VALID or INVALID, it means the validator hasn't decided yet. - // In that case, we don't do anything. + private bool IsReleaseEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && ((args.Action == ButtonAction.UP && triggerUpToRelease) + || (args.Action == ButtonAction.DOWN && !triggerUpToRelease)) + && isReshaping; } - // Update the preview of the mesh. For each mesh, if we have last good state for it (by courtesy of the - // background validator), then we use it for display. If we don't, we use our naive meshes. - foreach (KeyValuePair pair in grabbedMeshesAndPreviews) { - MMesh snappyPreview; - if (validity != BackgroundMeshValidator.Validity.NOT_YET_KNOWN) { - // Display the "last known good" state. - Dictionary lastGoodState = backgroundValidator.GetLastValidState(); - MMesh lastGoodStateForMesh; - lastGoodState.TryGetValue(pair.Key.id, out lastGoodStateForMesh); - snappyPreview = lastGoodStateForMesh != null ? lastGoodStateForMesh : naivelyMutatedMeshes[pair.Key.id]; - } else { - // Display the naive preview, as that's the best thing we have for now. - // Hopefully BackgroundMeshValidator will catch up in a few frames and we'll have something - // better to show. - snappyPreview = naivelyMutatedMeshes[pair.Key.id]; - } - - // We wish to avoid re-triangulating the entire mesh to generate a preview as often as possible, which is - // controlled by the updateOnly flag below. - // Our current heuristic, which we expect to work in the vast majority of cases, is to check whether the face - // and vertex count has not changed as a result of the most-recent mesh fixing operation. If they have - // remained constant then it's likely we haven't merged any verts, split any faces, or un-done either of - // those things (or anything else that can affect the geometry beyond vertex location). - FaceAndVertexCount lastKnownFaceAndVertexCount = meshIdToLastKnownFaceAndVertexCounts[pair.Key.id]; - FaceAndVertexCount currentFaceAndVertexCount = - new FaceAndVertexCount(snappyPreview.faceCount, snappyPreview.vertexCount); - bool updateOnly = lastKnownFaceAndVertexCount.faceCount == currentFaceAndVertexCount.faceCount - && lastKnownFaceAndVertexCount.vertexCount == currentFaceAndVertexCount.vertexCount; - - MMesh.AttachMeshToGameObject(worldSpace, pair.Value, snappyPreview, updateOnly); - meshIdToLastKnownFaceAndVertexCounts[pair.Key.id] = currentFaceAndVertexCount; + private static bool IsStartSnapEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN; } - } else { - // Update the position of the selector. - if (selector == null) { - selector.TurnOnSelectIndicator(); + + private static bool IsEndSnapEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.UP; } - selector.SelectAtPosition(peltzerController.LastPositionModel, Selector.FACES_EDGES_AND_VERTICES); - selector.UpdateInactive(Selector.FACES_EDGES_AND_VERTICES); - // Play the selection animation for newly-hovered or -selected faces. - if (selector.hoverFace != null) { - PeltzerMain.Instance.highlightUtils.SetFaceStyleToSelect(selector.hoverFace, selector.selectorPosition); + + // Touchpad Hover + private bool IsSetUpHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && !isReshaping + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.TOP; } - if (selector.selectedFaces != null) { - foreach (FaceKey faceKey in selector.selectedFaces) { - PeltzerMain.Instance.highlightUtils.SetFaceStyleToSelect(faceKey, selector.selectorPosition); - } + + private bool IsSetDownHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && !isReshaping + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.BOTTOM; } - } - } - /// - /// Makes only the supplied tooltip visible and ensures the others are off. - /// - /// The tooltip text to activate. - /// The hover state. - private void SetHoverTooltip(GameObject tooltip, TouchpadHoverState state, TouchpadOverlay currentOverlay) { - if (!tooltip.activeSelf) { - UnsetAllHoverTooltips(); - if (currentOverlay == TouchpadOverlay.RESET_ZOOM) { - return; + private bool IsSetLeftHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && !isReshaping + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.LEFT; } - tooltip.SetActive(true); - peltzerController.SetTouchpadHoverTexture(state); - peltzerController.TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_1, - 0.003f, - 0.15f - ); - } - } - /// - /// Unset all of the touchpad hover text tooltips. - /// - private void UnsetAllHoverTooltips() { - peltzerController.controllerGeometry.modifyTooltipUp.SetActive(false); - peltzerController.controllerGeometry.modifyTooltipLeft.SetActive(false); - peltzerController.controllerGeometry.modifyTooltipRight.SetActive(false); - peltzerController.controllerGeometry.resizeUpTooltip.SetActive(false); - peltzerController.controllerGeometry.resizeDownTooltip.SetActive(false); - peltzerController.SetTouchpadHoverTexture(TouchpadHoverState.NONE); - } + private bool IsSetRightHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && !isReshaping + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.RIGHT; + } - private bool IsBeginOperationEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN - && !isReshaping; - } + private static bool IsUnsetAllHoverTooltipsEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.NONE; + } - private bool IsCompleteSingleClickEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.UP - && waitingToDetermineReleaseType; - } + /// + /// Generates a preview of a mesh with faces that the Reshaper will not modify removed. + /// + /// + private void GenerateBadMeshesAndPreviews() + { + badMeshesAndPreviews.Clear(); + + // Find the faces that are not being modified, per mesh. + Dictionary> movesByMesh = new Dictionary>(); + foreach (VertexKey vertexKey in allVertices) + { + HashSet set; + if (!movesByMesh.TryGetValue(vertexKey.meshId, out set)) + { + movesByMesh[vertexKey.meshId] = new HashSet(); + set = movesByMesh[vertexKey.meshId]; + } + set.Add(vertexKey.vertexId); + } - private bool IsReleaseEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && ((args.Action == ButtonAction.UP && triggerUpToRelease) - || (args.Action == ButtonAction.DOWN && !triggerUpToRelease)) - && isReshaping; - } + // Remove static faces and create a preview. + foreach (MMesh mesh in grabbedMeshesAndPreviews.Keys) + { + // We need to clone the original mesh as we will be modifying it. + MMesh badMesh = mesh.Clone(); + HashSet badMeshVerts = movesByMesh[badMesh.id]; + MMesh.GeometryOperation operation = badMesh.StartOperation(); + foreach (int faceId in mesh.GetFaceIds()) + { + // For each face, we check to see if any of its vertices are not in the 'allVertices' being reshaped. + // If so, that face will never move and as such we will never need an 'error' preview for it, so we + // remove it from the 'badMesh'. + // We don't *need* to remove the vertices, so we don't bother here. + bool shouldRemoveFace = true; + Face face = badMesh.GetFace(faceId); + foreach (int vertId in face.vertexIds) + { + if (badMeshVerts.Contains(vertId)) + { + shouldRemoveFace = false; + break; + } + } + + if (shouldRemoveFace) + { + operation.DeleteFace(faceId); + } + } + // Face deletion doesn't change normals, so don't recalculate them. + operation.CommitWithoutRecalculation(); + + // Add the preview to the collection of previews for this operation. + GameObject badMeshPreview = MeshHelper.GameObjectFromMMesh(worldSpace, badMesh, + MaterialRegistry.GetReshaperErrorMaterial()); + badMeshesAndPreviews.Add(badMesh, badMeshPreview); + badMeshPreview.SetActive(false); + } + } - private static bool IsStartSnapEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN; - } + public void StartMove() + { + moveStartPosition = peltzerController.LastPositionModel; + reshapeBeginOrientation = peltzerController.LastRotationModel; + peltzerController.HideTooltips(); + peltzerController.HideModifyOverlays(); + + // Create a Unity GameObject to render the meshes whilst they are being moved. + // + // Also add the vertices of each face to the collection of all selected vertices. + // Maintain a reference from vertex back to face, so we can tell which normal + // to move along if we are snapping. + + // Add all faces selected. + IEnumerable selectedFaces = selector.SelectedOrHoveredFaces(); + foreach (FaceKey faceKey in selectedFaces) + { + grabbedFaces.Add(faceKey); + MMesh mesh = model.GetMesh(faceKey.meshId); + Face face = mesh.GetFace(faceKey.faceId); + if (!grabbedMeshesAndPreviews.ContainsKey(mesh)) + { + GameObject preview = meshRepresentationCache.GeneratePreview(mesh); + grabbedMeshesAndPreviews.Add(mesh, preview); + meshIdToLastKnownFaceAndVertexCounts.Add(mesh.id, + new FaceAndVertexCount(mesh.faceCount, mesh.vertexCount)); + model.ClaimMesh(mesh.id, this); + } + foreach (int vertId in face.vertexIds) + { + VertexKey newVertexKey = new VertexKey(faceKey.meshId, vertId); + allVertices.Add(newVertexKey); + if (!vertexToFace.ContainsKey(newVertexKey)) + { + vertexToFace.Add(newVertexKey, faceKey); + } + } + } - private static bool IsEndSnapEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.UP; - } + // Add all edges selected. + IEnumerable selectedEdges = selector.SelectedOrHoveredEdges(); + foreach (EdgeKey edgeKey in selectedEdges) + { + grabbedEdges.Add(edgeKey); + MMesh mesh = model.GetMesh(edgeKey.meshId); + if (!grabbedMeshesAndPreviews.ContainsKey(mesh)) + { + GameObject preview = meshRepresentationCache.GeneratePreview(mesh); + grabbedMeshesAndPreviews.Add(mesh, preview); + meshIdToLastKnownFaceAndVertexCounts.Add(mesh.id, + new FaceAndVertexCount(mesh.faceCount, mesh.vertexCount)); + model.ClaimMesh(mesh.id, this); + } + + VertexKey newVertexKey1 = new VertexKey(edgeKey.meshId, edgeKey.vertexId1); + VertexKey newVertexKey2 = new VertexKey(edgeKey.meshId, edgeKey.vertexId2); + allVertices.Add(newVertexKey1); + allVertices.Add(newVertexKey2); + } - // Touchpad Hover - private bool IsSetUpHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && !isReshaping - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.TOP; - } + // Add all vertices selected. + IEnumerable selectedVertices = selector.SelectedOrHoveredVertices(); + foreach (VertexKey vertexKey in selectedVertices) + { + MMesh mesh = model.GetMesh(vertexKey.meshId); + if (!grabbedMeshesAndPreviews.ContainsKey(mesh)) + { + GameObject preview = meshRepresentationCache.GeneratePreview(mesh); + grabbedMeshesAndPreviews.Add(mesh, preview); + meshIdToLastKnownFaceAndVertexCounts.Add(mesh.id, + new FaceAndVertexCount(mesh.faceCount, mesh.vertexCount)); + model.ClaimMesh(mesh.id, this); + } + + // Add each selected vertex that isn't included in a face or segment. + if (!allVertices.Contains(vertexKey)) + { + allVertices.Add(vertexKey); + } + } - private bool IsSetDownHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && !isReshaping - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.BOTTOM; - } + // Generate the previews of 'bad meshes' -- the outline of meshes that will show in case of an error. + GenerateBadMeshesAndPreviews(); - private bool IsSetLeftHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && !isReshaping - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.LEFT; - } + // Did we actually select something? + isReshaping = allVertices.Count > 0; + if (!isReshaping) + { + return; + } - private bool IsSetRightHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && !isReshaping - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.RIGHT; - } + // Ensure we're not multi-selecting now. + selector.EndMultiSelection(); - private static bool IsUnsetAllHoverTooltipsEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.NONE; - } + // De-select everything, Reshaper will now manage state of what is being moved and the corresponding + // preview GameObjects. + selector.DeselectAll(); - /// - /// Generates a preview of a mesh with faces that the Reshaper will not modify removed. - /// - /// - private void GenerateBadMeshesAndPreviews() { - badMeshesAndPreviews.Clear(); - - // Find the faces that are not being modified, per mesh. - Dictionary> movesByMesh = new Dictionary>(); - foreach (VertexKey vertexKey in allVertices) { - HashSet set; - if (!movesByMesh.TryGetValue(vertexKey.meshId, out set)) { - movesByMesh[vertexKey.meshId] = new HashSet(); - set = movesByMesh[vertexKey.meshId]; - } - set.Add(vertexKey.vertexId); - } - - // Remove static faces and create a preview. - foreach (MMesh mesh in grabbedMeshesAndPreviews.Keys) { - // We need to clone the original mesh as we will be modifying it. - MMesh badMesh = mesh.Clone(); - HashSet badMeshVerts = movesByMesh[badMesh.id]; - MMesh.GeometryOperation operation = badMesh.StartOperation(); - foreach (int faceId in mesh.GetFaceIds()) { - // For each face, we check to see if any of its vertices are not in the 'allVertices' being reshaped. - // If so, that face will never move and as such we will never need an 'error' preview for it, so we - // remove it from the 'badMesh'. - // We don't *need* to remove the vertices, so we don't bother here. - bool shouldRemoveFace = true; - Face face = badMesh.GetFace(faceId); - foreach (int vertId in face.vertexIds) { - if (badMeshVerts.Contains(vertId)) { - shouldRemoveFace = false; - break; - } - } + // Set up the validation work. + backgroundValidator.StartValidating(); - if (shouldRemoveFace) { - operation.DeleteFace(faceId); - } - } - // Face deletion doesn't change normals, so don't recalculate them. - operation.CommitWithoutRecalculation(); - - // Add the preview to the collection of previews for this operation. - GameObject badMeshPreview = MeshHelper.GameObjectFromMMesh(worldSpace, badMesh, - MaterialRegistry.GetReshaperErrorMaterial()); - badMeshesAndPreviews.Add(badMesh, badMeshPreview); - badMeshPreview.SetActive(false); - } - } + startedReshapingThisFrame = true; - public void StartMove() { - moveStartPosition = peltzerController.LastPositionModel; - reshapeBeginOrientation = peltzerController.LastRotationModel; - peltzerController.HideTooltips(); - peltzerController.HideModifyOverlays(); - - // Create a Unity GameObject to render the meshes whilst they are being moved. - // - // Also add the vertices of each face to the collection of all selected vertices. - // Maintain a reference from vertex back to face, so we can tell which normal - // to move along if we are snapping. - - // Add all faces selected. - IEnumerable selectedFaces = selector.SelectedOrHoveredFaces(); - foreach (FaceKey faceKey in selectedFaces) { - grabbedFaces.Add(faceKey); - MMesh mesh = model.GetMesh(faceKey.meshId); - Face face = mesh.GetFace(faceKey.faceId); - if (!grabbedMeshesAndPreviews.ContainsKey(mesh)) { - GameObject preview = meshRepresentationCache.GeneratePreview(mesh); - grabbedMeshesAndPreviews.Add(mesh, preview); - meshIdToLastKnownFaceAndVertexCounts.Add(mesh.id, - new FaceAndVertexCount(mesh.faceCount, mesh.vertexCount)); - model.ClaimMesh(mesh.id, this); - } - foreach (int vertId in face.vertexIds) { - VertexKey newVertexKey = new VertexKey(faceKey.meshId, vertId); - allVertices.Add(newVertexKey); - if (!vertexToFace.ContainsKey(newVertexKey)) { - vertexToFace.Add(newVertexKey, faceKey); - } - } - } - - // Add all edges selected. - IEnumerable selectedEdges = selector.SelectedOrHoveredEdges(); - foreach (EdgeKey edgeKey in selectedEdges) { - grabbedEdges.Add(edgeKey); - MMesh mesh = model.GetMesh(edgeKey.meshId); - if (!grabbedMeshesAndPreviews.ContainsKey(mesh)) { - GameObject preview = meshRepresentationCache.GeneratePreview(mesh); - grabbedMeshesAndPreviews.Add(mesh, preview); - meshIdToLastKnownFaceAndVertexCounts.Add(mesh.id, - new FaceAndVertexCount(mesh.faceCount, mesh.vertexCount)); - model.ClaimMesh(mesh.id, this); + // Play some feedback. + audioLibrary.PlayClip(audioLibrary.grabMeshPartSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); } - VertexKey newVertexKey1 = new VertexKey(edgeKey.meshId, edgeKey.vertexId1); - VertexKey newVertexKey2 = new VertexKey(edgeKey.meshId, edgeKey.vertexId2); - allVertices.Add(newVertexKey1); - allVertices.Add(newVertexKey2); - } - - // Add all vertices selected. - IEnumerable selectedVertices = selector.SelectedOrHoveredVertices(); - foreach (VertexKey vertexKey in selectedVertices) { - MMesh mesh = model.GetMesh(vertexKey.meshId); - if (!grabbedMeshesAndPreviews.ContainsKey(mesh)) { - GameObject preview = meshRepresentationCache.GeneratePreview(mesh); - grabbedMeshesAndPreviews.Add(mesh, preview); - meshIdToLastKnownFaceAndVertexCounts.Add(mesh.id, - new FaceAndVertexCount(mesh.faceCount, mesh.vertexCount)); - model.ClaimMesh(mesh.id, this); + public bool IsReshaping() + { + return isReshaping; } - // Add each selected vertex that isn't included in a face or segment. - if (!allVertices.Contains(vertexKey)) { - allVertices.Add(vertexKey); + public bool IsReshapingFaces() + { + return isReshaping && grabbedFaces.Count > 0; } - } - // Generate the previews of 'bad meshes' -- the outline of meshes that will show in case of an error. - GenerateBadMeshesAndPreviews(); + private void CompleteMove() + { + AssertOrThrow.True(isReshaping, "CompleteMove() called without isReshaping == true."); - // Did we actually select something? - isReshaping = allVertices.Count > 0; - if (!isReshaping) { - return; - } + // First, finish any bg work. + backgroundValidator.StopValidating(); + isReshaping = false; - // Ensure we're not multi-selecting now. - selector.EndMultiSelection(); + peltzerController.ShowTooltips(); + peltzerController.ShowModifyOverlays(); - // De-select everything, Reshaper will now manage state of what is being moved and the corresponding - // preview GameObjects. - selector.DeselectAll(); + bool moveErrors = false; - // Set up the validation work. - backgroundValidator.StartValidating(); - - startedReshapingThisFrame = true; - - // Play some feedback. - audioLibrary.PlayClip(audioLibrary.grabMeshPartSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - } - - public bool IsReshaping() { - return isReshaping; - } - - public bool IsReshapingFaces() { - return isReshaping && grabbedFaces.Count > 0; - } - - private void CompleteMove() { - AssertOrThrow.True(isReshaping, "CompleteMove() called without isReshaping == true."); - - // First, finish any bg work. - backgroundValidator.StopValidating(); - isReshaping = false; + // Unhide and check we have some valid state for each mesh. + Dictionary lastGoodState = backgroundValidator.GetLastValidState(); + foreach (int meshId in naivelyMutatedMeshes.Keys) + { + if (!lastGoodState.ContainsKey(meshId)) + { + // We ended up with a mesh with no valid state from this move, abort. + lastGoodState.Clear(); + moveErrors = true; + break; + } + } - peltzerController.ShowTooltips(); - peltzerController.ShowModifyOverlays(); + List commands = new List(); + if (lastGoodState.Count > 0 && !moveErrors) + { + // Update each mesh. + foreach (int meshId in naivelyMutatedMeshes.Keys) + { + // If any mesh is invalid, don't bother checking the rest. + MMesh updatedMesh = lastGoodState[meshId]; + updatedMesh.RecalcBounds(); + if (model.CanAddMesh(updatedMesh)) + { + commands.Add(new ReplaceMeshCommand(meshId, updatedMesh)); + } + else + { + moveErrors = true; + break; + } + } + } - bool moveErrors = false; + if (moveErrors) + { + audioLibrary.PlayClip(audioLibrary.errorSound); + peltzerController.TriggerHapticFeedback(); + } + else + { + audioLibrary.PlayClip(audioLibrary.releaseMeshSound); + model.ApplyCommand(new CompositeCommand(commands)); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + if (grabbedFaces.Count > 0) + { + PeltzerMain.Instance.faceReshapesCompleted++; + } + } - // Unhide and check we have some valid state for each mesh. - Dictionary lastGoodState = backgroundValidator.GetLastValidState(); - foreach (int meshId in naivelyMutatedMeshes.Keys) { - if (!lastGoodState.ContainsKey(meshId)) { - // We ended up with a mesh with no valid state from this move, abort. - lastGoodState.Clear(); - moveErrors = true; - break; + ClearState(); } - } - - List commands = new List(); - if (lastGoodState.Count > 0 && !moveErrors) { - // Update each mesh. - foreach (int meshId in naivelyMutatedMeshes.Keys) { - // If any mesh is invalid, don't bother checking the rest. - MMesh updatedMesh = lastGoodState[meshId]; - updatedMesh.RecalcBounds(); - if (model.CanAddMesh(updatedMesh)) { - commands.Add(new ReplaceMeshCommand(meshId, updatedMesh)); - } else { - moveErrors = true; - break; - } + + /// + /// Claim responsibility for rendering a mesh from this class. + /// This should only be called by Model, as otherwise Model's knowledge of current ownership will be incorrect. + /// + public int ClaimMesh(int meshId, IMeshRenderOwner fosterRenderer) + { + MMesh mesh = model.GetMesh(meshId); + if (grabbedMeshesAndPreviews.ContainsKey(mesh)) + { + GameObject go = grabbedMeshesAndPreviews[model.GetMesh(meshId)]; + DestroyImmediate(go); + grabbedMeshesAndPreviews.Remove(mesh); + return meshId; + } + // Didn't have it, can't relinquish ownership. + return -1; } - } - - if (moveErrors) { - audioLibrary.PlayClip(audioLibrary.errorSound); - peltzerController.TriggerHapticFeedback(); - } else { - audioLibrary.PlayClip(audioLibrary.releaseMeshSound); - model.ApplyCommand(new CompositeCommand(commands)); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - if (grabbedFaces.Count > 0) { - PeltzerMain.Instance.faceReshapesCompleted++; + + public void ClearState() + { + ClearPreviewsAndRestoreHiddenMeshesAndSelector(); + grabbedFaces.Clear(); + grabbedEdges.Clear(); } - } - ClearState(); - } + public void ClearPreviewsAndRestoreHiddenMeshesAndSelector() + { + // Clear cached copy of preview mesh. + naivelyMutatedMeshes.Clear(); - /// - /// Claim responsibility for rendering a mesh from this class. - /// This should only be called by Model, as otherwise Model's knowledge of current ownership will be incorrect. - /// - public int ClaimMesh(int meshId, IMeshRenderOwner fosterRenderer) { - MMesh mesh = model.GetMesh(meshId); - if (grabbedMeshesAndPreviews.ContainsKey(mesh)) { - GameObject go = grabbedMeshesAndPreviews[model.GetMesh(meshId)]; - DestroyImmediate(go); - grabbedMeshesAndPreviews.Remove(mesh); - return meshId; - } - // Didn't have it, can't relinquish ownership. - return -1; - } + movesByMesh.Clear(); - public void ClearState() { - ClearPreviewsAndRestoreHiddenMeshesAndSelector(); - grabbedFaces.Clear(); - grabbedEdges.Clear(); - } + // Show the meshes again. + foreach (MMesh mesh in grabbedMeshesAndPreviews.Keys) + { + model.RelinquishMesh(mesh.id, this); + DestroyImmediate(grabbedMeshesAndPreviews[mesh]); + } - public void ClearPreviewsAndRestoreHiddenMeshesAndSelector() { - // Clear cached copy of preview mesh. - naivelyMutatedMeshes.Clear(); - - movesByMesh.Clear(); - - // Show the meshes again. - foreach (MMesh mesh in grabbedMeshesAndPreviews.Keys) { - model.RelinquishMesh(mesh.id, this); - DestroyImmediate(grabbedMeshesAndPreviews[mesh]); - } - - // Destroy all of the badmesh previews. - foreach (GameObject gameObject in badMeshesAndPreviews.Values) { - DestroyImmediate(gameObject); - } - - allVertices.Clear(); - vertexToFace.Clear(); - grabbedMeshesAndPreviews.Clear(); - meshIdToLastKnownFaceAndVertexCounts.Clear(); - badMeshesAndPreviews.Clear(); - selector.DeselectAll(); - } + // Destroy all of the badmesh previews. + foreach (GameObject gameObject in badMeshesAndPreviews.Values) + { + DestroyImmediate(gameObject); + } - /// - /// Updates our naive meshes, moving the vertices to reflect the the controller's movement. - /// This means making a copy of the mesh from the model and then applying the delta to each mesh separately. - /// - private void UpdateNaivelyMutatedMeshes() { - Vector3 delta = peltzerController.LastPositionModel - moveStartPosition; - - foreach (VertexKey vertexKey in allVertices) { - MMesh mesh; - if (!naivelyMutatedMeshes.TryGetValue(vertexKey.meshId, out mesh)) { - mesh = model.GetMesh(vertexKey.meshId).Clone(); - naivelyMutatedMeshes[vertexKey.meshId] = mesh; - movesByMesh[vertexKey.meshId] = new Dictionary(); + allVertices.Clear(); + vertexToFace.Clear(); + grabbedMeshesAndPreviews.Clear(); + meshIdToLastKnownFaceAndVertexCounts.Clear(); + badMeshesAndPreviews.Clear(); + selector.DeselectAll(); } - Vector3 oldLocInModelCoords = model.GetMesh(vertexKey.meshId).VertexPositionInModelCoords(vertexKey.vertexId); - Vector3 newLocInMeshSpace; - if (isSnapping || peltzerController.isBlockMode) { - FaceKey faceKey; - if (vertexToFace.TryGetValue(vertexKey, out faceKey) && mesh.HasFace(faceKey.faceId)) { - // This vertex is part of an entire face that we are moving. In this case we don't want individual - // vertices to snap to the grid or other vertices, as that would deform the face. Instead, we want the face - // motion to snap to an ad-hoc grid defined by its normal. - newLocInMeshSpace = mesh.ModelCoordsToMeshCoords( - oldLocInModelCoords + Vector3.Project(GridUtils.SnapToGrid(delta), - mesh.rotation * mesh.GetFace(faceKey.faceId).normal)); - } else { - // This is an individual vertex that we're moving (not part of a selected face). So snap it to - // other vertices and the grid in MODEL space. - List nearbyVertices = null; - if (allVertices.Count == 1) { - List> nearbyVertexPairs; - spatialIndex.FindVerticesClosestTo( - oldLocInModelCoords + delta, - GridUtils.GRID_SIZE, - out nearbyVertexPairs); - - // Parse the DistancePairs returned from the SpatialIndex, we will only need the Keys. - nearbyVertices = nearbyVertexPairs.Select(pair => pair.value).ToList(); + + /// + /// Updates our naive meshes, moving the vertices to reflect the the controller's movement. + /// This means making a copy of the mesh from the model and then applying the delta to each mesh separately. + /// + private void UpdateNaivelyMutatedMeshes() + { + Vector3 delta = peltzerController.LastPositionModel - moveStartPosition; + + foreach (VertexKey vertexKey in allVertices) + { + MMesh mesh; + if (!naivelyMutatedMeshes.TryGetValue(vertexKey.meshId, out mesh)) + { + mesh = model.GetMesh(vertexKey.meshId).Clone(); + naivelyMutatedMeshes[vertexKey.meshId] = mesh; + movesByMesh[vertexKey.meshId] = new Dictionary(); + } + Vector3 oldLocInModelCoords = model.GetMesh(vertexKey.meshId).VertexPositionInModelCoords(vertexKey.vertexId); + Vector3 newLocInMeshSpace; + if (isSnapping || peltzerController.isBlockMode) + { + FaceKey faceKey; + if (vertexToFace.TryGetValue(vertexKey, out faceKey) && mesh.HasFace(faceKey.faceId)) + { + // This vertex is part of an entire face that we are moving. In this case we don't want individual + // vertices to snap to the grid or other vertices, as that would deform the face. Instead, we want the face + // motion to snap to an ad-hoc grid defined by its normal. + newLocInMeshSpace = mesh.ModelCoordsToMeshCoords( + oldLocInModelCoords + Vector3.Project(GridUtils.SnapToGrid(delta), + mesh.rotation * mesh.GetFace(faceKey.faceId).normal)); + } + else + { + // This is an individual vertex that we're moving (not part of a selected face). So snap it to + // other vertices and the grid in MODEL space. + List nearbyVertices = null; + if (allVertices.Count == 1) + { + List> nearbyVertexPairs; + spatialIndex.FindVerticesClosestTo( + oldLocInModelCoords + delta, + GridUtils.GRID_SIZE, + out nearbyVertexPairs); + + // Parse the DistancePairs returned from the SpatialIndex, we will only need the Keys. + nearbyVertices = nearbyVertexPairs.Select(pair => pair.value).ToList(); + } + + if (nearbyVertices != null && nearbyVertices.Count > 0) + { + // Found a vertex that we should be snapping to. + newLocInMeshSpace = mesh.ModelCoordsToMeshCoords( + MeshMath.FindClosestVertex(nearbyVertices, oldLocInModelCoords + delta, model)); + } + else + { + // We have more than one vertex held, or the vertex we're holding was not snapped to another vertex. + // So, we move it by a snapped delta. + Vector3 newLocModelSpace = oldLocInModelCoords + GridUtils.SnapToGrid(delta); + // Vertex positions are expressed in MESH space, so compute the corresponding mesh space position. + newLocInMeshSpace = mesh.ModelCoordsToMeshCoords(newLocModelSpace); + } + } + } + else + { + // If grid mode is not on and we are not snapping, allow for rotation of the selected face(s). + + // The point about which the face should be rotated, after the translation. + Vector3 rotationPivotModel = peltzerController.LastPositionModel; + // The model-space delta between the controller's current rotation and its rotation when the + // operation began. + Quaternion rotDelta = peltzerController.LastRotationModel * Quaternion.Inverse(reshapeBeginOrientation); + + // Move and rotate each vert by the positional and rotational delta. + MoveAndRotateVertexFreely(vertexKey, delta, rotationPivotModel, rotDelta, out newLocInMeshSpace); + } + movesByMesh[vertexKey.meshId][vertexKey.vertexId] = (new Vertex(vertexKey.vertexId, newLocInMeshSpace)); } - if (nearbyVertices != null && nearbyVertices.Count > 0) { - // Found a vertex that we should be snapping to. - newLocInMeshSpace = mesh.ModelCoordsToMeshCoords( - MeshMath.FindClosestVertex(nearbyVertices, oldLocInModelCoords + delta, model)); - } else { - // We have more than one vertex held, or the vertex we're holding was not snapped to another vertex. - // So, we move it by a snapped delta. - Vector3 newLocModelSpace = oldLocInModelCoords + GridUtils.SnapToGrid(delta); - // Vertex positions are expressed in MESH space, so compute the corresponding mesh space position. - newLocInMeshSpace = mesh.ModelCoordsToMeshCoords(newLocModelSpace); + // Update the vertex positions in naivelyMutatedMeshes. It's a "naive" operation because we just move + // vertices around without caring whether the movement is valid or not. It's not our job to care about + // this. Proper triangulation and cleanup will be done by the BackgroundMeshValidator. + List meshIds = new List(naivelyMutatedMeshes.Keys); + foreach (int meshId in meshIds) + { + MMesh mesh = naivelyMutatedMeshes[meshId]; + MMesh.GeometryOperation mutateOperation = mesh.StartOperation(); + mutateOperation.ModifyVertices(movesByMesh[mesh.id]); + mutateOperation.Commit(); } - } - } else { - // If grid mode is not on and we are not snapping, allow for rotation of the selected face(s). - - // The point about which the face should be rotated, after the translation. - Vector3 rotationPivotModel = peltzerController.LastPositionModel; - // The model-space delta between the controller's current rotation and its rotation when the - // operation began. - Quaternion rotDelta = peltzerController.LastRotationModel * Quaternion.Inverse(reshapeBeginOrientation); - - // Move and rotate each vert by the positional and rotational delta. - MoveAndRotateVertexFreely(vertexKey, delta, rotationPivotModel, rotDelta, out newLocInMeshSpace); } - movesByMesh[vertexKey.meshId][vertexKey.vertexId] = (new Vertex(vertexKey.vertexId, newLocInMeshSpace)); - } - - // Update the vertex positions in naivelyMutatedMeshes. It's a "naive" operation because we just move - // vertices around without caring whether the movement is valid or not. It's not our job to care about - // this. Proper triangulation and cleanup will be done by the BackgroundMeshValidator. - List meshIds = new List(naivelyMutatedMeshes.Keys); - foreach (int meshId in meshIds) { - MMesh mesh = naivelyMutatedMeshes[meshId]; - MMesh.GeometryOperation mutateOperation = mesh.StartOperation(); - mutateOperation.ModifyVertices(movesByMesh[mesh.id]); - mutateOperation.Commit(); - } - } - - /// - /// Moves and rotates a given vertex by the given model-space position and rotation deltas. - /// - private void MoveAndRotateVertexFreely(VertexKey vertexKey, Vector3 delta, Vector3 rotationPivotModel, - Quaternion rotationDelta, out Vector3 newLocationMeshSpace) { - MMesh mesh = model.GetMesh(vertexKey.meshId); - Vector3 oldLocationModelSpace = mesh.VertexPositionInModelCoords(vertexKey.vertexId); - Vector3 newLocationModelSpace = oldLocationModelSpace + delta; - - // Rotate the point about the requested pivot. - newLocationModelSpace = Math3d.RotatePointAroundPivot(newLocationModelSpace, rotationPivotModel, - rotationDelta); - - newLocationMeshSpace = mesh.ModelCoordsToMeshCoords(newLocationModelSpace); - } - - /// - /// Begin reshaping, if we have anything to reshape. - /// - private void MaybeStartOperation() { - if (selector.SelectedOrHoveredFaces().Count() > 0 - || selector.SelectedOrHoveredEdges().Count() > 0 - || selector.SelectedOrHoveredVertices().Count() > 0) { - StartMove(); - } - } - /// - /// An event handler that listens for controller input and delegates accordingly. - /// - /// The sender of the controller event. - /// The controller event arguments. - private void ControllerEventHandler(object sender, ControllerEventArgs args) { - if (peltzerController.mode != ControllerMode.reshape) - return; - - if (IsBeginOperationEvent(args)) { - // If we are about to operate on selected items, ensure the click is near those items. - if (selector.selectedEdges.Count > 0 || selector.selectedFaces.Count > 0 || selector.selectedVertices.Count > 0) { - if (!selector.ClickIsWithinCurrentSelection(peltzerController.LastPositionModel)) { - return; - } - } - triggerUpToRelease = false; - waitingToDetermineReleaseType = true; - triggerDownTime = Time.time; - MaybeStartOperation(); - } else if (IsCompleteSingleClickEvent(args)) { - waitingToDetermineReleaseType = false; - triggerUpToRelease = false; - } else if (IsReleaseEvent(args)) { - if (isSnapping) { - // We snapped while modifying, so we have learned a bit more about snapping. - completedSnaps++; + /// + /// Moves and rotates a given vertex by the given model-space position and rotation deltas. + /// + private void MoveAndRotateVertexFreely(VertexKey vertexKey, Vector3 delta, Vector3 rotationPivotModel, + Quaternion rotationDelta, out Vector3 newLocationMeshSpace) + { + MMesh mesh = model.GetMesh(vertexKey.meshId); + Vector3 oldLocationModelSpace = mesh.VertexPositionInModelCoords(vertexKey.vertexId); + Vector3 newLocationModelSpace = oldLocationModelSpace + delta; + + // Rotate the point about the requested pivot. + newLocationModelSpace = Math3d.RotatePointAroundPivot(newLocationModelSpace, rotationPivotModel, + rotationDelta); + + newLocationMeshSpace = mesh.ModelCoordsToMeshCoords(newLocationModelSpace); } - CompleteMove(); - } else if (IsStartSnapEvent(args) && !peltzerController.isBlockMode) { - // can only snap when grabbing one face, otherwise we'd have to resize faces. - if (IsReshapingFaces()) { - PeltzerMain.Instance.snappedWhenReshapingFaces = true; - } - if (completedSnaps < SNAP_KNOW_HOW_COUNT) { - PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); + + /// + /// Begin reshaping, if we have anything to reshape. + /// + private void MaybeStartOperation() + { + if (selector.SelectedOrHoveredFaces().Count() > 0 + || selector.SelectedOrHoveredEdges().Count() > 0 + || selector.SelectedOrHoveredVertices().Count() > 0) + { + StartMove(); + } } - isSnapping = true; - PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.alignSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - } else if (IsEndSnapEvent(args) && !peltzerController.isBlockMode) { - isSnapping = false; - if (isReshaping) { - // We snapped while modifying, so we have learned a bit more about snapping. - completedSnaps++; + + /// + /// An event handler that listens for controller input and delegates accordingly. + /// + /// The sender of the controller event. + /// The controller event arguments. + private void ControllerEventHandler(object sender, ControllerEventArgs args) + { + if (peltzerController.mode != ControllerMode.reshape) + return; + + if (IsBeginOperationEvent(args)) + { + // If we are about to operate on selected items, ensure the click is near those items. + if (selector.selectedEdges.Count > 0 || selector.selectedFaces.Count > 0 || selector.selectedVertices.Count > 0) + { + if (!selector.ClickIsWithinCurrentSelection(peltzerController.LastPositionModel)) + { + return; + } + } + triggerUpToRelease = false; + waitingToDetermineReleaseType = true; + triggerDownTime = Time.time; + MaybeStartOperation(); + } + else if (IsCompleteSingleClickEvent(args)) + { + waitingToDetermineReleaseType = false; + triggerUpToRelease = false; + } + else if (IsReleaseEvent(args)) + { + if (isSnapping) + { + // We snapped while modifying, so we have learned a bit more about snapping. + completedSnaps++; + } + CompleteMove(); + } + else if (IsStartSnapEvent(args) && !peltzerController.isBlockMode) + { + // can only snap when grabbing one face, otherwise we'd have to resize faces. + if (IsReshapingFaces()) + { + PeltzerMain.Instance.snappedWhenReshapingFaces = true; + } + if (completedSnaps < SNAP_KNOW_HOW_COUNT) + { + PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); + } + isSnapping = true; + PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.alignSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + } + else if (IsEndSnapEvent(args) && !peltzerController.isBlockMode) + { + isSnapping = false; + if (isReshaping) + { + // We snapped while modifying, so we have learned a bit more about snapping. + completedSnaps++; + } + PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); + } + else if (IsSetUpHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadUpAllowed) + { + SetHoverTooltip( + peltzerController.controllerGeometry.modifyTooltipUp, TouchpadHoverState.UP, args.TouchpadOverlay); + } + else if ( + IsSetLeftHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed) + { + SetHoverTooltip( + peltzerController.controllerGeometry.modifyTooltipLeft, TouchpadHoverState.LEFT, args.TouchpadOverlay); + } + else if (IsSetRightHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadRightAllowed) + { + SetHoverTooltip( + peltzerController.controllerGeometry.modifyTooltipRight, TouchpadHoverState.RIGHT, args.TouchpadOverlay); + } + else if (IsUnsetAllHoverTooltipsEvent(args)) + { + UnsetAllHoverTooltips(); + } } - PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); - } else if (IsSetUpHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadUpAllowed) { - SetHoverTooltip( - peltzerController.controllerGeometry.modifyTooltipUp, TouchpadHoverState.UP, args.TouchpadOverlay); - } else if ( - IsSetLeftHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed) { - SetHoverTooltip( - peltzerController.controllerGeometry.modifyTooltipLeft, TouchpadHoverState.LEFT, args.TouchpadOverlay); - } else if (IsSetRightHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadRightAllowed) { - SetHoverTooltip( - peltzerController.controllerGeometry.modifyTooltipRight, TouchpadHoverState.RIGHT, args.TouchpadOverlay); - } else if (IsUnsetAllHoverTooltipsEvent(args)) { - UnsetAllHoverTooltips(); - } - } - private void ModeChangeEventHandler(ControllerMode oldMode, ControllerMode newMode) { - if (oldMode != ControllerMode.reshape) return; + private void ModeChangeEventHandler(ControllerMode oldMode, ControllerMode newMode) + { + if (oldMode != ControllerMode.reshape) return; - if (isReshaping) { - CompleteMove(); - } + if (isReshaping) + { + CompleteMove(); + } - selector.TurnOffSelectIndicator(); - selector.ResetInactive(); - UnsetAllHoverTooltips(); + selector.TurnOffSelectIndicator(); + selector.ResetInactive(); + UnsetAllHoverTooltips(); + } } - } } diff --git a/Assets/Scripts/tools/Scaler.cs b/Assets/Scripts/tools/Scaler.cs index 070cf621..5cca280c 100644 --- a/Assets/Scripts/tools/Scaler.cs +++ b/Assets/Scripts/tools/Scaler.cs @@ -20,104 +20,121 @@ using com.google.apps.peltzer.client.model.util; using com.google.apps.peltzer.client.tools.utils; -namespace com.google.apps.peltzer.client.tools { - /// - /// Scales meshes. - /// - class Scaler { +namespace com.google.apps.peltzer.client.tools +{ /// - /// Scales a collection of meshes in place. + /// Scales meshes. /// - /// - /// The collection of meshes to scale. - /// - /// The factor used for scaling. - /// - /// True if the operation succeeded, false if scaling would have made any mesh too small. - /// If false, nothing is modified. - /// - public static bool TryScalingMeshes(List meshes, float scaleFactor) { - if (meshes.Count == 0) { - return false; - } + class Scaler + { + /// + /// Scales a collection of meshes in place. + /// + /// + /// The collection of meshes to scale. + /// + /// The factor used for scaling. + /// + /// True if the operation succeeded, false if scaling would have made any mesh too small. + /// If false, nothing is modified. + /// + public static bool TryScalingMeshes(List meshes, float scaleFactor) + { + if (meshes.Count == 0) + { + return false; + } - // Ensure that scaling down wouldn't take any mesh beneath the minimal grid size. - if (scaleFactor < 1) { - foreach (MMesh mesh in meshes) { - float maxSize = Mathf.Max(mesh.bounds.size.x, Mathf.Max(mesh.bounds.size.y, mesh.bounds.size.z)); - if (maxSize * scaleFactor < GridUtils.GRID_SIZE * 2) { - return false; - } - } - } + // Ensure that scaling down wouldn't take any mesh beneath the minimal grid size. + if (scaleFactor < 1) + { + foreach (MMesh mesh in meshes) + { + float maxSize = Mathf.Max(mesh.bounds.size.x, Mathf.Max(mesh.bounds.size.y, mesh.bounds.size.z)); + if (maxSize * scaleFactor < GridUtils.GRID_SIZE * 2) + { + return false; + } + } + } + + // Scale the vertices of each mesh by expanding or contracting its vertices relative to the center of the mesh. + Vector3 centroid = Math3d.FindCentroid(meshes); - // Scale the vertices of each mesh by expanding or contracting its vertices relative to the center of the mesh. - Vector3 centroid = Math3d.FindCentroid(meshes); + foreach (MMesh mesh in meshes) + { + MMesh.GeometryOperation scaleOperation = mesh.StartOperation(); + foreach (int vertexId in mesh.GetVertexIds()) + { + // Scale the vector, and move it towards/from the mesh origin by half of the size difference. + Vector3 loc = mesh.VertexPositionInMeshCoords(vertexId); + scaleOperation.ModifyVertexMeshSpace(vertexId, loc * scaleFactor); + } + // Uniform scale means this is safe + scaleOperation.CommitWithoutRecalculation(); + mesh.RecalcBounds(); + Vector3 offsetVector = (mesh.offset - centroid) * scaleFactor; + mesh.offset = centroid + (offsetVector.normalized * offsetVector.magnitude); + } - foreach (MMesh mesh in meshes) { - MMesh.GeometryOperation scaleOperation = mesh.StartOperation(); - foreach (int vertexId in mesh.GetVertexIds()) { - // Scale the vector, and move it towards/from the mesh origin by half of the size difference. - Vector3 loc = mesh.VertexPositionInMeshCoords(vertexId); - scaleOperation.ModifyVertexMeshSpace(vertexId, loc * scaleFactor); + return true; } - // Uniform scale means this is safe - scaleOperation.CommitWithoutRecalculation(); - mesh.RecalcBounds(); - Vector3 offsetVector = (mesh.offset - centroid) * scaleFactor; - mesh.offset = centroid + (offsetVector.normalized * offsetVector.magnitude); - } - return true; - } + /// + /// Given a list of MMeshes scales them down to fit within a desiredSize. + /// This is currently used by the PolyMenu to scale down previews and is model independent. + /// + /// The MMeshes to be scaled. + /// The scaled MMeshes. + public static List ScaleMeshes(List originalMeshes, float desiredSize) + { + Bounds bounds = new Bounds(); - /// - /// Given a list of MMeshes scales them down to fit within a desiredSize. - /// This is currently used by the PolyMenu to scale down previews and is model independent. - /// - /// The MMeshes to be scaled. - /// The scaled MMeshes. - public static List ScaleMeshes(List originalMeshes, float desiredSize) { - Bounds bounds = new Bounds(); + List meshes = new List(originalMeshes.Count); + // Create a clone of the meshes. We don't want to scale the actual meshes. + for (int i = 0; i < originalMeshes.Count; i++) + { + meshes.Add(originalMeshes[i].Clone()); + } - List meshes = new List(originalMeshes.Count); - // Create a clone of the meshes. We don't want to scale the actual meshes. - for (int i = 0; i < originalMeshes.Count; i++) { - meshes.Add(originalMeshes[i].Clone()); - } + List offsets = new List(meshes.Count()); + for (int i = 0; i < meshes.Count; i++) + { + offsets.Add(meshes[i].offset); - List offsets = new List(meshes.Count()); - for (int i = 0; i < meshes.Count; i++) { - offsets.Add(meshes[i].offset); + // Find the bounds of the meshes so that we can scale them. + if (bounds.size == Vector3.zero) + { + bounds = meshes[i].bounds; + } + else + { + bounds.Encapsulate(meshes[i].bounds); + } + } - // Find the bounds of the meshes so that we can scale them. - if (bounds.size == Vector3.zero) { - bounds = meshes[i].bounds; - } else { - bounds.Encapsulate(meshes[i].bounds); - } - } + Vector3 centroid = Math3d.FindCentroid(offsets); - Vector3 centroid = Math3d.FindCentroid(offsets); + float maxSize = Mathf.Max(bounds.size.x, Mathf.Max(bounds.size.y, bounds.size.z)); + float scaleFactor = desiredSize / maxSize; - float maxSize = Mathf.Max(bounds.size.x, Mathf.Max(bounds.size.y, bounds.size.z)); - float scaleFactor = desiredSize / maxSize; + // Scale the meshes to fit into the menu. + for (int i = 0; i < meshes.Count; i++) + { + Vector3 originalOffset = meshes[i].offset; + MMesh.GeometryOperation scaleOperation = meshes[i].StartOperation(); + foreach (Vertex vertex in meshes[i].GetVertices()) + { + // Scale the vector, and move it towards/from the mesh origin by half of the size difference. + scaleOperation.ModifyVertexMeshSpace(vertex.id, vertex.loc * scaleFactor); + } + // Uniform scale means normals are unaffected + scaleOperation.CommitWithoutRecalculation(); + Vector3 offsetVector = (originalOffset - centroid) * scaleFactor; + meshes[i].offset = centroid + (offsetVector.normalized * offsetVector.magnitude); + } - // Scale the meshes to fit into the menu. - for (int i = 0; i < meshes.Count; i++) { - Vector3 originalOffset = meshes[i].offset; - MMesh.GeometryOperation scaleOperation = meshes[i].StartOperation(); - foreach (Vertex vertex in meshes[i].GetVertices()) { - // Scale the vector, and move it towards/from the mesh origin by half of the size difference. - scaleOperation.ModifyVertexMeshSpace(vertex.id, vertex.loc * scaleFactor); + return meshes; } - // Uniform scale means normals are unaffected - scaleOperation.CommitWithoutRecalculation(); - Vector3 offsetVector = (originalOffset - centroid) * scaleFactor; - meshes[i].offset = centroid + (offsetVector.normalized * offsetVector.magnitude); - } - - return meshes; } - } } diff --git a/Assets/Scripts/tools/Selector.cs b/Assets/Scripts/tools/Selector.cs index 6237680b..7c5a6df6 100644 --- a/Assets/Scripts/tools/Selector.cs +++ b/Assets/Scripts/tools/Selector.cs @@ -24,1323 +24,1544 @@ using com.google.apps.peltzer.client.model.render; using com.google.apps.peltzer.client.model.util; -namespace com.google.apps.peltzer.client.tools { - /// - /// A tool that handles the hovering and selection of meshes, faces, or vertices. - /// - public class Selector : MonoBehaviour, IMeshRenderOwner { +namespace com.google.apps.peltzer.client.tools +{ /// - /// Options to customize the selector's selection logic. + /// A tool that handles the hovering and selection of meshes, faces, or vertices. /// - public struct SelectorOptions { - /// Whether meshes can be selected. - public bool includeMeshes; - /// Whether faces can be selected. - public bool includeFaces; - /// Whether edges can be selected. - public bool includeEdges; - /// Whether vertices can be selected. - public bool includeVertices; - /// - /// Whether to honor grouping when selecting meshes (if true, if the user selects one mesh in - /// a group, all the other meshes in the group will also be selected). - /// - public bool includeMeshGroups; - - public SelectorOptions(bool includeMeshes, bool includeFaces, bool includeEdges, bool includeVertices, - bool includeMeshGroups) { - this.includeMeshes = includeMeshes; - this.includeFaces = includeFaces; - this.includeEdges = includeEdges; - this.includeVertices = includeVertices; - this.includeMeshGroups = includeMeshGroups; - } - } - - // The types of selectable objects. - public enum Type { VERTEX, EDGE, FACE, MESH }; - - public static SelectorOptions NONE = new SelectorOptions(false, false, false, false, false); - public static SelectorOptions ALL = new SelectorOptions(true, true, true, true, true); - public static SelectorOptions MESHES_ONLY = new SelectorOptions(true, false, false, false, true); - public static SelectorOptions MESHES_ONLY_IGNORE_GROUPS = new SelectorOptions(true, false, false, false, false); - public static SelectorOptions FACES_ONLY = new SelectorOptions(false, true, false, false, false); - public static SelectorOptions VERTICES_ONLY = new SelectorOptions(false, false, false, true, false); - public static SelectorOptions EDGES_ONLY = new SelectorOptions(false, false, true, false, false); - public static SelectorOptions FACES_EDGES_AND_VERTICES = new SelectorOptions(false, true, true, true, false); - public static SelectorOptions FACES_AND_EDGES = new SelectorOptions(false, true, true, false, false); - public static SelectorOptions NOT_MESHES = new SelectorOptions(false, true, true, true, true); - public static SelectorOptions NOT_FACES = new SelectorOptions(true, false, true, true, true); - public static SelectorOptions NOT_EDGES = new SelectorOptions(true, true, false, true, true); - public static SelectorOptions NOT_VERTICES = new SelectorOptions(true, true, true, false, true); + public class Selector : MonoBehaviour, IMeshRenderOwner + { + /// + /// Options to customize the selector's selection logic. + /// + public struct SelectorOptions + { + /// Whether meshes can be selected. + public bool includeMeshes; + /// Whether faces can be selected. + public bool includeFaces; + /// Whether edges can be selected. + public bool includeEdges; + /// Whether vertices can be selected. + public bool includeVertices; + /// + /// Whether to honor grouping when selecting meshes (if true, if the user selects one mesh in + /// a group, all the other meshes in the group will also be selected). + /// + public bool includeMeshGroups; + + public SelectorOptions(bool includeMeshes, bool includeFaces, bool includeEdges, bool includeVertices, + bool includeMeshGroups) + { + this.includeMeshes = includeMeshes; + this.includeFaces = includeFaces; + this.includeEdges = includeEdges; + this.includeVertices = includeVertices; + this.includeMeshGroups = includeMeshGroups; + } + } - /// - /// Modes for which selection should be active. - /// - private static readonly List selectModes = new List() { + // The types of selectable objects. + public enum Type { VERTEX, EDGE, FACE, MESH }; + + public static SelectorOptions NONE = new SelectorOptions(false, false, false, false, false); + public static SelectorOptions ALL = new SelectorOptions(true, true, true, true, true); + public static SelectorOptions MESHES_ONLY = new SelectorOptions(true, false, false, false, true); + public static SelectorOptions MESHES_ONLY_IGNORE_GROUPS = new SelectorOptions(true, false, false, false, false); + public static SelectorOptions FACES_ONLY = new SelectorOptions(false, true, false, false, false); + public static SelectorOptions VERTICES_ONLY = new SelectorOptions(false, false, false, true, false); + public static SelectorOptions EDGES_ONLY = new SelectorOptions(false, false, true, false, false); + public static SelectorOptions FACES_EDGES_AND_VERTICES = new SelectorOptions(false, true, true, true, false); + public static SelectorOptions FACES_AND_EDGES = new SelectorOptions(false, true, true, false, false); + public static SelectorOptions NOT_MESHES = new SelectorOptions(false, true, true, true, true); + public static SelectorOptions NOT_FACES = new SelectorOptions(true, false, true, true, true); + public static SelectorOptions NOT_EDGES = new SelectorOptions(true, true, false, true, true); + public static SelectorOptions NOT_VERTICES = new SelectorOptions(true, true, true, false, true); + + /// + /// Modes for which selection should be active. + /// + private static readonly List selectModes = new List() { ControllerMode.extrude, ControllerMode.reshape, ControllerMode.move, ControllerMode.insertVolume, }; - /// - /// The scale of the dots for the multiselect trail. - /// - //private readonly Vector3 MULTISELECT_DOT_SCALE = new Vector3(0.0025f, 0.0025f, 0.0025f); - private readonly Vector3 MULTISELECT_DOT_SCALE = new Vector3(0.0125f, 0.0125f, 0.0125f); - /// - /// The scale of the indicator dot for the multiselect trail. - /// - private readonly Vector3 MULTISELECT_INDICATOR_SCALE = new Vector3(0.015f, 0.015f, 0.015f); - /// - /// The animation time for the multiselect indicator animation. - /// - private const float MULTISELECT_INDICATOR_REVEAL_TIME = 0.15f; - /// - /// Used to determine minimum spacing between dots in multiselect trail. - /// - private const float SPACING = 0.01f; - - // The size of the selection indicator - used both for display and for determining when it overlaps - // selectable geometry. - private const float SELECT_BALL_SIZE_WORLD = 0.005f; + /// + /// The scale of the dots for the multiselect trail. + /// + //private readonly Vector3 MULTISELECT_DOT_SCALE = new Vector3(0.0025f, 0.0025f, 0.0025f); + private readonly Vector3 MULTISELECT_DOT_SCALE = new Vector3(0.0125f, 0.0125f, 0.0125f); + /// + /// The scale of the indicator dot for the multiselect trail. + /// + private readonly Vector3 MULTISELECT_INDICATOR_SCALE = new Vector3(0.015f, 0.015f, 0.015f); + /// + /// The animation time for the multiselect indicator animation. + /// + private const float MULTISELECT_INDICATOR_REVEAL_TIME = 0.15f; + /// + /// Used to determine minimum spacing between dots in multiselect trail. + /// + private const float SPACING = 0.01f; + + // The size of the selection indicator - used both for display and for determining when it overlaps + // selectable geometry. + private const float SELECT_BALL_SIZE_WORLD = 0.005f; + + // How far away from the front of a face the edge of the selection indicator needs to be to select it. + private const float FACE_SELECTION_DISTANCE_WORLD = 0.000f; + // How far beneath a face the selection indicator can be and still select it. + private const float ADDITIONAL_FACE_DEPTH_DISTANCE = 0.015f; + // When we're only selecting faces, how far from a face we need to be to select it. + private const float SELECTION_THRESHOLD_FACES_ONLY = 0.015f; + private const float INCREASED_SELECTION_FACTOR = 4f; + + /// + /// A threshold above which a mesh is considered too far away to select. + /// This is in Unity units, where 1.0f = 1 meter by default. + /// + private const float MESH_CLOSENESS_THRESHOLD_DEFAULT = 0.1f; + /// + /// A threshold above which a face is considered too far away to select for Mesh selection. + /// This is in Unity units, where 1.0f = 1 meter by default. + /// + private const float FACE_CLOSENESS_THRESHOLD_DEFAULT = 0.015f; + /// + /// The threshold above which a face is considered too far away for the selector during mesh selection. + /// + public float faceClosenessThreshold = FACE_CLOSENESS_THRESHOLD_DEFAULT; + /// + /// The position in model coords the selector should be centered on. + /// + public Vector3 selectorPosition; + public ControllerMain controllerMain; + /// + /// A reference to a controller for getting position. + /// + private PeltzerController peltzerController; + /// + /// A reference to the overall model being built. + /// + private Model model; + /// + /// The spatial index of the model. + /// + private SpatialIndex spatialIndex; + + /// + /// The vertex currently being hovered over. + /// + public VertexKey hoverVertex { get; private set; } + /// + /// The face currently being hovered over, and its planar highlight. + /// + public FaceKey hoverFace { get; private set; } + /// + /// The face currently being hovered over. + /// + public EdgeKey hoverEdge { get; private set; } + + /// + /// The meshes currently being hovered over, and their mesh highlights. + /// + public HashSet hoverMeshes { get; private set; } + + /// + /// Temporary hashset - maintained as a global to avoid it getting GCed. Instead should manually clear after use. + /// + private HashSet tempRemovalHashset = new HashSet(); + + /// + /// The vertices selected in the most recent multi-select operation. + /// + public HashSet selectedVertices { get; private set; } + /// + /// The faces selected in the most recent multi-select operation, and planar highlights. + /// + public HashSet selectedFaces { get; private set; } + /// + /// The edges selected in the most recent multi-select operation. + /// + public HashSet selectedEdges { get; private set; } + /// + /// The meshes selected in the most recent multi-select operation, and mesh highlights. + /// + public HashSet selectedMeshes { get; private set; } + + private InactiveSelectionHighlighter inactiveSelectionHighlighter; + + private Bounds boundingBoxOfAllSelections; + + // Cached results of lookups from the spatial index. + int? nearestMesh; + List> nearbyFaces = new List>(); + List> nearbyEdges = new List>(); + List> nearbyVertices = new List>(); + + // Stacks for undoing multi-select. + private Stack undoVertexMultiSelect = new Stack(); + private Stack undoEdgeMultiSelect = new Stack(); + private Stack undoFaceMultiSelect = new Stack(); + private Stack undoMeshMultiSelect = new Stack(); + + // Stacks for redoing multi-select. + private Stack redoVertexMultiSelect = new Stack(); + private Stack redoEdgeMultiSelect = new Stack(); + private Stack redoFaceMultiSelect = new Stack(); + public Stack redoMeshMultiSelect { get; private set; } + + /// + /// Whether multi-select is currently enabled. + /// + private bool multiSelectEnabled = false; + /// + /// Whether multi-select is currently turned on. + /// + public bool isMultiSelecting { get; private set; } + + private WorldSpace worldSpace; + + /// + /// Parent game object to hold all "dots" of the multiselection trail. + /// + private GameObject multiselectTrail; + /// + /// The last dot placed - reference in support of animations. + /// + private GameObject lastDot; + /// + /// The position of the last dot placed. Used for calculating distance. + /// + private Vector3 lastDotPosition; + /// + /// The lifetime decay value for dots in the multitrail. + /// + private float multiselectTrailTime = 0.4f; + /// + /// The material for the dots in the multiselect trail. + /// + private Material multiselectDotMaterial; + /// + /// HighlightUtils for managing mesh highlights. + /// + private HighlightUtils highlightUtils; + + /// + /// Indicator showing the target position of selection. + /// + GameObject selectIndicator; + + /// + /// The radius in world coords of rendered points. This is pulled from the point rendering shader to ensure + /// that selection radii are visually consistent with it. + /// + public float pointRadiusWorld; + /// + /// The radius in world coords of rendered edges. This is pulled from the edge rendering shader to ensure + /// that selection radii are visually consistent with it. + /// + public float edgeRadiusWorld; + + // List of dots in multiselect trail. + List DOTS = new List(); + + // Mesh used for dots. + Mesh DOTSmesh; + + /// + /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. + /// As such, this setup method must be called before the tool is used for it to have a valid state. + /// + public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, + PaletteController paletteController, WorldSpace worldSpace, SpatialIndex spatialIndex, + HighlightUtils highlightUtils, MaterialLibrary materialLibrary) + { + this.model = model; + this.controllerMain = controllerMain; + this.peltzerController = peltzerController; + this.worldSpace = worldSpace; + this.spatialIndex = spatialIndex; + this.highlightUtils = highlightUtils; + this.inactiveSelectionHighlighter = new InactiveSelectionHighlighter(spatialIndex, highlightUtils, worldSpace, + model); + + controllerMain.ControllerActionHandler += ControllerEventHandler; + peltzerController.ModeChangedHandler += ModeChangeEventHandler; + + DOTSmesh = Resources.Load("Models/IcosphereSmall") as Mesh; + + hoverMeshes = new HashSet(); + hoverFace = null; + hoverEdge = null; + hoverVertex = null; + selectedMeshes = new HashSet(); + selectedFaces = new HashSet(); + selectedEdges = new HashSet(); + selectedVertices = new HashSet(); + + redoMeshMultiSelect = new Stack(); + + // Setup the multi-select components. + multiselectDotMaterial = materialLibrary.selectMaterial; + multiselectTrail = new GameObject("multiselectTrail"); + multiselectTrail.transform.position = -1f * worldSpace.WorldToModel(Vector3.zero); + multiSelectEnabled = selectModes.Contains(peltzerController.mode); + + this.edgeRadiusWorld = materialLibrary.edgeHighlightMaterial.GetFloat("_PointSphereRadius"); + this.pointRadiusWorld = materialLibrary.pointHighlightMaterial.GetFloat("_PointSphereRadius"); + } - // How far away from the front of a face the edge of the selection indicator needs to be to select it. - private const float FACE_SELECTION_DISTANCE_WORLD = 0.000f; - // How far beneath a face the selection indicator can be and still select it. - private const float ADDITIONAL_FACE_DEPTH_DISTANCE = 0.015f; - // When we're only selecting faces, how far from a face we need to be to select it. - private const float SELECTION_THRESHOLD_FACES_ONLY = 0.015f; - private const float INCREASED_SELECTION_FACTOR = 4f; + void Update() + { + // Handle selector animations. + // Multi-Selection + if (isMultiSelecting) + { + UpdateMultiselectionTrail(); + } + } - /// - /// A threshold above which a mesh is considered too far away to select. - /// This is in Unity units, where 1.0f = 1 meter by default. - /// - private const float MESH_CLOSENESS_THRESHOLD_DEFAULT = 0.1f; - /// - /// A threshold above which a face is considered too far away to select for Mesh selection. - /// This is in Unity units, where 1.0f = 1 meter by default. - /// - private const float FACE_CLOSENESS_THRESHOLD_DEFAULT = 0.015f; - /// - /// The threshold above which a face is considered too far away for the selector during mesh selection. - /// - public float faceClosenessThreshold = FACE_CLOSENESS_THRESHOLD_DEFAULT; - /// - /// The position in model coords the selector should be centered on. - /// - public Vector3 selectorPosition; - public ControllerMain controllerMain; - /// - /// A reference to a controller for getting position. - /// - private PeltzerController peltzerController; - /// - /// A reference to the overall model being built. - /// - private Model model; - /// - /// The spatial index of the model. - /// - private SpatialIndex spatialIndex; + public void TurnOnSelectIndicator() + { + if (selectIndicator == null) + { + selectIndicator = GameObject.CreatePrimitive(PrimitiveType.Sphere); + selectIndicator.name = "SelectIndicator"; + selectIndicator.GetComponent().material = multiselectDotMaterial; + selectIndicator.GetComponent().enabled = false; + selectIndicator.transform.localScale = new Vector3(SELECT_BALL_SIZE_WORLD, SELECT_BALL_SIZE_WORLD, SELECT_BALL_SIZE_WORLD); + } + selectIndicator.transform.position = peltzerController.wandTip.transform.position; + selectIndicator.transform.rotation = peltzerController.wandTip.transform.rotation; + } - /// - /// The vertex currently being hovered over. - /// - public VertexKey hoverVertex { get; private set; } - /// - /// The face currently being hovered over, and its planar highlight. - /// - public FaceKey hoverFace { get; private set; } - /// - /// The face currently being hovered over. - /// - public EdgeKey hoverEdge { get; private set; } - - /// - /// The meshes currently being hovered over, and their mesh highlights. - /// - public HashSet hoverMeshes { get; private set; } + public void TurnOffSelectIndicator() + { + if (selectIndicator != null) + { + GameObject.DestroyImmediate(selectIndicator); + } + } - /// - /// Temporary hashset - maintained as a global to avoid it getting GCed. Instead should manually clear after use. - /// - private HashSet tempRemovalHashset = new HashSet(); + /// + /// Every tool should call Select from Update to try to select instead of within Selector. + /// + /// The position to select at. + public void SelectAtPosition(Vector3 position, SelectorOptions options) + { + if (!PeltzerController.AcquireIfNecessary(ref peltzerController)) + { + return; + } - /// - /// The vertices selected in the most recent multi-select operation. - /// - public HashSet selectedVertices { get; private set; } - /// - /// The faces selected in the most recent multi-select operation, and planar highlights. - /// - public HashSet selectedFaces { get; private set; } - /// - /// The edges selected in the most recent multi-select operation. - /// - public HashSet selectedEdges { get; private set; } - /// - /// The meshes selected in the most recent multi-select operation, and mesh highlights. - /// - public HashSet selectedMeshes { get; private set; } - - private InactiveSelectionHighlighter inactiveSelectionHighlighter; - - private Bounds boundingBoxOfAllSelections; - - // Cached results of lookups from the spatial index. - int? nearestMesh; - List> nearbyFaces = new List>(); - List> nearbyEdges = new List>(); - List> nearbyVertices = new List>(); - - // Stacks for undoing multi-select. - private Stack undoVertexMultiSelect = new Stack(); - private Stack undoEdgeMultiSelect = new Stack(); - private Stack undoFaceMultiSelect = new Stack(); - private Stack undoMeshMultiSelect = new Stack(); - - // Stacks for redoing multi-select. - private Stack redoVertexMultiSelect = new Stack(); - private Stack redoEdgeMultiSelect = new Stack(); - private Stack redoFaceMultiSelect = new Stack(); - public Stack redoMeshMultiSelect { get; private set; } - - /// - /// Whether multi-select is currently enabled. - /// - private bool multiSelectEnabled = false; - /// - /// Whether multi-select is currently turned on. - /// - public bool isMultiSelecting { get; private set; } + // While the menu is being pointed at, no selection is possible. See bug for discussion. + if (peltzerController.isPointingAtMenu) + { + return; + } - private WorldSpace worldSpace; + selectorPosition = position; - /// - /// Parent game object to hold all "dots" of the multiselection trail. - /// - private GameObject multiselectTrail; - /// - /// The last dot placed - reference in support of animations. - /// - private GameObject lastDot; - /// - /// The position of the last dot placed. Used for calculating distance. - /// - private Vector3 lastDotPosition; - /// - /// The lifetime decay value for dots in the multitrail. - /// - private float multiselectTrailTime = 0.4f; - /// - /// The material for the dots in the multiselect trail. - /// - private Material multiselectDotMaterial; - /// - /// HighlightUtils for managing mesh highlights. - /// - private HighlightUtils highlightUtils; + // Start selection by finding all nearby elements if specified in SelectorOptions. + if (options.includeVertices && selectedEdges.Count == 0 && selectedFaces.Count == 0) + { + spatialIndex.FindVerticesClosestTo(selectorPosition, + (InactiveRenderer.GetVertScaleFactor(worldSpace) + SELECT_BALL_SIZE_WORLD) / worldSpace.scale, + out nearbyVertices); + } + else + { + nearbyVertices.Clear(); + } - /// - /// Indicator showing the target position of selection. - /// - GameObject selectIndicator; - /// - /// The radius in world coords of rendered points. This is pulled from the point rendering shader to ensure - /// that selection radii are visually consistent with it. - /// - public float pointRadiusWorld; - /// - /// The radius in world coords of rendered edges. This is pulled from the edge rendering shader to ensure - /// that selection radii are visually consistent with it. - /// - public float edgeRadiusWorld; + if (options.includeEdges && selectedVertices.Count == 0 && selectedFaces.Count == 0) + { + spatialIndex.FindEdgesClosestTo(selectorPosition, + (InactiveRenderer.GetEdgeScaleFactor(worldSpace) + SELECT_BALL_SIZE_WORLD) / worldSpace.scale, + /*ignoreInEdge*/ false, out nearbyEdges); + } + else + { + nearbyEdges.Clear(); + } - // List of dots in multiselect trail. - List DOTS = new List(); + if (options.includeFaces && selectedEdges.Count == 0 && selectedVertices.Count == 0) + { + spatialIndex.FindFacesClosestTo(selectorPosition, ADDITIONAL_FACE_DEPTH_DISTANCE / worldSpace.scale, + /*ignoreInFace*/ false, out nearbyFaces); + } + else + { + nearbyFaces.Clear(); + } - // Mesh used for dots. - Mesh DOTSmesh; + // Booleans used as out values indicating successful selection. + bool successfulSelectionVertex; + bool successfulSelectionEdge; + bool successfulSelectionFace; + + // If the user has already selected something, look only for other things of that type. + if (selectedVertices.Count > 0) + { + if (nearbyVertices.Count > 0) + { + TryHighlightingAVertex(nearbyVertices[0].value, out successfulSelectionVertex); + } + return; + } - /// - /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. - /// As such, this setup method must be called before the tool is used for it to have a valid state. - /// - public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, - PaletteController paletteController, WorldSpace worldSpace, SpatialIndex spatialIndex, - HighlightUtils highlightUtils, MaterialLibrary materialLibrary) { - this.model = model; - this.controllerMain = controllerMain; - this.peltzerController = peltzerController; - this.worldSpace = worldSpace; - this.spatialIndex = spatialIndex; - this.highlightUtils = highlightUtils; - this.inactiveSelectionHighlighter = new InactiveSelectionHighlighter(spatialIndex, highlightUtils, worldSpace, - model); - - controllerMain.ControllerActionHandler += ControllerEventHandler; - peltzerController.ModeChangedHandler += ModeChangeEventHandler; - - DOTSmesh = Resources.Load("Models/IcosphereSmall") as Mesh; - - hoverMeshes = new HashSet(); - hoverFace = null; - hoverEdge = null; - hoverVertex = null; - selectedMeshes = new HashSet(); - selectedFaces = new HashSet(); - selectedEdges = new HashSet(); - selectedVertices = new HashSet(); - - redoMeshMultiSelect = new Stack(); - - // Setup the multi-select components. - multiselectDotMaterial = materialLibrary.selectMaterial; - multiselectTrail = new GameObject("multiselectTrail"); - multiselectTrail.transform.position = -1f * worldSpace.WorldToModel(Vector3.zero); - multiSelectEnabled = selectModes.Contains(peltzerController.mode); - - this.edgeRadiusWorld = materialLibrary.edgeHighlightMaterial.GetFloat("_PointSphereRadius"); - this.pointRadiusWorld = materialLibrary.pointHighlightMaterial.GetFloat("_PointSphereRadius"); - } + if (selectedEdges.Count > 0) + { + if (nearbyEdges.Count > 0) + { + TryHighlightingAnEdge(nearbyEdges[0].value, out successfulSelectionEdge); + } + return; + } + if (selectedFaces.Count > 0) + { + if (nearbyFaces.Count > 0) + { + TryHighlightingAFace(nearbyFaces[0].value, position, out successfulSelectionFace); + } + return; + } - void Update() { - // Handle selector animations. - // Multi-Selection - if (isMultiSelecting) { - UpdateMultiselectionTrail(); - } - } + // Check for overlaps between the visible previews and the selection indicator. We use the visual radii + // of each to drive the selection in order to ensure that the selection is consistent with what the user + // sees and expects. + if (nearbyVertices.Count > 0 + && nearbyVertices[0].distance * worldSpace.scale < pointRadiusWorld + SELECT_BALL_SIZE_WORLD) + { + TryHighlightingAVertex(nearbyVertices[0].value, out successfulSelectionVertex); + if (successfulSelectionVertex) + { + // Upon vertex selection, clear all other undo stacks and all redo stacks to indicate a new multi-selection. + ClearMultiSelectUndoState(Type.VERTEX); + ClearMultiSelectRedoState(); + } + return; + } - public void TurnOnSelectIndicator() { - if(selectIndicator == null) { - selectIndicator = GameObject.CreatePrimitive(PrimitiveType.Sphere); - selectIndicator.name = "SelectIndicator"; - selectIndicator.GetComponent().material = multiselectDotMaterial; - selectIndicator.GetComponent().enabled = false; - selectIndicator.transform.localScale = new Vector3(SELECT_BALL_SIZE_WORLD, SELECT_BALL_SIZE_WORLD, SELECT_BALL_SIZE_WORLD); - } - selectIndicator.transform.position = peltzerController.wandTip.transform.position; - selectIndicator.transform.rotation = peltzerController.wandTip.transform.rotation; - } + // Same thing for edges. + if (nearbyEdges.Count > 0 + && nearbyEdges[0].distance * worldSpace.scale < edgeRadiusWorld + SELECT_BALL_SIZE_WORLD) + { + TryHighlightingAnEdge(nearbyEdges[0].value, out successfulSelectionEdge); + if (successfulSelectionEdge) + { + // Upon edge selection, clear all other undo stacks and all redo stacks to indicate a new multi-selection. + ClearMultiSelectUndoState(Type.EDGE); + ClearMultiSelectRedoState(); + } + return; + } - public void TurnOffSelectIndicator() { - if (selectIndicator != null) { - GameObject.DestroyImmediate(selectIndicator); - } - } + // Faces are different since they don't have a volumetric select-target rendered - instead we allow them + // to be "depthy" for the purposes of selection - giving them imaginary volume beneat the surface. + // This also allows the user to select faces by reaching a bit further into the mesh. + // In the event that face only selection is chosen, we increase the "in front of face" selection radius. We don't + // do this when edges and verts are selectable as this can cause slightly unintuitive behavior where the face + // hover is "lost" when the user hits an edge or a vert - it's better to teach users that they need to reach into + // the face to select it, as this is the best way to ensure a face is selected in a crowded environment. + float faceDistance = (!options.includeVertices && !options.includeEdges ? + SELECTION_THRESHOLD_FACES_ONLY : FACE_SELECTION_DISTANCE_WORLD) + SELECT_BALL_SIZE_WORLD; + if (nearbyFaces.Count > 0) + { + FaceInfo faceInfo = spatialIndex.GetFaceInfo(nearbyFaces[0].value); + float distanceToPlaneModel = faceInfo.plane.GetDistanceToPoint(selectorPosition); + float distanceToPlane = distanceToPlaneModel * worldSpace.scale; + if ((distanceToPlane < faceDistance) + || (distanceToPlane < 0f && -distanceToPlane < ADDITIONAL_FACE_DEPTH_DISTANCE)) + { + // Can do better if we can generate point on the edge of the poly that is closest and check distance + // but that's significantly harder, and will mostly be in positions where we'd prefer to select an edge. + if (Math3d.IsInside(faceInfo.border, selectorPosition - faceInfo.plane.normal * distanceToPlaneModel)) + { + TryHighlightingAFace(nearbyFaces[0].value, selectorPosition, out successfulSelectionFace); + if (successfulSelectionFace) + { + // Upon face selection, clear all other undo stacks and all redo stacks to indicate a new multi-selection. + ClearMultiSelectUndoState(Type.FACE); + ClearMultiSelectRedoState(); + } + return; + } + } + } - /// - /// Every tool should call Select from Update to try to select instead of within Selector. - /// - /// The position to select at. - public void SelectAtPosition(Vector3 position, SelectorOptions options) { - if (!PeltzerController.AcquireIfNecessary(ref peltzerController)) { - return; - } - - // While the menu is being pointed at, no selection is possible. See bug for discussion. - if (peltzerController.isPointingAtMenu) { - return; - } - - selectorPosition = position; - - // Start selection by finding all nearby elements if specified in SelectorOptions. - if (options.includeVertices && selectedEdges.Count == 0 && selectedFaces.Count == 0) { - spatialIndex.FindVerticesClosestTo(selectorPosition, - (InactiveRenderer.GetVertScaleFactor(worldSpace) + SELECT_BALL_SIZE_WORLD) / worldSpace.scale, - out nearbyVertices); - } else { - nearbyVertices.Clear(); - } - - - if (options.includeEdges && selectedVertices.Count == 0 && selectedFaces.Count == 0) { - spatialIndex.FindEdgesClosestTo(selectorPosition, - (InactiveRenderer.GetEdgeScaleFactor(worldSpace) + SELECT_BALL_SIZE_WORLD) / worldSpace.scale, - /*ignoreInEdge*/ false, out nearbyEdges); - } else { - nearbyEdges.Clear(); - } - - if (options.includeFaces && selectedEdges.Count == 0 && selectedVertices.Count == 0) { - spatialIndex.FindFacesClosestTo(selectorPosition, ADDITIONAL_FACE_DEPTH_DISTANCE / worldSpace.scale, - /*ignoreInFace*/ false, out nearbyFaces); - } else { - nearbyFaces.Clear(); - } - - // Booleans used as out values indicating successful selection. - bool successfulSelectionVertex; - bool successfulSelectionEdge; - bool successfulSelectionFace; - - // If the user has already selected something, look only for other things of that type. - if (selectedVertices.Count > 0) { - if (nearbyVertices.Count > 0) { - TryHighlightingAVertex(nearbyVertices[0].value, out successfulSelectionVertex); + // We didn't select anything. Clear any previous hover highlights. + Deselect(ALL, /*deselectSelectedHighlights*/ false, /*deselectHoveredhighlights*/ true); } - return; - } - if (selectedEdges.Count > 0) { - if (nearbyEdges.Count > 0) { - TryHighlightingAnEdge(nearbyEdges[0].value, out successfulSelectionEdge); - } - return; - } - if (selectedFaces.Count > 0) { - if (nearbyFaces.Count > 0) { - TryHighlightingAFace(nearbyFaces[0].value, position, out successfulSelectionFace); - } - return; - } - - // Check for overlaps between the visible previews and the selection indicator. We use the visual radii - // of each to drive the selection in order to ensure that the selection is consistent with what the user - // sees and expects. - if (nearbyVertices.Count > 0 - && nearbyVertices[0].distance * worldSpace.scale < pointRadiusWorld + SELECT_BALL_SIZE_WORLD) { - TryHighlightingAVertex(nearbyVertices[0].value, out successfulSelectionVertex); - if (successfulSelectionVertex) { - // Upon vertex selection, clear all other undo stacks and all redo stacks to indicate a new multi-selection. - ClearMultiSelectUndoState(Type.VERTEX); - ClearMultiSelectRedoState(); + /// + /// Every tool should call Select from Update to try to select instead of within Selector. + /// + /// The position to select at. + /// For use in special cases when we need to force selection. Usually we + /// don't, so we default to false. + public void SelectMeshAtPosition(Vector3 position, SelectorOptions options, bool forceSelection = false) + { + if (!PeltzerController.AcquireIfNecessary(ref peltzerController)) + { + return; + } + + // While the menu is being pointed at, no selection is possible. See bug for discussion. + if (peltzerController.isPointingAtMenu) + { + return; + } + + selectorPosition = position; + // Mesh selection is just face selection but we will highlight the mesh the face belongs to if + // options.includeMeshes == true; + + float selectionThreshold = + isMultiSelecting && PeltzerMain.Instance.restrictionManager.increasedMultiSelectRadiusAllowed ? + faceClosenessThreshold * INCREASED_SELECTION_FACTOR : + faceClosenessThreshold; + + spatialIndex.FindFacesClosestTo(selectorPosition, selectionThreshold / worldSpace.scale, + /*ignoreInFace*/ false, out nearbyFaces); + + bool successfulSelectionMesh; + + if (nearbyFaces.Count > 0) + { + TryHighlightingAMesh(nearbyFaces[0].value.meshId, options.includeMeshGroups, forceSelection, out successfulSelectionMesh); + if (successfulSelectionMesh) + { + // Upon mesh selection, clear all other undo stacks and all redo stacks to indicate a new multi-selection. + ClearMultiSelectUndoState(Type.MESH); + ClearMultiSelectRedoState(); + } + return; + } + else if (!isMultiSelecting || forceSelection) + { + selectionThreshold = MESH_CLOSENESS_THRESHOLD_DEFAULT; + + // If we are not multiselecting, we can afford to be a bit more flexible so that the user can + // easily grab a mesh even though they are not hovering near any of its faces (for example, grab + // a mesh from inside). If we are multiselecting, however, we don't want this behavior because it + // might cause large objects to get accidentally selected while the user is trying to multiselect + // smaller objects near it (see bug). + int? nearestMesh = null; + if (spatialIndex.FindNearestMeshTo(selectorPosition, selectionThreshold / worldSpace.scale, out nearestMesh)) + { + // If we didn't find any nearby faces, but have a nearby mesh, we'll highlight that. + TryHighlightingAMesh(nearestMesh.Value, options.includeMeshGroups, forceSelection, out successfulSelectionMesh); + return; + } + } + + // We didn't select anything. Clear any previous hover highlights. + Deselect(MESHES_ONLY, /*deselectSelectedHighlights*/ false, /*deselectHoveredhighlights*/ true); } - return; - } - - // Same thing for edges. - if (nearbyEdges.Count > 0 - && nearbyEdges[0].distance * worldSpace.scale < edgeRadiusWorld + SELECT_BALL_SIZE_WORLD) { - TryHighlightingAnEdge(nearbyEdges[0].value, out successfulSelectionEdge); - if (successfulSelectionEdge) { - // Upon edge selection, clear all other undo stacks and all redo stacks to indicate a new multi-selection. - ClearMultiSelectUndoState(Type.EDGE); - ClearMultiSelectRedoState(); + + public void ResetInactive() + { + inactiveSelectionHighlighter.TurnOffVertsEdges(); } - return; - } - - // Faces are different since they don't have a volumetric select-target rendered - instead we allow them - // to be "depthy" for the purposes of selection - giving them imaginary volume beneat the surface. - // This also allows the user to select faces by reaching a bit further into the mesh. - // In the event that face only selection is chosen, we increase the "in front of face" selection radius. We don't - // do this when edges and verts are selectable as this can cause slightly unintuitive behavior where the face - // hover is "lost" when the user hits an edge or a vert - it's better to teach users that they need to reach into - // the face to select it, as this is the best way to ensure a face is selected in a crowded environment. - float faceDistance = (!options.includeVertices && !options.includeEdges ? - SELECTION_THRESHOLD_FACES_ONLY : FACE_SELECTION_DISTANCE_WORLD) + SELECT_BALL_SIZE_WORLD; - if (nearbyFaces.Count > 0) { - FaceInfo faceInfo = spatialIndex.GetFaceInfo(nearbyFaces[0].value); - float distanceToPlaneModel = faceInfo.plane.GetDistanceToPoint(selectorPosition); - float distanceToPlane = distanceToPlaneModel * worldSpace.scale; - if ((distanceToPlane < faceDistance) - || (distanceToPlane < 0f && -distanceToPlane < ADDITIONAL_FACE_DEPTH_DISTANCE)) { - // Can do better if we can generate point on the edge of the poly that is closest and check distance - // but that's significantly harder, and will mostly be in positions where we'd prefer to select an edge. - if (Math3d.IsInside(faceInfo.border, selectorPosition - faceInfo.plane.normal * distanceToPlaneModel)) { - TryHighlightingAFace(nearbyFaces[0].value, selectorPosition, out successfulSelectionFace); - if (successfulSelectionFace) { - // Upon face selection, clear all other undo stacks and all redo stacks to indicate a new multi-selection. - ClearMultiSelectUndoState(Type.FACE); - ClearMultiSelectRedoState(); - } - return; - } + + public void UpdateInactive(SelectorOptions options) + { + if (selectedFaces.Count == 0) + { + if (selectedVertices.Count == 0 && selectedEdges.Count == 0) + { + if (options.includeEdges) + { + if (options.includeVertices) + { + inactiveSelectionHighlighter.ShowSelectableVertsEdgesNear(selectorPosition, selectedVertices, hoverVertex, + selectedEdges, hoverEdge); + } + else + { + inactiveSelectionHighlighter.ShowSelectableEdgesNear(selectorPosition, selectedEdges, hoverEdge); + } + } + else if (options.includeVertices) + { + inactiveSelectionHighlighter.ShowSelectableVertsNear(selectorPosition, selectedVertices, hoverVertex); + } + } + else if (selectedVertices.Count > 0 && options.includeVertices) + { + inactiveSelectionHighlighter.ShowSelectableVertsNear(selectorPosition, selectedVertices, hoverVertex); + } + else if (selectedEdges.Count > 0 && options.includeEdges) + { + inactiveSelectionHighlighter.ShowSelectableEdgesNear(selectorPosition, selectedEdges, hoverEdge); + } + } + else if (selectedFaces.Count > 0) + { + ResetInactive(); + } } - } - // We didn't select anything. Clear any previous hover highlights. - Deselect(ALL, /*deselectSelectedHighlights*/ false, /*deselectHoveredhighlights*/ true); - } + /// + /// Returns an enumeration of: + /// - Selected vertices, if any; or + /// - Hovered vertices, if any; or + /// - Nothing (an empty enumeration). + /// + public IEnumerable SelectedOrHoveredVertices() + { + if (selectedVertices.Count > 0) + { + return selectedVertices; + } + else + { + // If there are no hovered items, this will return the empty enumeration, which is our intention. + return hoverVertex == null ? + new List() : new List { hoverVertex }; + } + } - /// - /// Every tool should call Select from Update to try to select instead of within Selector. - /// - /// The position to select at. - /// For use in special cases when we need to force selection. Usually we - /// don't, so we default to false. - public void SelectMeshAtPosition(Vector3 position, SelectorOptions options, bool forceSelection = false) { - if (!PeltzerController.AcquireIfNecessary(ref peltzerController)) { - return; - } - - // While the menu is being pointed at, no selection is possible. See bug for discussion. - if (peltzerController.isPointingAtMenu) { - return; - } - - selectorPosition = position; - // Mesh selection is just face selection but we will highlight the mesh the face belongs to if - // options.includeMeshes == true; - - float selectionThreshold = - isMultiSelecting && PeltzerMain.Instance.restrictionManager.increasedMultiSelectRadiusAllowed ? - faceClosenessThreshold * INCREASED_SELECTION_FACTOR : - faceClosenessThreshold; - - spatialIndex.FindFacesClosestTo(selectorPosition, selectionThreshold / worldSpace.scale, - /*ignoreInFace*/ false, out nearbyFaces); - - bool successfulSelectionMesh; - - if (nearbyFaces.Count > 0) { - TryHighlightingAMesh(nearbyFaces[0].value.meshId, options.includeMeshGroups, forceSelection, out successfulSelectionMesh); - if (successfulSelectionMesh) { - // Upon mesh selection, clear all other undo stacks and all redo stacks to indicate a new multi-selection. - ClearMultiSelectUndoState(Type.MESH); - ClearMultiSelectRedoState(); + /// + /// Returns an enumeration of: + /// - Selected edges, if any; or + /// - Hovered edges, if any; or + /// - Nothing (an empty enumeration). + /// + public IEnumerable SelectedOrHoveredEdges() + { + if (selectedEdges.Count > 0) + { + return selectedEdges; + } + else + { + // If there are no hovered items, this will return the empty enumeration, which is our intention. + return hoverEdge == null ? + new List() : new List { hoverEdge }; + } } - return; - } else if (!isMultiSelecting || forceSelection) { - selectionThreshold = MESH_CLOSENESS_THRESHOLD_DEFAULT; - - // If we are not multiselecting, we can afford to be a bit more flexible so that the user can - // easily grab a mesh even though they are not hovering near any of its faces (for example, grab - // a mesh from inside). If we are multiselecting, however, we don't want this behavior because it - // might cause large objects to get accidentally selected while the user is trying to multiselect - // smaller objects near it (see bug). - int? nearestMesh = null; - if (spatialIndex.FindNearestMeshTo(selectorPosition, selectionThreshold / worldSpace.scale, out nearestMesh)) { - // If we didn't find any nearby faces, but have a nearby mesh, we'll highlight that. - TryHighlightingAMesh(nearestMesh.Value, options.includeMeshGroups, forceSelection, out successfulSelectionMesh); - return; + + /// + /// Returns an enumeration of: + /// - Selected faces, if any; or + /// - Hovered faces, if any; or + /// - Nothing (an empty enumeration). + /// + public IEnumerable SelectedOrHoveredFaces() + { + if (selectedFaces.Count > 0) + { + return selectedFaces; + } + else + { + // If there are no hovered items, this will return the empty enumeration, which is our intention. + return hoverFace == null ? + new List() : new List { hoverFace }; + } } - } - // We didn't select anything. Clear any previous hover highlights. - Deselect(MESHES_ONLY, /*deselectSelectedHighlights*/ false, /*deselectHoveredhighlights*/ true); - } + /// + /// Returns an enumeration of: + /// - Selected meshes, if any; or + /// - Hovered meshes, if any; or + /// - Nothing (an empty enumeration). + /// + public IEnumerable SelectedOrHoveredMeshes() + { + if (selectedMeshes.Count > 0) + { + return selectedMeshes; + } + else + { + // If there are no hovered items, this will return the empty enumeration, which is our intention. + return hoverMeshes; + } + } - public void ResetInactive() { - inactiveSelectionHighlighter.TurnOffVertsEdges(); - } - public void UpdateInactive(SelectorOptions options) { - if (selectedFaces.Count == 0) { - if (selectedVertices.Count == 0 && selectedEdges.Count == 0) { - if (options.includeEdges) { - if (options.includeVertices) { - inactiveSelectionHighlighter.ShowSelectableVertsEdgesNear(selectorPosition, selectedVertices, hoverVertex, - selectedEdges, hoverEdge); - } else { - inactiveSelectionHighlighter.ShowSelectableEdgesNear(selectorPosition, selectedEdges, hoverEdge); - } - } else if (options.includeVertices) { - inactiveSelectionHighlighter.ShowSelectableVertsNear(selectorPosition, selectedVertices, hoverVertex); - } - } else if (selectedVertices.Count > 0 && options.includeVertices) { - inactiveSelectionHighlighter.ShowSelectableVertsNear(selectorPosition, selectedVertices, hoverVertex); - } else if (selectedEdges.Count > 0 && options.includeEdges) { - inactiveSelectionHighlighter.ShowSelectableEdgesNear(selectorPosition, selectedEdges, hoverEdge); + /// + /// Reset state to prepare for the given controller mode. + /// + /// The new mode. + private void ModeChangeEventHandler(ControllerMode oldMode, ControllerMode newMode) + { + if ((oldMode == ControllerMode.extrude && newMode == ControllerMode.reshape) || + (oldMode == ControllerMode.reshape && newMode == ControllerMode.extrude)) + { + // The user is switching between reshape/extrude: preserve any selected faces. + Deselect(NOT_FACES, /*deselectSelectedHighlights*/ true, /*deselectHoveredhighlights*/ true); + } + else + { + // The user is switching between any other modes: remove any existing selections or highlights. + Deselect(ALL, /*deselectSelectedHighlights*/ true, /*deselectHoveredhighlights*/ true); + } + multiSelectEnabled = selectModes.Contains(newMode); } - } else if (selectedFaces.Count > 0) { - ResetInactive(); - } - } - /// - /// Returns an enumeration of: - /// - Selected vertices, if any; or - /// - Hovered vertices, if any; or - /// - Nothing (an empty enumeration). - /// - public IEnumerable SelectedOrHoveredVertices() { - if (selectedVertices.Count > 0) { - return selectedVertices; - } else { - // If there are no hovered items, this will return the empty enumeration, which is our intention. - return hoverVertex == null ? - new List() : new List { hoverVertex }; - } - } + /// + /// If includeGroups is true, selecting one mesh will also select its group mates (meshes in the same group). + /// + /// For use in special cases when we need to force selection. + /// + /// Tells us if a mesh has been successfully selected or not. An out parameter was chosen instead of changing + /// the return type of the method to bool because the method also handles mesh hovering, whereas this bool value + /// only deals with selection. + /// + private void TryHighlightingAMesh(int nearestMeshId, bool includeGroups, bool forceSelection, out bool successfulSelection) + { + // Default assumption that we don't successfully select. + successfulSelection = false; + + // Quick check for tutorial mode restrictions. + if (PeltzerMain.Instance.restrictionManager.onlySelectableMeshIdForTutorial.HasValue + && nearestMeshId != PeltzerMain.Instance.restrictionManager.onlySelectableMeshIdForTutorial) + { + // No mesh was selected due to tutorial restrictions. + successfulSelection = false; + return; + } - /// - /// Returns an enumeration of: - /// - Selected edges, if any; or - /// - Hovered edges, if any; or - /// - Nothing (an empty enumeration). - /// - public IEnumerable SelectedOrHoveredEdges() { - if (selectedEdges.Count > 0) { - return selectedEdges; - } else { - // If there are no hovered items, this will return the empty enumeration, which is our intention. - return hoverEdge == null ? - new List() : new List { hoverEdge }; - } - } + // Add the mesh to touched meshes. + HashSet touchedMeshIds = new HashSet { nearestMeshId }; - /// - /// Returns an enumeration of: - /// - Selected faces, if any; or - /// - Hovered faces, if any; or - /// - Nothing (an empty enumeration). - /// - public IEnumerable SelectedOrHoveredFaces() { - if (selectedFaces.Count > 0) { - return selectedFaces; - } else { - // If there are no hovered items, this will return the empty enumeration, which is our intention. - return hoverFace == null ? - new List() : new List { hoverFace }; - } - } + if (includeGroups) + { + // Expand the list of touched meshes to include all the meshes in the their groups. + // Note: GetMeshesAndGroupMates() will mutate touchedMeshIds in-place (for efficiency). + model.ExpandMeshIdsToGroupMates(touchedMeshIds); + } - /// - /// Returns an enumeration of: - /// - Selected meshes, if any; or - /// - Hovered meshes, if any; or - /// - Nothing (an empty enumeration). - /// - public IEnumerable SelectedOrHoveredMeshes() { - if (selectedMeshes.Count > 0) { - return selectedMeshes; - } else { - // If there are no hovered items, this will return the empty enumeration, which is our intention. - return hoverMeshes; - } - } + // In multi-select mode, append any newly-hovered meshes to the selected list. This is also the functionality we want + // when forcing click to select functionality. + if (isMultiSelecting || forceSelection) + { + // Add only the nearest mesh id to the undo stack to avoid bugs with grouped objects. + // When click to select is enabled, the mesh is hidden on hover, but we still want to add it to the undo stack. + if ((!model.IsMeshHidden(nearestMeshId) || forceSelection) && !selectedMeshes.Contains(nearestMeshId)) + { + undoMeshMultiSelect.Push(nearestMeshId); + } + foreach (int meshId in touchedMeshIds) + { + // Or case is needed because for click to select, the mesh is highlighted and therefore hidden. + if ((!model.IsMeshHidden(meshId) || forceSelection) && !selectedMeshes.Contains(meshId)) + { + SelectMesh(meshId); + // We're guaranteed to have selected a mesh if the selected meshes don't contain the current mesh and it's not hidden. + successfulSelection = true; + peltzerController.TriggerHapticFeedback(HapticFeedback.HapticFeedbackType.FEEDBACK_1, + /* durationSeconds */ 0.03f, /* strength */ 0.15f); + } + } + return; + } + // If we are not multi-selecting and another mesh is already selected, we are not going to select again. + if (selectedMeshes.Count > 0) + { + return; + } - /// - /// Reset state to prepare for the given controller mode. - /// - /// The new mode. - private void ModeChangeEventHandler(ControllerMode oldMode, ControllerMode newMode) { - if ((oldMode == ControllerMode.extrude && newMode == ControllerMode.reshape) || - (oldMode == ControllerMode.reshape && newMode == ControllerMode.extrude)) { - // The user is switching between reshape/extrude: preserve any selected faces. - Deselect(NOT_FACES, /*deselectSelectedHighlights*/ true, /*deselectHoveredhighlights*/ true); - } else { - // The user is switching between any other modes: remove any existing selections or highlights. - Deselect(ALL, /*deselectSelectedHighlights*/ true, /*deselectHoveredhighlights*/ true); - } - multiSelectEnabled = selectModes.Contains(newMode); - } + // Clear any existing single-select hover highlights that aren't meshes. + Deselect(NOT_MESHES, /*deselectSelectedHighlights*/ false, /*deselectHoveredHighlights*/ true); + + // In single-select mode, create highlights for every newlyHoveredMesh. You can only hover a single element, but + // if we hover a single mesh in a group of meshes we need to treat them like one element. + foreach (int meshId in touchedMeshIds) + { + if (!hoverMeshes.Contains(meshId)) + { + int canClaimMesh = model.ClaimMesh(meshId, this); + if (canClaimMesh != -1) + { + hoverMeshes.Add(meshId); + highlightUtils.TurnOnMesh(meshId); + if (Features.vibrateOnHover) + { + peltzerController.TriggerHapticFeedback(HapticFeedback.HapticFeedbackType.FEEDBACK_1, + /* durationSeconds */ 0.02f, /* strength */ 0.15f); + } + } + } + } - /// - /// If includeGroups is true, selecting one mesh will also select its group mates (meshes in the same group). - /// - /// For use in special cases when we need to force selection. - /// - /// Tells us if a mesh has been successfully selected or not. An out parameter was chosen instead of changing - /// the return type of the method to bool because the method also handles mesh hovering, whereas this bool value - /// only deals with selection. - /// - private void TryHighlightingAMesh(int nearestMeshId, bool includeGroups, bool forceSelection, out bool successfulSelection) { - // Default assumption that we don't successfully select. - successfulSelection = false; - - // Quick check for tutorial mode restrictions. - if (PeltzerMain.Instance.restrictionManager.onlySelectableMeshIdForTutorial.HasValue - && nearestMeshId != PeltzerMain.Instance.restrictionManager.onlySelectableMeshIdForTutorial) { - // No mesh was selected due to tutorial restrictions. - successfulSelection = false; - return; - } - - // Add the mesh to touched meshes. - HashSet touchedMeshIds = new HashSet { nearestMeshId }; - - if (includeGroups) { - // Expand the list of touched meshes to include all the meshes in the their groups. - // Note: GetMeshesAndGroupMates() will mutate touchedMeshIds in-place (for efficiency). - model.ExpandMeshIdsToGroupMates(touchedMeshIds); - } - - // In multi-select mode, append any newly-hovered meshes to the selected list. This is also the functionality we want - // when forcing click to select functionality. - if (isMultiSelecting || forceSelection) { - // Add only the nearest mesh id to the undo stack to avoid bugs with grouped objects. - // When click to select is enabled, the mesh is hidden on hover, but we still want to add it to the undo stack. - if ((!model.IsMeshHidden(nearestMeshId) || forceSelection) && !selectedMeshes.Contains(nearestMeshId)) { - undoMeshMultiSelect.Push(nearestMeshId); - } - foreach (int meshId in touchedMeshIds) { - // Or case is needed because for click to select, the mesh is highlighted and therefore hidden. - if ((!model.IsMeshHidden(meshId) || forceSelection) && !selectedMeshes.Contains(meshId)) { - SelectMesh(meshId); - // We're guaranteed to have selected a mesh if the selected meshes don't contain the current mesh and it's not hidden. - successfulSelection = true; - peltzerController.TriggerHapticFeedback(HapticFeedback.HapticFeedbackType.FEEDBACK_1, - /* durationSeconds */ 0.03f, /* strength */ 0.15f); - } - } - return; - } - - // If we are not multi-selecting and another mesh is already selected, we are not going to select again. - if (selectedMeshes.Count > 0) { - return; - } - - // Clear any existing single-select hover highlights that aren't meshes. - Deselect(NOT_MESHES, /*deselectSelectedHighlights*/ false, /*deselectHoveredHighlights*/ true); - - // In single-select mode, create highlights for every newlyHoveredMesh. You can only hover a single element, but - // if we hover a single mesh in a group of meshes we need to treat them like one element. - foreach (int meshId in touchedMeshIds) { - if (!hoverMeshes.Contains(meshId)) { - int canClaimMesh = model.ClaimMesh(meshId, this); - if (canClaimMesh != -1) { - hoverMeshes.Add(meshId); - highlightUtils.TurnOnMesh(meshId); - if (Features.vibrateOnHover) { - peltzerController.TriggerHapticFeedback(HapticFeedback.HapticFeedbackType.FEEDBACK_1, - /* durationSeconds */ 0.02f, /* strength */ 0.15f); + + // In single-select mode, destroy highlights for, and unhide, any unhovered meshes. + foreach (int meshId in hoverMeshes) + { + if (!touchedMeshIds.Contains(meshId)) + { + highlightUtils.TurnOffMesh(meshId); + model.RelinquishMesh(meshId, this); + tempRemovalHashset.Add(meshId); + } + } + foreach (int meshId in tempRemovalHashset) + { + hoverMeshes.Remove(meshId); } - } + tempRemovalHashset.Clear(); } - } - - // In single-select mode, destroy highlights for, and unhide, any unhovered meshes. - foreach (int meshId in hoverMeshes) { - if (!touchedMeshIds.Contains(meshId)) { - highlightUtils.TurnOffMesh(meshId); - model.RelinquishMesh(meshId, this); - tempRemovalHashset.Add(meshId); + /// + /// Deselects a mesh. Handles grouped meshes as well. + /// + /// (meshes in the same group). + private void DeselectMesh(int meshIdToDeselect) + { + // Add the mesh to touched meshes. + HashSet meshIds = new HashSet { meshIdToDeselect }; + + // Expand the list of touched meshes to include all the meshes in their groups. + // Note: GetMeshesAndGroupMates() will mutate touchedMeshIds in-place (for efficiency). + model.ExpandMeshIdsToGroupMates(meshIds); + + foreach (int meshId in meshIds) + { + if (selectedMeshes.Contains(meshId)) + { + DeselectOneMesh(meshId); + } + } } - } - foreach (int meshId in tempRemovalHashset) { - hoverMeshes.Remove(meshId); - } - tempRemovalHashset.Clear(); - } - /// - /// Deselects a mesh. Handles grouped meshes as well. - /// - /// (meshes in the same group). - private void DeselectMesh(int meshIdToDeselect) { - // Add the mesh to touched meshes. - HashSet meshIds = new HashSet { meshIdToDeselect }; - - // Expand the list of touched meshes to include all the meshes in their groups. - // Note: GetMeshesAndGroupMates() will mutate touchedMeshIds in-place (for efficiency). - model.ExpandMeshIdsToGroupMates(meshIds); - - foreach (int meshId in meshIds) { - if (selectedMeshes.Contains(meshId)) { - DeselectOneMesh(meshId); + /// + /// Claim responsibility for rendering a mesh from this class. + /// This should only be called by Model, as otherwise Model's knowledge of current ownership will be incorrect. + /// + public int ClaimMesh(int meshId, IMeshRenderOwner fosterRenderer) + { + if (selectedMeshes.Contains(meshId)) + { + highlightUtils.TurnOffMesh(meshId); + selectedMeshes.Remove(meshId); + PeltzerMain.Instance.SetSaveSelectedButtonActiveIfSelectionNotEmpty(); + return meshId; + } + if (hoverMeshes.Contains(meshId)) + { + highlightUtils.TurnOffMesh(meshId); + hoverMeshes.Remove(meshId); + return meshId; + } + // Didn't have it, can't relinquish ownership. + return -1; } - } - } - /// - /// Claim responsibility for rendering a mesh from this class. - /// This should only be called by Model, as otherwise Model's knowledge of current ownership will be incorrect. - /// - public int ClaimMesh(int meshId, IMeshRenderOwner fosterRenderer) { - if (selectedMeshes.Contains(meshId)) { - highlightUtils.TurnOffMesh(meshId); - selectedMeshes.Remove(meshId); - PeltzerMain.Instance.SetSaveSelectedButtonActiveIfSelectionNotEmpty(); - return meshId; - } - if (hoverMeshes.Contains(meshId)) { - highlightUtils.TurnOffMesh(meshId); - hoverMeshes.Remove(meshId); - return meshId; - } - // Didn't have it, can't relinquish ownership. - return -1; - } + public void SelectMesh(int meshId) + { + MMesh mesh = model.GetMesh(meshId); - public void SelectMesh(int meshId) { - MMesh mesh = model.GetMesh(meshId); - - model.ClaimMesh(meshId, this); - highlightUtils.TurnOnMesh(meshId); - selectedMeshes.Add(meshId); - PeltzerMain.Instance.SetSaveSelectedButtonActiveIfSelectionNotEmpty(); - - if (boundingBoxOfAllSelections == null) { - boundingBoxOfAllSelections = mesh.bounds; - } else { - boundingBoxOfAllSelections.Encapsulate(mesh.bounds); - } - if (hoverMeshes.Contains(meshId)) { - hoverMeshes.Remove(meshId); - } - } + model.ClaimMesh(meshId, this); + highlightUtils.TurnOnMesh(meshId); + selectedMeshes.Add(meshId); + PeltzerMain.Instance.SetSaveSelectedButtonActiveIfSelectionNotEmpty(); - /// Tells us if a face has been successfully selected or not. An out parameter was chosen instead of changing - /// the return type of the method to bool because the method also handles face hovering, whereas this bool value only deals with selection. - private void TryHighlightingAFace(FaceKey faceKey, Vector3 position, out bool successfulSelection) { - // Default assumption that we don't successfully select. - successfulSelection = false; - - // In multi-select mode, mark all hovered faces as selected and generate their highlights. - if (isMultiSelecting) { - if (!selectedFaces.Contains(faceKey)) { - SelectFace(faceKey, position); - // We're guaranteed to have selected a face if the selected faces don't contain the current face. - successfulSelection = true; + if (boundingBoxOfAllSelections == null) + { + boundingBoxOfAllSelections = mesh.bounds; + } + else + { + boundingBoxOfAllSelections.Encapsulate(mesh.bounds); + } + if (hoverMeshes.Contains(meshId)) + { + hoverMeshes.Remove(meshId); + } } - } - // If anything is currently selected, regardless of multi-select state, then hovering and selection is disabled, so return. - if (selectedFaces.Count > 0) { - return; - } + /// Tells us if a face has been successfully selected or not. An out parameter was chosen instead of changing + /// the return type of the method to bool because the method also handles face hovering, whereas this bool value only deals with selection. + private void TryHighlightingAFace(FaceKey faceKey, Vector3 position, out bool successfulSelection) + { + // Default assumption that we don't successfully select. + successfulSelection = false; + + // In multi-select mode, mark all hovered faces as selected and generate their highlights. + if (isMultiSelecting) + { + if (!selectedFaces.Contains(faceKey)) + { + SelectFace(faceKey, position); + // We're guaranteed to have selected a face if the selected faces don't contain the current face. + successfulSelection = true; + } + } - // Clear any existing single-select hover highlights that aren't faces. - Deselect(NOT_FACES, /*deselectSelectedHighlights*/ false, /*deselectHoveredHighlights*/ true); + // If anything is currently selected, regardless of multi-select state, then hovering and selection is disabled, so return. + if (selectedFaces.Count > 0) + { + return; + } + + // Clear any existing single-select hover highlights that aren't faces. + Deselect(NOT_FACES, /*deselectSelectedHighlights*/ false, /*deselectHoveredHighlights*/ true); - // In single-select mode, create highlights for the newly hovered face if it is not already hovered. - if (hoverFace == null || hoverFace != faceKey) { - // If there is an existing face highlight destroy it. - if (hoverFace != null) { - highlightUtils.TurnOff(hoverFace); + // In single-select mode, create highlights for the newly hovered face if it is not already hovered. + if (hoverFace == null || hoverFace != faceKey) + { + // If there is an existing face highlight destroy it. + if (hoverFace != null) + { + highlightUtils.TurnOff(hoverFace); + } + + hoverFace = faceKey; + highlightUtils.TurnOn(faceKey, position); + } } - hoverFace = faceKey; - highlightUtils.TurnOn(faceKey, position); - } - } + private void SelectFace(FaceKey faceKey, Vector3 position) + { + selectedFaces.Add(faceKey); + undoFaceMultiSelect.Push(faceKey); - private void SelectFace(FaceKey faceKey, Vector3 position) { - selectedFaces.Add(faceKey); - undoFaceMultiSelect.Push(faceKey); - - Bounds faceBounds = model.GetMesh(faceKey.meshId).CalculateFaceBoundsInModelSpace(faceKey.faceId); - if (boundingBoxOfAllSelections == null) { - boundingBoxOfAllSelections = faceBounds; - } else { - boundingBoxOfAllSelections.Encapsulate(faceBounds); - } - - highlightUtils.TurnOn(faceKey, position); - if (hoverFace != null && hoverFace == faceKey) { - hoverFace = null; - } - } + Bounds faceBounds = model.GetMesh(faceKey.meshId).CalculateFaceBoundsInModelSpace(faceKey.faceId); + if (boundingBoxOfAllSelections == null) + { + boundingBoxOfAllSelections = faceBounds; + } + else + { + boundingBoxOfAllSelections.Encapsulate(faceBounds); + } - /// Tells us if an edge has been successfully selected or not. An out parameter was chosen instead of changing - /// the return type of the method to bool because the method also handles edge hovering, whereas this bool value only deals with selection. - private void TryHighlightingAnEdge(EdgeKey edge, out bool successfulSelection) { - // Default assumption that we don't successfully select. - successfulSelection = false; - - // In multi-select mode, mark all hovered edges as selected and generate their highlights. - if (isMultiSelecting) { - if (!selectedEdges.Contains(edge)) { - SelectEdge(edge); - // We're guaranteed to have selected an edge if the selected edges don't contain the current edge. - successfulSelection = true; + highlightUtils.TurnOn(faceKey, position); + if (hoverFace != null && hoverFace == faceKey) + { + hoverFace = null; + } } - } - // If anything is currently selected, regardless of multi-select state, then hovering and selection is disabled, so return. - if (selectedEdges.Count > 0) { - return; - } + /// Tells us if an edge has been successfully selected or not. An out parameter was chosen instead of changing + /// the return type of the method to bool because the method also handles edge hovering, whereas this bool value only deals with selection. + private void TryHighlightingAnEdge(EdgeKey edge, out bool successfulSelection) + { + // Default assumption that we don't successfully select. + successfulSelection = false; + + // In multi-select mode, mark all hovered edges as selected and generate their highlights. + if (isMultiSelecting) + { + if (!selectedEdges.Contains(edge)) + { + SelectEdge(edge); + // We're guaranteed to have selected an edge if the selected edges don't contain the current edge. + successfulSelection = true; + } + } - // Clear any existing single-select hover highlights that aren't edges. - Deselect(NOT_EDGES, /*deselectSelectedHighlights*/ false, /*deselectHoveredHighlights*/ true); + // If anything is currently selected, regardless of multi-select state, then hovering and selection is disabled, so return. + if (selectedEdges.Count > 0) + { + return; + } - // In single-select mode, turn on highlights for the newly hovered edge if it is not already hovered. - if (hoverEdge == null || hoverEdge != edge) { - // If there is an existing edge highlight destroy it. - if (hoverEdge != null) { - highlightUtils.TurnOff(hoverEdge); + // Clear any existing single-select hover highlights that aren't edges. + Deselect(NOT_EDGES, /*deselectSelectedHighlights*/ false, /*deselectHoveredHighlights*/ true); + + // In single-select mode, turn on highlights for the newly hovered edge if it is not already hovered. + if (hoverEdge == null || hoverEdge != edge) + { + // If there is an existing edge highlight destroy it. + if (hoverEdge != null) + { + highlightUtils.TurnOff(hoverEdge); + } + + highlightUtils.TurnOn(edge); + highlightUtils.SetEdgeStyleToSelect(edge); + hoverEdge = edge; + } } - highlightUtils.TurnOn(edge); - highlightUtils.SetEdgeStyleToSelect(edge); - hoverEdge = edge; - } - } + private void SelectEdge(EdgeKey edgeKey) + { + highlightUtils.TurnOn(edgeKey); + highlightUtils.SetEdgeStyleToSelect(edgeKey); + selectedEdges.Add(edgeKey); + undoEdgeMultiSelect.Push(edgeKey); - private void SelectEdge(EdgeKey edgeKey) { - highlightUtils.TurnOn(edgeKey); - highlightUtils.SetEdgeStyleToSelect(edgeKey); - selectedEdges.Add(edgeKey); - undoEdgeMultiSelect.Push(edgeKey); - - if (hoverEdge != null && hoverEdge == edgeKey) { - hoverEdge = null; - } - - // Encapsulate the vertices making up the edge in the bounding box of all selections. - MMesh mesh = model.GetMesh(edgeKey.meshId); - if (boundingBoxOfAllSelections == null) { - boundingBoxOfAllSelections = new Bounds( - mesh.VertexPositionInModelCoords(edgeKey.vertexId1), // center - Vector3.zero); // size - } else { - boundingBoxOfAllSelections.Encapsulate(mesh.VertexPositionInModelCoords(edgeKey.vertexId1)); - } - boundingBoxOfAllSelections.Encapsulate(mesh.VertexPositionInModelCoords(edgeKey.vertexId2)); - } + if (hoverEdge != null && hoverEdge == edgeKey) + { + hoverEdge = null; + } - /// Tells us if a vertex has been successfully selected or not. An out parameter was chosen instead of changing - /// the return type of the method to bool because the method also handles vertex hovering, whereas this bool value only deals with selection. - private void TryHighlightingAVertex(VertexKey vertex, out bool successfulSelection) { - // Default assumption that we don't successfully select. - successfulSelection = false; - - // In multi-select mode, mark all hovered vertices as selected and generate their highlights. - if (isMultiSelecting) { - if (!selectedVertices.Contains(vertex)) { - SelectVertex(vertex); - // We're guaranteed to have selected a vertex if the selected vertices don't contain the current vertex. - successfulSelection = true; - } - return; - } - - // If anything is currently selected, regardless of multi-select state, then hovering and selection is disabled, so return. - if (selectedVertices.Count > 0) { - return; - } - - // Clear any existing single-select hover highlights that aren't vertices. - Deselect(NOT_VERTICES, /*deselectSelectedHighlights*/ false, /*deselectHoveredHighlights*/ true); - - // In single-select mode, activate highlights for the newly hovered vertex if it is not already hovered. - if (hoverVertex == null || hoverVertex != vertex) { - // If there is an existing vertex highlight destroy it. - if (hoverVertex != null) { - highlightUtils.TurnOff(hoverVertex); + // Encapsulate the vertices making up the edge in the bounding box of all selections. + MMesh mesh = model.GetMesh(edgeKey.meshId); + if (boundingBoxOfAllSelections == null) + { + boundingBoxOfAllSelections = new Bounds( + mesh.VertexPositionInModelCoords(edgeKey.vertexId1), // center + Vector3.zero); // size + } + else + { + boundingBoxOfAllSelections.Encapsulate(mesh.VertexPositionInModelCoords(edgeKey.vertexId1)); + } + boundingBoxOfAllSelections.Encapsulate(mesh.VertexPositionInModelCoords(edgeKey.vertexId2)); } - highlightUtils.TurnOn(vertex); - highlightUtils.SetVertexStyleToSelect(vertex); - hoverVertex = vertex; - } - } + /// Tells us if a vertex has been successfully selected or not. An out parameter was chosen instead of changing + /// the return type of the method to bool because the method also handles vertex hovering, whereas this bool value only deals with selection. + private void TryHighlightingAVertex(VertexKey vertex, out bool successfulSelection) + { + // Default assumption that we don't successfully select. + successfulSelection = false; + + // In multi-select mode, mark all hovered vertices as selected and generate their highlights. + if (isMultiSelecting) + { + if (!selectedVertices.Contains(vertex)) + { + SelectVertex(vertex); + // We're guaranteed to have selected a vertex if the selected vertices don't contain the current vertex. + successfulSelection = true; + } + return; + } - private void SelectVertex(VertexKey vertexKey) { - selectedVertices.Add(vertexKey); - highlightUtils.TurnOn(vertexKey); - highlightUtils.SetVertexStyleToSelect(vertexKey); - undoVertexMultiSelect.Push(vertexKey); - - MMesh mesh = model.GetMesh(vertexKey.meshId); - if (boundingBoxOfAllSelections == null) { - boundingBoxOfAllSelections = new Bounds( - mesh.VertexPositionInModelCoords(vertexKey.vertexId), // center - Vector3.zero); // size - } else { - boundingBoxOfAllSelections.Encapsulate(mesh.VertexPositionInModelCoords(vertexKey.vertexId)); - } - } + // If anything is currently selected, regardless of multi-select state, then hovering and selection is disabled, so return. + if (selectedVertices.Count > 0) + { + return; + } - /// - /// Whether this matches the pattern of a 'start multi-selection mode' event. - /// - /// The controller event arguments. - /// True if this is a select event, false otherwise. - private bool IsStartMultiSelecting(ControllerEventArgs args) { - // First check the controller seems in the right state. - return args.ControllerType == ControllerType.PELTZER - && args.Action == ButtonAction.DOWN - && PeltzerMain.Instance.peltzerController.mode != ControllerMode.insertVolume - && args.ButtonId == ButtonId.Trigger; - } + // Clear any existing single-select hover highlights that aren't vertices. + Deselect(NOT_VERTICES, /*deselectSelectedHighlights*/ false, /*deselectHoveredHighlights*/ true); + + // In single-select mode, activate highlights for the newly hovered vertex if it is not already hovered. + if (hoverVertex == null || hoverVertex != vertex) + { + // If there is an existing vertex highlight destroy it. + if (hoverVertex != null) + { + highlightUtils.TurnOff(hoverVertex); + } + + highlightUtils.TurnOn(vertex); + highlightUtils.SetVertexStyleToSelect(vertex); + hoverVertex = vertex; + } + } - /// - /// Whether this matches the pattern of a 'end multi-select mode' event. - /// - /// The controller event arguments. - /// True if this is a select event, false otherwise. - private bool IsStopMultiSelecting(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.Action == ButtonAction.UP - && args.ButtonId == ButtonId.Trigger; - } + private void SelectVertex(VertexKey vertexKey) + { + selectedVertices.Add(vertexKey); + highlightUtils.TurnOn(vertexKey); + highlightUtils.SetVertexStyleToSelect(vertexKey); + undoVertexMultiSelect.Push(vertexKey); + + MMesh mesh = model.GetMesh(vertexKey.meshId); + if (boundingBoxOfAllSelections == null) + { + boundingBoxOfAllSelections = new Bounds( + mesh.VertexPositionInModelCoords(vertexKey.vertexId), // center + Vector3.zero); // size + } + else + { + boundingBoxOfAllSelections.Encapsulate(mesh.VertexPositionInModelCoords(vertexKey.vertexId)); + } + } - /// - /// An event handler that listens for controller input and delegates accordingly. - /// - /// The sender of the controller event. - /// The controller event arguments. - private void ControllerEventHandler(object sender, ControllerEventArgs args) { - if (!multiSelectEnabled) { - return; - } - - if (IsStartMultiSelecting(args) && !PeltzerMain.Instance.GetMover().IsMoving() - && !PeltzerMain.Instance.peltzerController.isPointingAtMenu) { - StartMultiSelection(); - } else if (isMultiSelecting && IsStopMultiSelecting(args)) { - EndMultiSelection(); - } - } + /// + /// Whether this matches the pattern of a 'start multi-selection mode' event. + /// + /// The controller event arguments. + /// True if this is a select event, false otherwise. + private bool IsStartMultiSelecting(ControllerEventArgs args) + { + // First check the controller seems in the right state. + return args.ControllerType == ControllerType.PELTZER + && args.Action == ButtonAction.DOWN + && PeltzerMain.Instance.peltzerController.mode != ControllerMode.insertVolume + && args.ButtonId == ButtonId.Trigger; + } - /// - /// If the given position lies inside the bounding box of all selected items, returns true. - /// Else, de-selects everything that was selected and returns false. - /// - /// We give some leeway by growing the bounding box by 10% in world-space. The bounding box of all selections - /// and the click position are both in model-space, but the growth is scaled to world-space because the user's - /// input accuracy is limited by world-space, not model-space. - /// - /// If a user has pulled the trigger to begin an operation on selected items, it is expected (but not - /// enforced in code) that the responsible tool first call this method. - /// - /// Will naturally return 'false' in the case that nothing is selected (because the bounding box will be empty), - /// which seems reasonable given that 'false' implies the tool should not continue with its operation anyway. - /// - public bool ClickIsWithinCurrentSelection(Vector3 clickPosition) { - if (!Features.clickAwayToDeselect) { - return true; // Short-circuit if the feature is disabled. - } + /// + /// Whether this matches the pattern of a 'end multi-select mode' event. + /// + /// The controller event arguments. + /// True if this is a select event, false otherwise. + private bool IsStopMultiSelecting(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.Action == ButtonAction.UP + && args.ButtonId == ButtonId.Trigger; + } - float scaleFactor = 1 + (0.1f * worldSpace.scale); - Bounds grownBounds = - new Bounds(boundingBoxOfAllSelections.center, boundingBoxOfAllSelections.size * scaleFactor); + /// + /// An event handler that listens for controller input and delegates accordingly. + /// + /// The sender of the controller event. + /// The controller event arguments. + private void ControllerEventHandler(object sender, ControllerEventArgs args) + { + if (!multiSelectEnabled) + { + return; + } - if (grownBounds.Contains(clickPosition)) { - return true; - } + if (IsStartMultiSelecting(args) && !PeltzerMain.Instance.GetMover().IsMoving() + && !PeltzerMain.Instance.peltzerController.isPointingAtMenu) + { + StartMultiSelection(); + } + else if (isMultiSelecting && IsStopMultiSelecting(args)) + { + EndMultiSelection(); + } + } - Deselect(ALL, /* deselectSelectedHighlights */ true, /* deselectHoveredHighlights */ true); - return false; - } + /// + /// If the given position lies inside the bounding box of all selected items, returns true. + /// Else, de-selects everything that was selected and returns false. + /// + /// We give some leeway by growing the bounding box by 10% in world-space. The bounding box of all selections + /// and the click position are both in model-space, but the growth is scaled to world-space because the user's + /// input accuracy is limited by world-space, not model-space. + /// + /// If a user has pulled the trigger to begin an operation on selected items, it is expected (but not + /// enforced in code) that the responsible tool first call this method. + /// + /// Will naturally return 'false' in the case that nothing is selected (because the bounding box will be empty), + /// which seems reasonable given that 'false' implies the tool should not continue with its operation anyway. + /// + public bool ClickIsWithinCurrentSelection(Vector3 clickPosition) + { + if (!Features.clickAwayToDeselect) + { + return true; // Short-circuit if the feature is disabled. + } - public void DeselectAll() { - Deselect(Selector.ALL, true, true); - TurnOffSelectIndicator(); - ResetInactive(); - } + float scaleFactor = 1 + (0.1f * worldSpace.scale); + Bounds grownBounds = + new Bounds(boundingBoxOfAllSelections.center, boundingBoxOfAllSelections.size * scaleFactor); - /// - /// Removes one mesh from list of selected meshes. - /// - private void DeselectOneMesh(int meshId) { - model.RelinquishMesh(meshId, this); - highlightUtils.TurnOffMesh(meshId); - selectedMeshes.Remove(meshId); - PeltzerMain.Instance.SetSaveSelectedButtonActiveIfSelectionNotEmpty(); - } - - /// - /// Removes one face from list of selected faces. - /// - private void DeselectOneFace(FaceKey faceKey) { - highlightUtils.TurnOff(faceKey); - selectedFaces.Remove(faceKey); - } + if (grownBounds.Contains(clickPosition)) + { + return true; + } - /// - /// Removes one edge from list of selected edges. - /// - private void DeselectOneEdge(EdgeKey edgeKey) { - highlightUtils.TurnOff(edgeKey); - selectedEdges.Remove(edgeKey); - } + Deselect(ALL, /* deselectSelectedHighlights */ true, /* deselectHoveredHighlights */ true); + return false; + } - /// - /// Removes one vertex from list of selected vertices. - /// - private void DeselectOneVertex(VertexKey vertexKey) { - highlightUtils.TurnOff(vertexKey); - selectedVertices.Remove(vertexKey); - } + public void DeselectAll() + { + Deselect(Selector.ALL, true, true); + TurnOffSelectIndicator(); + ResetInactive(); + } - /// - /// Removes all items from all lists of selected items and deletes their highlighting GameObjects. - /// - private void Deselect(SelectorOptions options, bool deselectSelectedHighlights, bool deselectHoveredHighlights) { - if (!PeltzerMain.Instance.restrictionManager.deselectAllowed) { - return; - } - - if (options.includeMeshes) { - MeshCycler.ResetCycler(); - if (deselectSelectedHighlights) { - foreach (int meshId in selectedMeshes) { + /// + /// Removes one mesh from list of selected meshes. + /// + private void DeselectOneMesh(int meshId) + { model.RelinquishMesh(meshId, this); highlightUtils.TurnOffMesh(meshId); - } - selectedMeshes.Clear(); - PeltzerMain.Instance.SetSaveSelectedButtonActiveIfSelectionNotEmpty(); + selectedMeshes.Remove(meshId); + PeltzerMain.Instance.SetSaveSelectedButtonActiveIfSelectionNotEmpty(); } - if (deselectHoveredHighlights) { - foreach (int meshId in hoverMeshes) { - model.RelinquishMesh(meshId, this); - highlightUtils.TurnOffMesh(meshId); - } + /// + /// Removes one face from list of selected faces. + /// + private void DeselectOneFace(FaceKey faceKey) + { + highlightUtils.TurnOff(faceKey); + selectedFaces.Remove(faceKey); } - hoverMeshes.Clear(); - } - - if (options.includeFaces) { - if (deselectHoveredHighlights && deselectSelectedHighlights) { - highlightUtils.ClearFaces(); - selectedFaces.Clear(); - hoverFace = null; - } else { - if (deselectSelectedHighlights) { - foreach (FaceKey key in selectedFaces) { - highlightUtils.TurnOff(key); - } - selectedFaces.Clear(); - } - if (deselectHoveredHighlights) { - if (hoverFace != null) { - highlightUtils.TurnOff(hoverFace); - hoverFace = null; - } - } + + /// + /// Removes one edge from list of selected edges. + /// + private void DeselectOneEdge(EdgeKey edgeKey) + { + highlightUtils.TurnOff(edgeKey); + selectedEdges.Remove(edgeKey); } - } - if (options.includeEdges) { - if (deselectHoveredHighlights && deselectSelectedHighlights) { - highlightUtils.ClearEdges(); - selectedEdges.Clear(); - hoverEdge = null; - } else { - if (deselectSelectedHighlights) { - foreach (EdgeKey key in selectedEdges) { - highlightUtils.TurnOff(key); - } + /// + /// Removes one vertex from list of selected vertices. + /// + private void DeselectOneVertex(VertexKey vertexKey) + { + highlightUtils.TurnOff(vertexKey); + selectedVertices.Remove(vertexKey); + } - selectedEdges.Clear(); - } + /// + /// Removes all items from all lists of selected items and deletes their highlighting GameObjects. + /// + private void Deselect(SelectorOptions options, bool deselectSelectedHighlights, bool deselectHoveredHighlights) + { + if (!PeltzerMain.Instance.restrictionManager.deselectAllowed) + { + return; + } - if (deselectHoveredHighlights) { - if (hoverEdge != null) { - highlightUtils.TurnOff(hoverEdge); - hoverEdge = null; + if (options.includeMeshes) + { + MeshCycler.ResetCycler(); + if (deselectSelectedHighlights) + { + foreach (int meshId in selectedMeshes) + { + model.RelinquishMesh(meshId, this); + highlightUtils.TurnOffMesh(meshId); + } + selectedMeshes.Clear(); + PeltzerMain.Instance.SetSaveSelectedButtonActiveIfSelectionNotEmpty(); + } + + if (deselectHoveredHighlights) + { + foreach (int meshId in hoverMeshes) + { + model.RelinquishMesh(meshId, this); + highlightUtils.TurnOffMesh(meshId); + } + } + hoverMeshes.Clear(); } - } - } - } - if (options.includeVertices) { - if (deselectHoveredHighlights && deselectSelectedHighlights) { - highlightUtils.ClearVertices(); - selectedVertices.Clear(); - hoverVertex = null; - } else { - if (deselectSelectedHighlights) { - foreach (VertexKey vertexKey in selectedVertices) { - highlightUtils.TurnOff(vertexKey); + if (options.includeFaces) + { + if (deselectHoveredHighlights && deselectSelectedHighlights) + { + highlightUtils.ClearFaces(); + selectedFaces.Clear(); + hoverFace = null; + } + else + { + if (deselectSelectedHighlights) + { + foreach (FaceKey key in selectedFaces) + { + highlightUtils.TurnOff(key); + } + selectedFaces.Clear(); + } + if (deselectHoveredHighlights) + { + if (hoverFace != null) + { + highlightUtils.TurnOff(hoverFace); + hoverFace = null; + } + } + } } - selectedVertices.Clear(); - } + if (options.includeEdges) + { + if (deselectHoveredHighlights && deselectSelectedHighlights) + { + highlightUtils.ClearEdges(); + selectedEdges.Clear(); + hoverEdge = null; + } + else + { + if (deselectSelectedHighlights) + { + foreach (EdgeKey key in selectedEdges) + { + highlightUtils.TurnOff(key); + } + + selectedEdges.Clear(); + } + + if (deselectHoveredHighlights) + { + if (hoverEdge != null) + { + highlightUtils.TurnOff(hoverEdge); + hoverEdge = null; + } + } + } + } - if (deselectHoveredHighlights) { - if (hoverVertex != null) { - highlightUtils.TurnOff(hoverVertex); - hoverVertex = null; + if (options.includeVertices) + { + if (deselectHoveredHighlights && deselectSelectedHighlights) + { + highlightUtils.ClearVertices(); + selectedVertices.Clear(); + hoverVertex = null; + } + else + { + if (deselectSelectedHighlights) + { + foreach (VertexKey vertexKey in selectedVertices) + { + highlightUtils.TurnOff(vertexKey); + } + + selectedVertices.Clear(); + } + + if (deselectHoveredHighlights) + { + if (hoverVertex != null) + { + highlightUtils.TurnOff(hoverVertex); + hoverVertex = null; + } + } + } } - } } - } - } - - public bool AnythingSelected() { - return selectedVertices.Count > 0 || selectedEdges.Count > 0 - || selectedFaces.Count > 0 || selectedMeshes.Count > 0; - } - /// - /// If undo is possible, checks through undo stacks to see which one is populated, and undoes from there. - /// - public bool UndoMultiSelect() { - // Can only undo if one of the undo stacks is populated. - if (selectedVertices.Count != 0) { - VertexKey lastVert = undoVertexMultiSelect.Pop(); - DeselectOneVertex(lastVert); - redoVertexMultiSelect.Push(lastVert); - return true; - } else if (selectedEdges.Count != 0) { - EdgeKey lastEdge = undoEdgeMultiSelect.Pop(); - DeselectOneEdge(lastEdge); - redoEdgeMultiSelect.Push(lastEdge); - return true; - } else if (selectedFaces.Count != 0) { - FaceKey lastFace = undoFaceMultiSelect.Pop(); - DeselectOneFace(lastFace); - redoFaceMultiSelect.Push(lastFace); - return true; - } else if (selectedMeshes.Count != 0) { - int lastMesh = undoMeshMultiSelect.Pop(); - DeselectMesh(lastMesh); - redoMeshMultiSelect.Push(lastMesh); - return true; - } - return false; - } + public bool AnythingSelected() + { + return selectedVertices.Count > 0 || selectedEdges.Count > 0 + || selectedFaces.Count > 0 || selectedMeshes.Count > 0; + } - /// - /// If redo is possible, checks through redo stacks to see which one is populated, and redoes from there. - /// - public bool RedoMultiSelect() { - // Can only redo if one of the redo stacks is populated. - if (redoVertexMultiSelect.Count != 0) { - VertexKey lastVert = redoVertexMultiSelect.Pop(); - SelectVertex(lastVert); - return true; - } else if (redoEdgeMultiSelect.Count != 0) { - EdgeKey lastEdge = redoEdgeMultiSelect.Pop(); - SelectEdge(lastEdge); - return true; - } else if (redoFaceMultiSelect.Count != 0) { - FaceKey lastFaceKey = redoFaceMultiSelect.Pop(); - MMesh mesh = model.GetMesh(lastFaceKey.meshId); - Face face = mesh.GetFace(lastFaceKey.faceId); - // Calculate face center so that we can animate from center. - Vector3 centerOfFace = MeshMath.CalculateGeometricCenter(face, mesh); - SelectFace(lastFaceKey, centerOfFace); - return true; - } else if (redoMeshMultiSelect.Count != 0) { - int lastMesh = redoMeshMultiSelect.Pop(); - bool successfulSelectionMesh; - TryHighlightingAMesh(lastMesh, /* includeGroups = */true, /* forceSelection = */ Features.clickToSelectEnabled, out successfulSelectionMesh); - return true; - } - return false; - } + /// + /// If undo is possible, checks through undo stacks to see which one is populated, and undoes from there. + /// + public bool UndoMultiSelect() + { + // Can only undo if one of the undo stacks is populated. + if (selectedVertices.Count != 0) + { + VertexKey lastVert = undoVertexMultiSelect.Pop(); + DeselectOneVertex(lastVert); + redoVertexMultiSelect.Push(lastVert); + return true; + } + else if (selectedEdges.Count != 0) + { + EdgeKey lastEdge = undoEdgeMultiSelect.Pop(); + DeselectOneEdge(lastEdge); + redoEdgeMultiSelect.Push(lastEdge); + return true; + } + else if (selectedFaces.Count != 0) + { + FaceKey lastFace = undoFaceMultiSelect.Pop(); + DeselectOneFace(lastFace); + redoFaceMultiSelect.Push(lastFace); + return true; + } + else if (selectedMeshes.Count != 0) + { + int lastMesh = undoMeshMultiSelect.Pop(); + DeselectMesh(lastMesh); + redoMeshMultiSelect.Push(lastMesh); + return true; + } + return false; + } - /// - /// Clears all local redo stacks for vertices, edges, faces, and meshes. - /// - public void ClearMultiSelectRedoState() { - redoVertexMultiSelect.Clear(); - redoEdgeMultiSelect.Clear(); - redoFaceMultiSelect.Clear(); - redoMeshMultiSelect.Clear(); - } + /// + /// If redo is possible, checks through redo stacks to see which one is populated, and redoes from there. + /// + public bool RedoMultiSelect() + { + // Can only redo if one of the redo stacks is populated. + if (redoVertexMultiSelect.Count != 0) + { + VertexKey lastVert = redoVertexMultiSelect.Pop(); + SelectVertex(lastVert); + return true; + } + else if (redoEdgeMultiSelect.Count != 0) + { + EdgeKey lastEdge = redoEdgeMultiSelect.Pop(); + SelectEdge(lastEdge); + return true; + } + else if (redoFaceMultiSelect.Count != 0) + { + FaceKey lastFaceKey = redoFaceMultiSelect.Pop(); + MMesh mesh = model.GetMesh(lastFaceKey.meshId); + Face face = mesh.GetFace(lastFaceKey.faceId); + // Calculate face center so that we can animate from center. + Vector3 centerOfFace = MeshMath.CalculateGeometricCenter(face, mesh); + SelectFace(lastFaceKey, centerOfFace); + return true; + } + else if (redoMeshMultiSelect.Count != 0) + { + int lastMesh = redoMeshMultiSelect.Pop(); + bool successfulSelectionMesh; + TryHighlightingAMesh(lastMesh, /* includeGroups = */true, /* forceSelection = */ Features.clickToSelectEnabled, out successfulSelectionMesh); + return true; + } + return false; + } - /// - /// Clears all local undo stacks except the undo stack of the type passed in. Defaults to clearing - /// all the undo stacks. - /// - /// The type whose undo stack we don't want to clear. - public void ClearMultiSelectUndoState(Type type) { - switch (type) { - case Type.VERTEX: - undoEdgeMultiSelect.Clear(); - undoFaceMultiSelect.Clear(); - undoMeshMultiSelect.Clear(); - break; - case Type.EDGE: - undoVertexMultiSelect.Clear(); - undoFaceMultiSelect.Clear(); - undoMeshMultiSelect.Clear(); - break; - case Type.FACE: - undoVertexMultiSelect.Clear(); - undoEdgeMultiSelect.Clear(); - undoMeshMultiSelect.Clear(); - break; - case Type.MESH: - undoVertexMultiSelect.Clear(); - undoEdgeMultiSelect.Clear(); - undoFaceMultiSelect.Clear(); - break; - default: - undoVertexMultiSelect.Clear(); - undoEdgeMultiSelect.Clear(); - undoFaceMultiSelect.Clear(); - undoMeshMultiSelect.Clear(); - break; - } - } + /// + /// Clears all local redo stacks for vertices, edges, faces, and meshes. + /// + public void ClearMultiSelectRedoState() + { + redoVertexMultiSelect.Clear(); + redoEdgeMultiSelect.Clear(); + redoFaceMultiSelect.Clear(); + redoMeshMultiSelect.Clear(); + } - // Class describing each dot - mainly used for the age. - class MSdot { - public Transform _transform; - public float _age; - public Renderer _rend; - public Vector3 rot; - - public MSdot() { - _age = 1f; - rot = new Vector3(UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value); - } - } + /// + /// Clears all local undo stacks except the undo stack of the type passed in. Defaults to clearing + /// all the undo stacks. + /// + /// The type whose undo stack we don't want to clear. + public void ClearMultiSelectUndoState(Type type) + { + switch (type) + { + case Type.VERTEX: + undoEdgeMultiSelect.Clear(); + undoFaceMultiSelect.Clear(); + undoMeshMultiSelect.Clear(); + break; + case Type.EDGE: + undoVertexMultiSelect.Clear(); + undoFaceMultiSelect.Clear(); + undoMeshMultiSelect.Clear(); + break; + case Type.FACE: + undoVertexMultiSelect.Clear(); + undoEdgeMultiSelect.Clear(); + undoMeshMultiSelect.Clear(); + break; + case Type.MESH: + undoVertexMultiSelect.Clear(); + undoEdgeMultiSelect.Clear(); + undoFaceMultiSelect.Clear(); + break; + default: + undoVertexMultiSelect.Clear(); + undoEdgeMultiSelect.Clear(); + undoFaceMultiSelect.Clear(); + undoMeshMultiSelect.Clear(); + break; + } + } - private void StartMultiSelection() { - isMultiSelecting = true; - multiselectTrail.SetActive(true); - } + // Class describing each dot - mainly used for the age. + class MSdot + { + public Transform _transform; + public float _age; + public Renderer _rend; + public Vector3 rot; + + public MSdot() + { + _age = 1f; + rot = new Vector3(UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value); + } + } - private void UpdateMultiselectionTrail() { - multiselectTrail.transform.position = -1f * worldSpace.WorldToModel(Vector3.zero); - Vector3 heading = PeltzerMain.Instance.worldSpace.ModelToWorld(peltzerController.LastPositionModel) - lastDotPosition; - - if (heading.magnitude > SPACING) { - lastDotPosition = PeltzerMain.Instance.worldSpace.ModelToWorld(peltzerController.LastPositionModel); - MSdot _dot = new MSdot(); - - GameObject g = new GameObject("dot"); - MeshFilter mf = g.AddComponent(); - mf.mesh = DOTSmesh; - _dot._rend = g.AddComponent(); - _dot._transform = g.transform; - - _dot._transform.localScale = Vector3.zero; - _dot._transform.position = lastDotPosition; - _dot._transform.localRotation = Quaternion.Euler(_dot.rot); - _dot._rend.material = multiselectDotMaterial; - _dot._transform.parent = multiselectTrail.transform; - DOTS.Add(_dot); - } - - float partA = .925f; - float partB = .075f; - for (int i = DOTS.Count - 1; i >= 0; i--) { - DOTS[i]._age = Mathf.Clamp01(DOTS[i]._age - Time.deltaTime*1.5f); - if (DOTS[i]._age > partA) { - DOTS[i]._transform.localScale = MULTISELECT_DOT_SCALE * (1 - (DOTS[i]._age - partA) / partB); - } else { - DOTS[i]._transform.localScale = MULTISELECT_DOT_SCALE * DOTS[i]._age / partA; + private void StartMultiSelection() + { + isMultiSelecting = true; + multiselectTrail.SetActive(true); } - // DOTS[i]._transform.Rotate(DOTS[i].rot); - DOTS[i]._rend.material.color = Color.HSVToRGB(DOTS[i]._age, 1f - DOTS[i]._age, 1) * new Color(1, 1, 1, .75f * DOTS[i]._age); - if (DOTS[i]._age < .01f) { - Destroy(DOTS[i]._transform.gameObject); - DOTS.RemoveAt(i); + + private void UpdateMultiselectionTrail() + { + multiselectTrail.transform.position = -1f * worldSpace.WorldToModel(Vector3.zero); + Vector3 heading = PeltzerMain.Instance.worldSpace.ModelToWorld(peltzerController.LastPositionModel) - lastDotPosition; + + if (heading.magnitude > SPACING) + { + lastDotPosition = PeltzerMain.Instance.worldSpace.ModelToWorld(peltzerController.LastPositionModel); + MSdot _dot = new MSdot(); + + GameObject g = new GameObject("dot"); + MeshFilter mf = g.AddComponent(); + mf.mesh = DOTSmesh; + _dot._rend = g.AddComponent(); + _dot._transform = g.transform; + + _dot._transform.localScale = Vector3.zero; + _dot._transform.position = lastDotPosition; + _dot._transform.localRotation = Quaternion.Euler(_dot.rot); + _dot._rend.material = multiselectDotMaterial; + _dot._transform.parent = multiselectTrail.transform; + DOTS.Add(_dot); + } + + float partA = .925f; + float partB = .075f; + for (int i = DOTS.Count - 1; i >= 0; i--) + { + DOTS[i]._age = Mathf.Clamp01(DOTS[i]._age - Time.deltaTime * 1.5f); + if (DOTS[i]._age > partA) + { + DOTS[i]._transform.localScale = MULTISELECT_DOT_SCALE * (1 - (DOTS[i]._age - partA) / partB); + } + else + { + DOTS[i]._transform.localScale = MULTISELECT_DOT_SCALE * DOTS[i]._age / partA; + } + // DOTS[i]._transform.Rotate(DOTS[i].rot); + DOTS[i]._rend.material.color = Color.HSVToRGB(DOTS[i]._age, 1f - DOTS[i]._age, 1) * new Color(1, 1, 1, .75f * DOTS[i]._age); + if (DOTS[i]._age < .01f) + { + Destroy(DOTS[i]._transform.gameObject); + DOTS.RemoveAt(i); + } + } } - } - } - public void EndMultiSelection() { - isMultiSelecting = false; + public void EndMultiSelection() + { + isMultiSelecting = false; - for (int i = DOTS.Count - 1; i >= 0; i--) { - Destroy(DOTS[i]._transform.gameObject); - } - DOTS.Clear(); + for (int i = DOTS.Count - 1; i >= 0; i--) + { + Destroy(DOTS[i]._transform.gameObject); + } + DOTS.Clear(); - multiselectTrail.SetActive(false); - } + multiselectTrail.SetActive(false); + } - public void ClearState() { - Deselect(Selector.ALL, /*deselectSelectedHighlights*/ true, /*deselectHoveredHighlights*/ true); - } + public void ClearState() + { + Deselect(Selector.ALL, /*deselectSelectedHighlights*/ true, /*deselectHoveredHighlights*/ true); + } - // Test Method - public void SetSelectModeActiveForTest(bool selectModeActive) { - this.isMultiSelecting = selectModeActive; - } + // Test Method + public void SetSelectModeActiveForTest(bool selectModeActive) + { + this.isMultiSelecting = selectModeActive; + } - // Test Method - public void AddSelectedFaceForTest(FaceKey faceKey) { - selectedFaces.Add(faceKey); - } + // Test Method + public void AddSelectedFaceForTest(FaceKey faceKey) + { + selectedFaces.Add(faceKey); + } - // Test Method - public void AddSelectedMeshForTest(int meshId) { - selectedMeshes.Add(meshId); + // Test Method + public void AddSelectedMeshForTest(int meshId) + { + selectedMeshes.Add(meshId); + } } - } } diff --git a/Assets/Scripts/tools/Subdivider.cs b/Assets/Scripts/tools/Subdivider.cs index b30b2534..02290351 100644 --- a/Assets/Scripts/tools/Subdivider.cs +++ b/Assets/Scripts/tools/Subdivider.cs @@ -24,1259 +24,1423 @@ using com.google.apps.peltzer.client.model.render; using System; -namespace com.google.apps.peltzer.client.tools { - /// - /// Tool for subdividing faces. User sees a line across a face, and is able to subdivide the face - /// into two faces based on the line. - /// - public class Subdivider : MonoBehaviour { - - /// - /// The current mode of operation for the tool. - /// - public enum Mode { - // Standard subdivide tool with a single subdivision on the selected face. - SINGLE_FACE_SUBDIVIDE, - // Experimental tool that uses a plane to create subdivisions on all intersecting faces. - // Gated by Features.planeSubdivideEnabled - PLANE_SUBDIVIDE, - // Experimental tool that loops around the geometry creating subdivisions. - // Gated by Features.loopSubdivideEnabled and Features.allowNoncoplanarFaces. - LOOP_SUBDIVIDE - }; - +namespace com.google.apps.peltzer.client.tools +{ /// - /// If the subdivision vertex is within this squared distance from an existing vertex on the face or a - /// previously created vertex in this subdivide operation, the existing vertex will be used instead of - /// creating a new one. + /// Tool for subdividing faces. User sees a line across a face, and is able to subdivide the face + /// into two faces based on the line. /// - private const float VERTEX_REUSE_SQUARED_DISTANCE_THRESHOLD = 0.00000025f; + public class Subdivider : MonoBehaviour + { + + /// + /// The current mode of operation for the tool. + /// + public enum Mode + { + // Standard subdivide tool with a single subdivision on the selected face. + SINGLE_FACE_SUBDIVIDE, + // Experimental tool that uses a plane to create subdivisions on all intersecting faces. + // Gated by Features.planeSubdivideEnabled + PLANE_SUBDIVIDE, + // Experimental tool that loops around the geometry creating subdivisions. + // Gated by Features.loopSubdivideEnabled and Features.allowNoncoplanarFaces. + LOOP_SUBDIVIDE + }; + + /// + /// If the subdivision vertex is within this squared distance from an existing vertex on the face or a + /// previously created vertex in this subdivide operation, the existing vertex will be used instead of + /// creating a new one. + /// + private const float VERTEX_REUSE_SQUARED_DISTANCE_THRESHOLD = 0.00000025f; + + // Over how many seconds the highlight shown after a subdivision animates in. + private const float EDGE_HIGHLIGHT_ANIMATION_IN_TIME = 0.5f; + + // After how long the highlight shown after a subdivision expires. + private const float EDGE_HIGHLIGHT_DURATION = 0.5f; + + // Amount of time after a press and hold operation is complete. + private const float PRESS_AND_HOLD_DELAY = 0.5f; + + /// + /// Struct that maintains the two endpoints of a subdivide as well as the vertex index + /// that would occur before the endpoint if going around the face vertices in order. + /// It also holds information about the edges in which those endpoints occur. + /// + private class SubdividePoints + { + public Vector3 point1 { get; set; } + public int point1Index { get; set; } + public EdgeKey edge1 { get; set; } + public Vector3 point2 { get; set; } + public int point2Index { get; set; } + public EdgeKey edge2 { get; set; } + } - // Over how many seconds the highlight shown after a subdivision animates in. - private const float EDGE_HIGHLIGHT_ANIMATION_IN_TIME = 0.5f; + /// + /// A class that will hold state for a single subdivision. + /// + private class Subdivision + { + public Vector3 startPoint { get; set; } + public Vector3 sliceDirection { get; set; } + public Face face { get; set; } + public MMesh mesh { get; set; } + } - // After how long the highlight shown after a subdivision expires. - private const float EDGE_HIGHLIGHT_DURATION = 0.5f; + public ControllerMain controllerMain; + /// + /// A reference to a controller capable of issuing subdivide commands. + /// + private PeltzerController peltzerController; + /// + /// A reference to the overall model being built. + /// + private Model model; + /// + /// Face, edge, and vertex selector for detecting which face is hovered. + /// + private Selector selector; + /// + /// Library for playing sounds. + /// + private AudioLibrary audioLibrary; + + /// + /// Holds details about the active subdivisions to be performed. + /// + private List activeSubdivisions = new List(); + + /// + /// The current mode the tool is in, such as loop subdivide or single face subdivide. + /// + private Mode currentMode = Mode.SINGLE_FACE_SUBDIVIDE; + + /// + /// If true, the trigger can be held to transition to a different tool. Currently only + /// used when loop subdivide is enabled. + /// + private bool pressAndHoldEnabled + { + get { return Features.loopSubdivideEnabled; } + } - // Amount of time after a press and hold operation is complete. - private const float PRESS_AND_HOLD_DELAY = 0.5f; + /// + /// Keeps track of the user holding the trigger button. Needed to transition the tool + /// into other modes such as loop subdivide. + /// + private bool isTriggerBeingHeld; + + /// + /// The time the user started holding the trigger button, only set if isTriggerBeingHeld + /// is true. + /// + private float triggerHoldStartTime; + + /// + /// Whether we are snapping. + /// + private bool isSnapping; + + private WorldSpace worldSpace; + + /// + /// The list of meshes that are currently selected. + /// + private List selectedMeshes = new List(); + + /// + /// A GameObject which is used to indicate something to the user, like a gizmo. Currently used by the planeSubdivider + /// to show a plane representing the cut. + /// + GameObject guidanceMesh; + + /// + /// Keeps track of the faces that each edge in the mesh is connecting. This is needed to find + /// the next face when performing loop subdivide operations. + /// + private Dictionary> edgeKeysToFaceIds = new Dictionary>(); + + /// + /// The edge hints currently being shown. + /// + private List currentTemporaryEdges; + + /// + /// After a subdivision, we briefly flash an edge highlight to show success. This queue manages turning off the + /// highlights once they've flashed. + /// + Queue> highlightsToTurnOff = new Queue>(); + + /// + /// Used to determine if we should show the snap tooltip or not. Don't show the tooltip if the user already + /// showed enough knowledge of how to snap. + /// + private int completedSnaps = 0; + private const int SNAP_KNOW_HOW_COUNT = 3; + + /// + /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. + /// As such, this setup method must be called before the tool is used for it to have a valid state. + /// + public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, + PaletteController paletteController, Selector selector, AudioLibrary audioLibrary, WorldSpace worldSpace) + { + this.model = model; + this.controllerMain = controllerMain; + this.peltzerController = peltzerController; + this.selector = selector; + this.audioLibrary = audioLibrary; + this.worldSpace = worldSpace; + + controllerMain.ControllerActionHandler += ControllerEventHandler; + peltzerController.ModeChangedHandler += ControllerModeChangedHandler; + + currentTemporaryEdges = new List(); + currentMode = Mode.SINGLE_FACE_SUBDIVIDE; + } - /// - /// Struct that maintains the two endpoints of a subdivide as well as the vertex index - /// that would occur before the endpoint if going around the face vertices in order. - /// It also holds information about the edges in which those endpoints occur. - /// - private class SubdividePoints { - public Vector3 point1 { get; set; } - public int point1Index { get; set; } - public EdgeKey edge1 { get; set; } - public Vector3 point2 { get; set; } - public int point2Index { get; set; } - public EdgeKey edge2 { get; set; } - } + void Update() + { + // We need to clean up any 'thrown away' objects when their time comes (even if the user has changed mode). + while (highlightsToTurnOff.Count > 0 && highlightsToTurnOff.Peek().Key < Time.time) + { + KeyValuePair highlightToTurnOff = highlightsToTurnOff.Dequeue(); + PeltzerMain.Instance.highlightUtils.TurnOff(highlightToTurnOff.Value); + } - /// - /// A class that will hold state for a single subdivision. - /// - private class Subdivision { - public Vector3 startPoint { get; set; } - public Vector3 sliceDirection { get; set; } - public Face face { get; set; } - public MMesh mesh { get; set; } - } + if (!PeltzerController.AcquireIfNecessary(ref peltzerController) || peltzerController.mode != ControllerMode.subdivideFace) + { + return; + } - public ControllerMain controllerMain; - /// - /// A reference to a controller capable of issuing subdivide commands. - /// - private PeltzerController peltzerController; - /// - /// A reference to the overall model being built. - /// - private Model model; - /// - /// Face, edge, and vertex selector for detecting which face is hovered. - /// - private Selector selector; - /// - /// Library for playing sounds. - /// - private AudioLibrary audioLibrary; + bool isNewMode = false; + if (pressAndHoldEnabled + && isTriggerBeingHeld + && Time.time > triggerHoldStartTime + PRESS_AND_HOLD_DELAY) + { + currentMode = Mode.LOOP_SUBDIVIDE; + audioLibrary.PlayClip(audioLibrary.genericSelectSound); + peltzerController.TriggerHapticFeedback(); + isTriggerBeingHeld = false; + isNewMode = true; + } - /// - /// Holds details about the active subdivisions to be performed. - /// - private List activeSubdivisions = new List(); + // Update the position of the selector. + if (currentMode == Mode.PLANE_SUBDIVIDE) + { + selector.SelectMeshAtPosition(peltzerController.LastPositionModel, Selector.MESHES_ONLY); + } + else + { + selector.SelectAtPosition(peltzerController.LastPositionModel, Selector.FACES_ONLY); + } + selector.UpdateInactive(Selector.EDGES_ONLY); - /// - /// The current mode the tool is in, such as loop subdivide or single face subdivide. - /// - private Mode currentMode = Mode.SINGLE_FACE_SUBDIVIDE; + // Clean up all temp edges. + foreach (EdgeTemporaryStyle.TemporaryEdge tempEdge in currentTemporaryEdges) + { + PeltzerMain.Instance.highlightUtils.TurnOff(tempEdge); + } + currentTemporaryEdges.Clear(); - /// - /// If true, the trigger can be held to transition to a different tool. Currently only - /// used when loop subdivide is enabled. - /// - private bool pressAndHoldEnabled { - get { return Features.loopSubdivideEnabled; } - } + UpdateHighlights(isNewMode); + } - /// - /// Keeps track of the user holding the trigger button. Needed to transition the tool - /// into other modes such as loop subdivide. - /// - private bool isTriggerBeingHeld; + private void ControllerModeChangedHandler(ControllerMode oldMode, ControllerMode newMode) + { + if (oldMode == ControllerMode.subdivideFace) + { + ClearState(); + UnsetAllHoverTooltips(); + } + } - /// - /// The time the user started holding the trigger button, only set if isTriggerBeingHeld - /// is true. - /// - private float triggerHoldStartTime; + public bool IsSubdividing() + { + return activeSubdivisions.Count > 0; + } - /// - /// Whether we are snapping. - /// - private bool isSnapping; + private void UpdateHighlights(bool isNewMode) + { + activeSubdivisions.Clear(); + + switch (currentMode) + { + case Mode.LOOP_SUBDIVIDE: + UpdateHighlightsForLoopSubdivide(isNewMode); + break; + case Mode.PLANE_SUBDIVIDE: + UpdateHighlightsForPlaneSubdivide(); + break; + case Mode.SINGLE_FACE_SUBDIVIDE: + UpdateHighlightsForSingleFaceSubdivide(); + break; + default: + break; + } + } - private WorldSpace worldSpace; + private void CleanUpAfterMeshSelectionEnds() + { + selectedMeshes.Clear(); + Destroy(guidanceMesh); + guidanceMesh = null; + PeltzerMain.Instance.highlightUtils.ClearTemporaryEdges(); + } - /// - /// The list of meshes that are currently selected. - /// - private List selectedMeshes = new List(); + /// + /// Update highlights for a loop subdivide operation. + /// + private void UpdateHighlightsForLoopSubdivide(bool isNewMode) + { + if (selector.hoverFace == null) + { + CleanUpAfterMeshSelectionEnds(); + return; + } - /// - /// A GameObject which is used to indicate something to the user, like a gizmo. Currently used by the planeSubdivider - /// to show a plane representing the cut. - /// - GameObject guidanceMesh; + List hoverMeshes = new List(); + hoverMeshes.Add(model.GetMesh(selector.hoverFace.meshId)); + + // Check if the list of selected meshes has changed, ignoring ordering. + bool selectionChanged = (hoverMeshes.Count != selectedMeshes.Count) || hoverMeshes.Except(selectedMeshes).Any(); + if (selectionChanged || isNewMode) + { + selectedMeshes = hoverMeshes; + // Update our cache of edge keys to faces. + edgeKeysToFaceIds.Clear(); + foreach (MMesh mesh in selectedMeshes) + { + foreach (var keyValue in MeshUtil.ComputeEdgeKeysToFaceIdsMap(mesh)) + { + edgeKeysToFaceIds[keyValue.Key] = keyValue.Value; + } + } + } - /// - /// Keeps track of the faces that each edge in the mesh is connecting. This is needed to find - /// the next face when performing loop subdivide operations. - /// - private Dictionary> edgeKeysToFaceIds = new Dictionary>(); + Subdivision initialFaceSubdivision = GetInitialFaceSubdivision(); - /// - /// The edge hints currently being shown. - /// - private List currentTemporaryEdges; + // A percentage representing how far along each edge the points of the subdivision will occur. + float loopSubdivideEdgeCutPercentage; - /// - /// After a subdivision, we briefly flash an edge highlight to show success. This queue manages turning off the - /// highlights once they've flashed. - /// - Queue> highlightsToTurnOff = new Queue>(); - - /// - /// Used to determine if we should show the snap tooltip or not. Don't show the tooltip if the user already - /// showed enough knowledge of how to snap. - /// - private int completedSnaps = 0; - private const int SNAP_KNOW_HOW_COUNT = 3; + // The edge from which this loop subdivision operation will begin. + EdgeKey initialEdge; + ComputeLoopSubdivideParameters( + initialFaceSubdivision, + out loopSubdivideEdgeCutPercentage, + out initialEdge); - /// - /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. - /// As such, this setup method must be called before the tool is used for it to have a valid state. - /// - public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, - PaletteController paletteController, Selector selector, AudioLibrary audioLibrary, WorldSpace worldSpace) { - this.model = model; - this.controllerMain = controllerMain; - this.peltzerController = peltzerController; - this.selector = selector; - this.audioLibrary = audioLibrary; - this.worldSpace = worldSpace; - - controllerMain.ControllerActionHandler += ControllerEventHandler; - peltzerController.ModeChangedHandler += ControllerModeChangedHandler; - - currentTemporaryEdges = new List(); - currentMode = Mode.SINGLE_FACE_SUBDIVIDE; - } + // If we are snapping or in block mode divide each face right through the middle. + if (isSnapping || peltzerController.isBlockMode) + { + loopSubdivideEdgeCutPercentage = 0.5f; + } - void Update() { - // We need to clean up any 'thrown away' objects when their time comes (even if the user has changed mode). - while (highlightsToTurnOff.Count > 0 && highlightsToTurnOff.Peek().Key < Time.time) { - KeyValuePair highlightToTurnOff = highlightsToTurnOff.Dequeue(); - PeltzerMain.Instance.highlightUtils.TurnOff(highlightToTurnOff.Value); - } - - if (!PeltzerController.AcquireIfNecessary(ref peltzerController) || peltzerController.mode != ControllerMode.subdivideFace) { - return; - } - - bool isNewMode = false; - if (pressAndHoldEnabled - && isTriggerBeingHeld - && Time.time > triggerHoldStartTime + PRESS_AND_HOLD_DELAY) { - currentMode = Mode.LOOP_SUBDIVIDE; - audioLibrary.PlayClip(audioLibrary.genericSelectSound); - peltzerController.TriggerHapticFeedback(); - isTriggerBeingHeld = false; - isNewMode = true; - } - - // Update the position of the selector. - if (currentMode == Mode.PLANE_SUBDIVIDE) { - selector.SelectMeshAtPosition(peltzerController.LastPositionModel, Selector.MESHES_ONLY); - } else { - selector.SelectAtPosition(peltzerController.LastPositionModel, Selector.FACES_ONLY); - } - selector.UpdateInactive(Selector.EDGES_ONLY); - - // Clean up all temp edges. - foreach (EdgeTemporaryStyle.TemporaryEdge tempEdge in currentTemporaryEdges) { - PeltzerMain.Instance.highlightUtils.TurnOff(tempEdge); - } - currentTemporaryEdges.Clear(); - - UpdateHighlights(isNewMode); - } + // Stop the operation if the initial face is not a quad. + // TODO(bug): Look into making this work with triangles in some cases, like a the top of a primitive + // sphere. + if (initialFaceSubdivision.face.vertexIds.Count != 4) + { + return; + } - private void ControllerModeChangedHandler(ControllerMode oldMode, ControllerMode newMode) { - if (oldMode == ControllerMode.subdivideFace) { - ClearState(); - UnsetAllHoverTooltips(); - } - } + // We keep track of the most recent edge the subdivision has passed through. + EdgeKey currentSubdivisionExitEdge; + AdjustSubdivisionForEdgeCutPercentage( + initialFaceSubdivision, + loopSubdivideEdgeCutPercentage, + initialEdge, + out currentSubdivisionExitEdge); + + // Once we come back to an face that has already been part of a subdivision, the operation has completed. + HashSet visitedFaceIds = new HashSet() { initialFaceSubdivision.face.id }; + Subdivision currentSubdivision = initialFaceSubdivision; + + SubdividePoints subdividePoints = null; + + bool nextSubdivisionFound = currentSubdivision != null && currentSubdivisionExitEdge != null; + while (nextSubdivisionFound) + { + // We don't snap subdivisions in loop subdivide operations. Snapping is instead handled by locking the + // edge cut percentage to 50% instead of computing it. + subdividePoints = HandleSingleSubdivision(currentSubdivision, shouldSnapSubdivision: false); + + // Attempt to find the next subdivision. + Subdivision nextSubdivision; + EdgeKey nextSubdivisionExitEdge; + nextSubdivisionFound = FindNextSubdivisionForLoopSubdivide(currentSubdivision, + currentSubdivisionExitEdge, + loopSubdivideEdgeCutPercentage, + subdividePoints, + edgeKeysToFaceIds, + ref visitedFaceIds, + out nextSubdivision, + out nextSubdivisionExitEdge); + + currentSubdivision = nextSubdivision; + currentSubdivisionExitEdge = nextSubdivisionExitEdge; + } + } - public bool IsSubdividing() { - return activeSubdivisions.Count > 0; - } + /// + /// Update highlights for a plane slice subdivision. + /// + private void UpdateHighlightsForPlaneSubdivide() + { + if (selector.hoverFace == null && selector.hoverMeshes.Count == 0) + { + CleanUpAfterMeshSelectionEnds(); + return; + } - private void UpdateHighlights(bool isNewMode) { - activeSubdivisions.Clear(); - - switch (currentMode) { - case Mode.LOOP_SUBDIVIDE: - UpdateHighlightsForLoopSubdivide(isNewMode); - break; - case Mode.PLANE_SUBDIVIDE: - UpdateHighlightsForPlaneSubdivide(); - break; - case Mode.SINGLE_FACE_SUBDIVIDE: - UpdateHighlightsForSingleFaceSubdivide(); - break; - default: - break; - } - } + selectedMeshes.Clear(); + foreach (int hoverMeshId in selector.hoverMeshes) + { + selectedMeshes.Add(model.GetMesh(hoverMeshId)); + } - private void CleanUpAfterMeshSelectionEnds() { - selectedMeshes.Clear(); - Destroy(guidanceMesh); - guidanceMesh = null; - PeltzerMain.Instance.highlightUtils.ClearTemporaryEdges(); - } + Vector3 planeNormal = (peltzerController.LastRotationModel * Vector3.up).normalized; + Vector3 planeOffset = peltzerController.LastPositionModel; - /// - /// Update highlights for a loop subdivide operation. - /// - private void UpdateHighlightsForLoopSubdivide(bool isNewMode) { - if (selector.hoverFace == null) { - CleanUpAfterMeshSelectionEnds(); - return; - } - - List hoverMeshes = new List(); - hoverMeshes.Add(model.GetMesh(selector.hoverFace.meshId)); - - // Check if the list of selected meshes has changed, ignoring ordering. - bool selectionChanged = (hoverMeshes.Count != selectedMeshes.Count) || hoverMeshes.Except(selectedMeshes).Any(); - if (selectionChanged || isNewMode) { - selectedMeshes = hoverMeshes; - // Update our cache of edge keys to faces. - edgeKeysToFaceIds.Clear(); - foreach (MMesh mesh in selectedMeshes) { - foreach (var keyValue in MeshUtil.ComputeEdgeKeysToFaceIdsMap(mesh)) { - edgeKeysToFaceIds[keyValue.Key] = keyValue.Value; - } - } - } - - Subdivision initialFaceSubdivision = GetInitialFaceSubdivision(); - - // A percentage representing how far along each edge the points of the subdivision will occur. - float loopSubdivideEdgeCutPercentage; - - // The edge from which this loop subdivision operation will begin. - EdgeKey initialEdge; - ComputeLoopSubdivideParameters( - initialFaceSubdivision, - out loopSubdivideEdgeCutPercentage, - out initialEdge); - - // If we are snapping or in block mode divide each face right through the middle. - if (isSnapping || peltzerController.isBlockMode) { - loopSubdivideEdgeCutPercentage = 0.5f; - } - - // Stop the operation if the initial face is not a quad. - // TODO(bug): Look into making this work with triangles in some cases, like a the top of a primitive - // sphere. - if (initialFaceSubdivision.face.vertexIds.Count != 4) { - return; - } - - // We keep track of the most recent edge the subdivision has passed through. - EdgeKey currentSubdivisionExitEdge; - AdjustSubdivisionForEdgeCutPercentage( - initialFaceSubdivision, - loopSubdivideEdgeCutPercentage, - initialEdge, - out currentSubdivisionExitEdge); - - // Once we come back to an face that has already been part of a subdivision, the operation has completed. - HashSet visitedFaceIds = new HashSet() { initialFaceSubdivision.face.id }; - Subdivision currentSubdivision = initialFaceSubdivision; - - SubdividePoints subdividePoints = null; - - bool nextSubdivisionFound = currentSubdivision != null && currentSubdivisionExitEdge != null; - while (nextSubdivisionFound) { - // We don't snap subdivisions in loop subdivide operations. Snapping is instead handled by locking the - // edge cut percentage to 50% instead of computing it. - subdividePoints = HandleSingleSubdivision(currentSubdivision, shouldSnapSubdivision: false); - - // Attempt to find the next subdivision. - Subdivision nextSubdivision; - EdgeKey nextSubdivisionExitEdge; - nextSubdivisionFound = FindNextSubdivisionForLoopSubdivide(currentSubdivision, - currentSubdivisionExitEdge, - loopSubdivideEdgeCutPercentage, - subdividePoints, - edgeKeysToFaceIds, - ref visitedFaceIds, - out nextSubdivision, - out nextSubdivisionExitEdge); - - currentSubdivision = nextSubdivision; - currentSubdivisionExitEdge = nextSubdivisionExitEdge; - } - } + // TODO(bug): Use Graphics.DrawMesh instead. + Plane plane = new Plane(planeNormal, planeOffset); + if (guidanceMesh == null) + { + guidanceMesh = CreatePlaneGuidanceMesh(); + } - /// - /// Update highlights for a plane slice subdivision. - /// - private void UpdateHighlightsForPlaneSubdivide() { - if (selector.hoverFace == null && selector.hoverMeshes.Count == 0) { - CleanUpAfterMeshSelectionEnds(); - return; - } - - selectedMeshes.Clear(); - foreach (int hoverMeshId in selector.hoverMeshes) { - selectedMeshes.Add(model.GetMesh(hoverMeshId)); - } - - Vector3 planeNormal = (peltzerController.LastRotationModel * Vector3.up).normalized; - Vector3 planeOffset = peltzerController.LastPositionModel; - - // TODO(bug): Use Graphics.DrawMesh instead. - Plane plane = new Plane(planeNormal, planeOffset); - if (guidanceMesh == null) { - guidanceMesh = CreatePlaneGuidanceMesh(); - } - - guidanceMesh.transform.position = worldSpace.ModelToWorld(planeOffset); - guidanceMesh.transform.rotation = worldSpace.ModelOrientationToWorld(peltzerController.LastRotationModel); - - // Go through each face in each mesh and find intersection points with the plane. - foreach (MMesh mesh in selectedMeshes) { - foreach (Face face in mesh.GetFaces()) { - - List intersectionPoints = new List(); - List vertexPositions = new List(); - - for (int i = 0; i < face.vertexIds.Count; i++) { - vertexPositions.Add(mesh.VertexPositionInModelCoords(mesh.GetVertex(face.vertexIds[i]).id)); - } - - FindFacePlaneIntersection(plane, vertexPositions, out intersectionPoints); - if (intersectionPoints.Count >= 2) { - Subdivision subdivision = new Subdivision(); - subdivision.face = face; - subdivision.mesh = mesh; - subdivision.startPoint = intersectionPoints[0]; - subdivision.sliceDirection = intersectionPoints[1] - intersectionPoints[0]; - HandleSingleSubdivision(subdivision, false); - } + guidanceMesh.transform.position = worldSpace.ModelToWorld(planeOffset); + guidanceMesh.transform.rotation = worldSpace.ModelOrientationToWorld(peltzerController.LastRotationModel); + + // Go through each face in each mesh and find intersection points with the plane. + foreach (MMesh mesh in selectedMeshes) + { + foreach (Face face in mesh.GetFaces()) + { + + List intersectionPoints = new List(); + List vertexPositions = new List(); + + for (int i = 0; i < face.vertexIds.Count; i++) + { + vertexPositions.Add(mesh.VertexPositionInModelCoords(mesh.GetVertex(face.vertexIds[i]).id)); + } + + FindFacePlaneIntersection(plane, vertexPositions, out intersectionPoints); + if (intersectionPoints.Count >= 2) + { + Subdivision subdivision = new Subdivision(); + subdivision.face = face; + subdivision.mesh = mesh; + subdivision.startPoint = intersectionPoints[0]; + subdivision.sliceDirection = intersectionPoints[1] - intersectionPoints[0]; + HandleSingleSubdivision(subdivision, false); + } + } + } } - } - } - - /// - /// Update highlights for a single face subdivision (i.e. plane subdivider and loop subdivider are disabled). - /// - private void UpdateHighlightsForSingleFaceSubdivide() { - if (selector.hoverFace == null) { - CleanUpAfterMeshSelectionEnds(); - return; - } - - selectedMeshes.Clear(); - selectedMeshes.Add(model.GetMesh(selector.hoverFace.meshId)); - - Subdivision singleSubdivision = GetInitialFaceSubdivision(); - if (singleSubdivision != null) { - HandleSingleSubdivision(singleSubdivision, - isSnapping || peltzerController.isBlockMode /* shouldSnapSubdivision */); - } - } - - /// - /// Creates a GameObject to guide the user when using the plane subdivide feature. - /// - private GameObject CreatePlaneGuidanceMesh() { - GameObject container = new GameObject(); - GameObject plane1 = GameObject.CreatePrimitive(PrimitiveType.Plane); - GameObject plane2 = GameObject.CreatePrimitive(PrimitiveType.Plane); - plane2.transform.localRotation *= Quaternion.AngleAxis(180, Vector3.right); - plane1.transform.parent = container.transform; - plane2.transform.parent = container.transform; - container.transform.localScale = new Vector3(0.03f, 0.03f, 0.03f); - return container; - } - /// - /// Creates a temp edge to highlight the current subdivision and handles snapping. - /// - /// The subdivision to consider. - /// - /// Whether the tool should attempt to snap the subdivision to the interesting points of the face it subdivides - /// (vertices and edge bisectors). - /// - /// The SubdivisionPoints representing that new edge that the subdivision will create. - private SubdividePoints HandleSingleSubdivision(Subdivision subdivision, bool shouldSnapSubdivision) { - activeSubdivisions.Add(subdivision); - - // Finding the points that the subdivision line interest with the edges of the face. - SubdividePoints edgeIntersectionPoints; - GetSubdividePoints(subdivision.face, subdivision.mesh, subdivision.startPoint, - subdivision.sliceDirection.normalized, out edgeIntersectionPoints); - - if (shouldSnapSubdivision) { - SnapSubdivision(subdivision, edgeIntersectionPoints); - // We need to recompute this because we just modified the subdivision. - GetSubdividePoints(subdivision.face, subdivision.mesh, subdivision.startPoint, - subdivision.sliceDirection.normalized, out edgeIntersectionPoints); - } - - // Create the temp edge and add it to the active list. - EdgeTemporaryStyle.TemporaryEdge tempEdge = new EdgeTemporaryStyle.TemporaryEdge(); - tempEdge.vertex1PositionModelSpace = edgeIntersectionPoints.point1; - tempEdge.vertex2PositionModelSpace = edgeIntersectionPoints.point2; - PeltzerMain.Instance.highlightUtils.TurnOn(tempEdge); - PeltzerMain.Instance.highlightUtils.SetTemporaryEdgeStyleToSelect(tempEdge); - currentTemporaryEdges.Add(tempEdge); - - return edgeIntersectionPoints; - } + /// + /// Update highlights for a single face subdivision (i.e. plane subdivider and loop subdivider are disabled). + /// + private void UpdateHighlightsForSingleFaceSubdivide() + { + if (selector.hoverFace == null) + { + CleanUpAfterMeshSelectionEnds(); + return; + } - private Subdivision GetInitialFaceSubdivision() { - FaceKey initialFaceKey = selector.hoverFace; - MMesh mesh = model.GetMesh(selector.hoverFace.meshId); - Face initialFace = mesh.GetFace(initialFaceKey.faceId); - - Subdivision initialFaceSubdivision = new Subdivision(); - initialFaceSubdivision.mesh = mesh; - initialFaceSubdivision.face = initialFace; - - // Project the position of the controller onto the plane of the current face. - List vertices = new List(initialFace.vertexIds.Count); - for (int i = 0; i < initialFace.vertexIds.Count; i++) { - int id = initialFace.vertexIds[i]; - vertices.Add(mesh.VertexPositionInModelCoords(id)); - } - Vector3 closestPointOnFace = Math3d.ProjectPointOnPlane( - MeshMath.CalculateNormal(vertices), - mesh.VertexPositionInModelCoords(initialFace.vertexIds[0]), - peltzerController.LastPositionModel); - initialFaceSubdivision.startPoint = closestPointOnFace; - - // Find the direction the subdivide will slice the initial face. - // We use Vector3.right because the resting position should be a horizontal line over a face in front of the - // camera. - initialFaceSubdivision.sliceDirection = peltzerController.LastRotationModel * Vector3.right; - - return initialFaceSubdivision; - } + selectedMeshes.Clear(); + selectedMeshes.Add(model.GetMesh(selector.hoverFace.meshId)); - /// - /// Given a face with 4 verts and an edge, find the opposite edge. - /// This can be expanded in the future to support faces with more than 4 verts. - /// - private EdgeKey FindOppositeEdge(Face face, EdgeKey input) { - if (face.vertexIds.Count != 4) { - Debug.LogError("Unable to find opposite edge on a non quad face: " + face.id); - } - int id1 = -1, id2 = -1; - bool id1Set = false; - foreach (int vertexId in face.vertexIds) { - if (input.ContainsVertex(vertexId)) { - continue; - } - if (!id1Set) { - id1 = vertexId; - id1Set = true; - } else { - id2 = vertexId; - break; + Subdivision singleSubdivision = GetInitialFaceSubdivision(); + if (singleSubdivision != null) + { + HandleSingleSubdivision(singleSubdivision, + isSnapping || peltzerController.isBlockMode /* shouldSnapSubdivision */); + } } - } - return new EdgeKey(input.meshId, id1, id2); - } - /// - /// Given a the current subdivision, some connectivity information and a list of visited faces, attempts to find - /// the next subdivision to be performed in a loop subdivision operation. - /// - /// The last subdivision of the chain so far. - /// The exit edge of currentSubdivision. - /// The percentage to cut the face at. - /// The SubdividePoints associated with currentSubdivision. - /// The dict containing the mapping from edges to face IDs for the mesh. - /// - /// A collection of faces to exclude because they have been visited already by previous subdivisions in the chain. - /// - /// The next subdivision in the chain, or null if this method can't find one. - /// - /// The exit edge for nextSubdivision, or null if this method can't find one. - /// - /// Whether a suitable subdivision was found to continue the chain. - private bool FindNextSubdivisionForLoopSubdivide(Subdivision currentSubdivision, - EdgeKey currentSubdivisionExitEdge, - float loopSubdivideEdgeCutPercentage, - SubdividePoints currentSubdividePoints, - Dictionary> edgeKeysToFaceIds, - ref HashSet visitedFaceIds, - out Subdivision nextSubdivision, - out EdgeKey nextSubdivisionExitEdge) { - List faceIds; - - // Attempt to figure out the next face to be subdivided. - // We find the 2 faces connected to the exit edge of the current subdivision and - // find a face that we haven't visited (i.e. the next one). We may have already - // visited both faces in which case the operation is complete (we looped around). - // Then we find the start point of the new subdivision, which is the exit point of - // the current one. Finally, we adjust the new subdivision according to the - // loopSubdivideEdgeCutPercentage, get its exit edge and return. - edgeKeysToFaceIds.TryGetValue(currentSubdivisionExitEdge, out faceIds); - foreach (int faceId in faceIds) { - if (currentSubdivision.face.id != faceId && !visitedFaceIds.Contains(faceId)) { - visitedFaceIds.Add(faceId); - - Vector3 startPoint = currentSubdivisionExitEdge == currentSubdividePoints.edge1 - ? currentSubdividePoints.point1 - : currentSubdividePoints.point2; - - nextSubdivision = new Subdivision(); - nextSubdivision.mesh = currentSubdivision.mesh; - nextSubdivision.face = currentSubdivision.mesh.GetFace(faceId); - nextSubdivision.startPoint = startPoint; - - // Stop the operation if we hit anything that is not a quad. - // TODO(bug): Look into making this work with triangles in some cases, like a the top of a primitive - // sphere. - if (nextSubdivision.face.vertexIds.Count != 4) { - continue; - } - - AdjustSubdivisionForEdgeCutPercentage( - nextSubdivision, - loopSubdivideEdgeCutPercentage, - currentSubdivisionExitEdge /* originEdge */, - out nextSubdivisionExitEdge); - - // All done, we found the next subdivision and exit edge. - return true; + /// + /// Creates a GameObject to guide the user when using the plane subdivide feature. + /// + private GameObject CreatePlaneGuidanceMesh() + { + GameObject container = new GameObject(); + GameObject plane1 = GameObject.CreatePrimitive(PrimitiveType.Plane); + GameObject plane2 = GameObject.CreatePrimitive(PrimitiveType.Plane); + plane2.transform.localRotation *= Quaternion.AngleAxis(180, Vector3.right); + plane1.transform.parent = container.transform; + plane2.transform.parent = container.transform; + container.transform.localScale = new Vector3(0.03f, 0.03f, 0.03f); + return container; } - } - // Could not find the next subdivision. - nextSubdivision = null; - nextSubdivisionExitEdge = null; - return false; - } + /// + /// Creates a temp edge to highlight the current subdivision and handles snapping. + /// + /// The subdivision to consider. + /// + /// Whether the tool should attempt to snap the subdivision to the interesting points of the face it subdivides + /// (vertices and edge bisectors). + /// + /// The SubdivisionPoints representing that new edge that the subdivision will create. + private SubdividePoints HandleSingleSubdivision(Subdivision subdivision, bool shouldSnapSubdivision) + { + activeSubdivisions.Add(subdivision); + + // Finding the points that the subdivision line interest with the edges of the face. + SubdividePoints edgeIntersectionPoints; + GetSubdividePoints(subdivision.face, subdivision.mesh, subdivision.startPoint, + subdivision.sliceDirection.normalized, out edgeIntersectionPoints); + + if (shouldSnapSubdivision) + { + SnapSubdivision(subdivision, edgeIntersectionPoints); + // We need to recompute this because we just modified the subdivision. + GetSubdividePoints(subdivision.face, subdivision.mesh, subdivision.startPoint, + subdivision.sliceDirection.normalized, out edgeIntersectionPoints); + } - /// - /// Modifies the provided subdivision to cut between the edge that is closest to the subdivision's - /// startPoint and the opposite edge in the face (assumming a quad). - /// - /// This works by taking the originEdge, and starting the subdivision along it according to - /// edgeCutPercentage, then ending the subdivision on (1 - edgeCutPercentage) on the exit - /// edge and returning it to the caller so that it can be passed as the origin edge for - /// the next subdivision in the chain. In other words, the exitEdge of a subdivision - /// becomes the originEdge of the next one in the chain. - /// - /// The subdivision to be modified. - /// - /// A value in (0,1) to indicate the point in which the cut should occur along the edge. - /// - /// The origin edge of the current subdivision. - /// - /// The computed exit edge of the current subdivision to be passed to the next one. - /// - private void AdjustSubdivisionForEdgeCutPercentage(Subdivision subdivision, - float edgeCutPercentage, - EdgeKey originEdge, - out EdgeKey exitEdge) { - - MMesh mesh = subdivision.mesh; - exitEdge = FindOppositeEdge(subdivision.face, originEdge); - - // The orientation of these points within an edge is not a concern here because - // GetFaceVertexIndicesForEdge() will get us the indices as they appear in clockwise - // manner along the face. This means that the vertices indices will be flipped for - // a face in which they appear on the exitEdge, and the next subdivision on the - // chain, in which the same edge will be the origin edge instead. - - // Points on the origin edge. - int[] originEdgeIndices = GetFaceVertexIndicesForEdge(subdivision.face, originEdge); - Vector3 v1 = mesh.VertexPositionInModelCoords(subdivision.face.vertexIds[originEdgeIndices[0]]); - Vector3 v2 = mesh.VertexPositionInModelCoords(subdivision.face.vertexIds[originEdgeIndices[1]]); - - // Points on the exit edge. - int[] exitEdgeIndices = GetFaceVertexIndicesForEdge(subdivision.face, exitEdge); - Vector3 op1 = mesh.VertexPositionInModelCoords(subdivision.face.vertexIds[exitEdgeIndices[0]]); - Vector3 op2 = mesh.VertexPositionInModelCoords(subdivision.face.vertexIds[exitEdgeIndices[1]]); - - var originEdgeCutPercentage = edgeCutPercentage; - var exitEdgeCutPercentage = 1 - edgeCutPercentage; - - subdivision.startPoint = v1 + originEdgeCutPercentage * (v2 - v1); - Vector3 target = op1 + exitEdgeCutPercentage * (op2 - op1); - subdivision.sliceDirection = target - subdivision.startPoint; - } + // Create the temp edge and add it to the active list. + EdgeTemporaryStyle.TemporaryEdge tempEdge = new EdgeTemporaryStyle.TemporaryEdge(); + tempEdge.vertex1PositionModelSpace = edgeIntersectionPoints.point1; + tempEdge.vertex2PositionModelSpace = edgeIntersectionPoints.point2; + PeltzerMain.Instance.highlightUtils.TurnOn(tempEdge); + PeltzerMain.Instance.highlightUtils.SetTemporaryEdgeStyleToSelect(tempEdge); + currentTemporaryEdges.Add(tempEdge); - /// - /// Given a subdivision, finds the closest edge to the cursor and the edgeCutPercentage along that edge. - /// - /// The subdivision used to compute the edgeCutPercentage. - /// - /// A value in (0,1) along origin edge at which the subdivision should be adjusted. - /// - /// - /// The origin edge of the subdivision (i.e. The edge closest to the subdivision's startPoint). - /// - private void ComputeLoopSubdivideParameters(Subdivision subdivision, - out float edgeCutPercentage, - out EdgeKey originEdge) { - - SubdividePoints subdividePoints; - MMesh mesh = subdivision.mesh; - GetSubdividePoints(subdivision.face, - subdivision.mesh, - subdivision.startPoint, - subdivision.sliceDirection.normalized, - out subdividePoints); - - float d1 = Vector3.Distance(subdivision.startPoint, subdividePoints.point1); - float d2 = Vector3.Distance(subdivision.startPoint, subdividePoints.point2); - - Vector3 closestPoint; - - // Choose the subdivide point closest to the startPoint. - if (d1 < d2) { - closestPoint = subdividePoints.point1; - originEdge = subdividePoints.edge1; - } else { - closestPoint = subdividePoints.point2; - originEdge = subdividePoints.edge2; - } - - // Points on the origin edge. - int[] originEdgeIndices = GetFaceVertexIndicesForEdge(subdivision.face, originEdge); - Vector3 v1 = mesh.VertexPositionInModelCoords(subdivision.face.vertexIds[originEdgeIndices[0]]); - Vector3 v2 = mesh.VertexPositionInModelCoords(subdivision.face.vertexIds[originEdgeIndices[1]]); - - edgeCutPercentage = (closestPoint - v1).magnitude / (v2 - v1).magnitude; - } - - /// - /// Given a face and an edge key, returns an array with 2 elements, containing the indices of - /// the verts in the edge in clockwise order as they appear on the face. - /// - private int[] GetFaceVertexIndicesForEdge(Face face, EdgeKey edge) { - int lastIndex = face.vertexIds.Count - 1; - if (edge.ContainsVertex(face.vertexIds[0]) && edge.ContainsVertex(face.vertexIds[lastIndex])) { - return new int[] { lastIndex, 0 }; - } - for (int i = 0; i < lastIndex; i++) { - if (edge.ContainsVertex(face.vertexIds[i]) && edge.ContainsVertex(face.vertexIds[i + 1])) { - return new int[] { i, i + 1 }; + return edgeIntersectionPoints; } - } - Debug.LogError("Edge not in face: (" + edge.vertexId1 + ", " + edge.vertexId2 + "), in face " + face.id); - return null; - } - - private List FindInterestingFacePoints(List coplanarVertices) { - // Add all the vertices to facePoints. - List facePoints = new List(coplanarVertices); - - // Add all the edge segment bisectors to facePoints. - facePoints.AddRange(MeshMath.CalculateEdgeBisectors(coplanarVertices)); - // Remove all colinear vertices from the face. - List corners = MeshMath.FindCornerVertices(coplanarVertices); - - // Find the edgeBisectors for a full edge. - foreach (Vector3 edgeBisector in MeshMath.CalculateEdgeBisectors(corners)) { - if (!facePoints.Contains(edgeBisector)) - facePoints.Add(edgeBisector); - } - - // Find the center of the face. - facePoints.Add(MeshMath.CalculateGeometricCenter(corners)); - - return facePoints; - } + private Subdivision GetInitialFaceSubdivision() + { + FaceKey initialFaceKey = selector.hoverFace; + MMesh mesh = model.GetMesh(selector.hoverFace.meshId); + Face initialFace = mesh.GetFace(initialFaceKey.faceId); + + Subdivision initialFaceSubdivision = new Subdivision(); + initialFaceSubdivision.mesh = mesh; + initialFaceSubdivision.face = initialFace; + + // Project the position of the controller onto the plane of the current face. + List vertices = new List(initialFace.vertexIds.Count); + for (int i = 0; i < initialFace.vertexIds.Count; i++) + { + int id = initialFace.vertexIds[i]; + vertices.Add(mesh.VertexPositionInModelCoords(id)); + } + Vector3 closestPointOnFace = Math3d.ProjectPointOnPlane( + MeshMath.CalculateNormal(vertices), + mesh.VertexPositionInModelCoords(initialFace.vertexIds[0]), + peltzerController.LastPositionModel); + initialFaceSubdivision.startPoint = closestPointOnFace; + + // Find the direction the subdivide will slice the initial face. + // We use Vector3.right because the resting position should be a horizontal line over a face in front of the + // camera. + initialFaceSubdivision.sliceDirection = peltzerController.LastRotationModel * Vector3.right; + + return initialFaceSubdivision; + } - /// - /// Makes only the supplied tooltip visible and ensures the others are off. - /// - /// The tooltip text to activate. - /// The hover state. - private void SetHoverTooltip(GameObject tooltip, TouchpadHoverState state) { - if (!tooltip.activeSelf) { - UnsetAllHoverTooltips(); - tooltip.SetActive(true); - peltzerController.SetTouchpadHoverTexture(state); - peltzerController.TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_1, - 0.003f, - 0.15f - ); - } - } + /// + /// Given a face with 4 verts and an edge, find the opposite edge. + /// This can be expanded in the future to support faces with more than 4 verts. + /// + private EdgeKey FindOppositeEdge(Face face, EdgeKey input) + { + if (face.vertexIds.Count != 4) + { + Debug.LogError("Unable to find opposite edge on a non quad face: " + face.id); + } + int id1 = -1, id2 = -1; + bool id1Set = false; + foreach (int vertexId in face.vertexIds) + { + if (input.ContainsVertex(vertexId)) + { + continue; + } + if (!id1Set) + { + id1 = vertexId; + id1Set = true; + } + else + { + id2 = vertexId; + break; + } + } + return new EdgeKey(input.meshId, id1, id2); + } - /// - /// Unset all of the touchpad hover text tooltips. - /// - private void UnsetAllHoverTooltips() { - peltzerController.controllerGeometry.modifyTooltipUp.SetActive(false); - peltzerController.controllerGeometry.modifyTooltipLeft.SetActive(false); - peltzerController.controllerGeometry.modifyTooltipRight.SetActive(false); - peltzerController.controllerGeometry.resizeUpTooltip.SetActive(false); - peltzerController.controllerGeometry.resizeDownTooltip.SetActive(false); - peltzerController.SetTouchpadHoverTexture(TouchpadHoverState.NONE); - } + /// + /// Given a the current subdivision, some connectivity information and a list of visited faces, attempts to find + /// the next subdivision to be performed in a loop subdivision operation. + /// + /// The last subdivision of the chain so far. + /// The exit edge of currentSubdivision. + /// The percentage to cut the face at. + /// The SubdividePoints associated with currentSubdivision. + /// The dict containing the mapping from edges to face IDs for the mesh. + /// + /// A collection of faces to exclude because they have been visited already by previous subdivisions in the chain. + /// + /// The next subdivision in the chain, or null if this method can't find one. + /// + /// The exit edge for nextSubdivision, or null if this method can't find one. + /// + /// Whether a suitable subdivision was found to continue the chain. + private bool FindNextSubdivisionForLoopSubdivide(Subdivision currentSubdivision, + EdgeKey currentSubdivisionExitEdge, + float loopSubdivideEdgeCutPercentage, + SubdividePoints currentSubdividePoints, + Dictionary> edgeKeysToFaceIds, + ref HashSet visitedFaceIds, + out Subdivision nextSubdivision, + out EdgeKey nextSubdivisionExitEdge) + { + List faceIds; + + // Attempt to figure out the next face to be subdivided. + // We find the 2 faces connected to the exit edge of the current subdivision and + // find a face that we haven't visited (i.e. the next one). We may have already + // visited both faces in which case the operation is complete (we looped around). + // Then we find the start point of the new subdivision, which is the exit point of + // the current one. Finally, we adjust the new subdivision according to the + // loopSubdivideEdgeCutPercentage, get its exit edge and return. + edgeKeysToFaceIds.TryGetValue(currentSubdivisionExitEdge, out faceIds); + foreach (int faceId in faceIds) + { + if (currentSubdivision.face.id != faceId && !visitedFaceIds.Contains(faceId)) + { + visitedFaceIds.Add(faceId); + + Vector3 startPoint = currentSubdivisionExitEdge == currentSubdividePoints.edge1 + ? currentSubdividePoints.point1 + : currentSubdividePoints.point2; + + nextSubdivision = new Subdivision(); + nextSubdivision.mesh = currentSubdivision.mesh; + nextSubdivision.face = currentSubdivision.mesh.GetFace(faceId); + nextSubdivision.startPoint = startPoint; + + // Stop the operation if we hit anything that is not a quad. + // TODO(bug): Look into making this work with triangles in some cases, like a the top of a primitive + // sphere. + if (nextSubdivision.face.vertexIds.Count != 4) + { + continue; + } + + AdjustSubdivisionForEdgeCutPercentage( + nextSubdivision, + loopSubdivideEdgeCutPercentage, + currentSubdivisionExitEdge /* originEdge */, + out nextSubdivisionExitEdge); + + // All done, we found the next subdivision and exit edge. + return true; + } + } - public void ResetOverlays() { - peltzerController.ShowModifyOverlays(); - peltzerController.ShowTooltips(); - } + // Could not find the next subdivision. + nextSubdivision = null; + nextSubdivisionExitEdge = null; + return false; + } - private void FinishSubdivide() { - SubdivideFaces(activeSubdivisions); - ResetOverlays(); - ClearState(); - } + /// + /// Modifies the provided subdivision to cut between the edge that is closest to the subdivision's + /// startPoint and the opposite edge in the face (assumming a quad). + /// + /// This works by taking the originEdge, and starting the subdivision along it according to + /// edgeCutPercentage, then ending the subdivision on (1 - edgeCutPercentage) on the exit + /// edge and returning it to the caller so that it can be passed as the origin edge for + /// the next subdivision in the chain. In other words, the exitEdge of a subdivision + /// becomes the originEdge of the next one in the chain. + /// + /// The subdivision to be modified. + /// + /// A value in (0,1) to indicate the point in which the cut should occur along the edge. + /// + /// The origin edge of the current subdivision. + /// + /// The computed exit edge of the current subdivision to be passed to the next one. + /// + private void AdjustSubdivisionForEdgeCutPercentage(Subdivision subdivision, + float edgeCutPercentage, + EdgeKey originEdge, + out EdgeKey exitEdge) + { + + MMesh mesh = subdivision.mesh; + exitEdge = FindOppositeEdge(subdivision.face, originEdge); + + // The orientation of these points within an edge is not a concern here because + // GetFaceVertexIndicesForEdge() will get us the indices as they appear in clockwise + // manner along the face. This means that the vertices indices will be flipped for + // a face in which they appear on the exitEdge, and the next subdivision on the + // chain, in which the same edge will be the origin edge instead. + + // Points on the origin edge. + int[] originEdgeIndices = GetFaceVertexIndicesForEdge(subdivision.face, originEdge); + Vector3 v1 = mesh.VertexPositionInModelCoords(subdivision.face.vertexIds[originEdgeIndices[0]]); + Vector3 v2 = mesh.VertexPositionInModelCoords(subdivision.face.vertexIds[originEdgeIndices[1]]); + + // Points on the exit edge. + int[] exitEdgeIndices = GetFaceVertexIndicesForEdge(subdivision.face, exitEdge); + Vector3 op1 = mesh.VertexPositionInModelCoords(subdivision.face.vertexIds[exitEdgeIndices[0]]); + Vector3 op2 = mesh.VertexPositionInModelCoords(subdivision.face.vertexIds[exitEdgeIndices[1]]); + + var originEdgeCutPercentage = edgeCutPercentage; + var exitEdgeCutPercentage = 1 - edgeCutPercentage; + + subdivision.startPoint = v1 + originEdgeCutPercentage * (v2 - v1); + Vector3 target = op1 + exitEdgeCutPercentage * (op2 - op1); + subdivision.sliceDirection = target - subdivision.startPoint; + } - /// - /// Resets the subdivide tool to its default state. - /// - public void ClearState() { - PeltzerMain.Instance.highlightUtils.ClearTemporaryEdges(); - selectedMeshes.Clear(); - Destroy(guidanceMesh); - guidanceMesh = null; - activeSubdivisions.Clear(); - - if (Features.planeSubdivideEnabled) { - currentMode = Mode.PLANE_SUBDIVIDE; - } else { - currentMode = Mode.SINGLE_FACE_SUBDIVIDE; - } - } + /// + /// Given a subdivision, finds the closest edge to the cursor and the edgeCutPercentage along that edge. + /// + /// The subdivision used to compute the edgeCutPercentage. + /// + /// A value in (0,1) along origin edge at which the subdivision should be adjusted. + /// + /// + /// The origin edge of the subdivision (i.e. The edge closest to the subdivision's startPoint). + /// + private void ComputeLoopSubdivideParameters(Subdivision subdivision, + out float edgeCutPercentage, + out EdgeKey originEdge) + { + + SubdividePoints subdividePoints; + MMesh mesh = subdivision.mesh; + GetSubdividePoints(subdivision.face, + subdivision.mesh, + subdivision.startPoint, + subdivision.sliceDirection.normalized, + out subdividePoints); + + float d1 = Vector3.Distance(subdivision.startPoint, subdividePoints.point1); + float d2 = Vector3.Distance(subdivision.startPoint, subdividePoints.point2); + + Vector3 closestPoint; + + // Choose the subdivide point closest to the startPoint. + if (d1 < d2) + { + closestPoint = subdividePoints.point1; + originEdge = subdividePoints.edge1; + } + else + { + closestPoint = subdividePoints.point2; + originEdge = subdividePoints.edge2; + } - private bool IsStartPressAndHoldEvent(ControllerEventArgs args) { - return pressAndHoldEnabled - && args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN; - } + // Points on the origin edge. + int[] originEdgeIndices = GetFaceVertexIndicesForEdge(subdivision.face, originEdge); + Vector3 v1 = mesh.VertexPositionInModelCoords(subdivision.face.vertexIds[originEdgeIndices[0]]); + Vector3 v2 = mesh.VertexPositionInModelCoords(subdivision.face.vertexIds[originEdgeIndices[1]]); - private bool IsFinishSubdivideEvent(ControllerEventArgs args) { - // Normally, we want the operation to complete as soon as the trigger - // is down, but when press and hold is enabled we want to wait until - // the trigger is released. - ButtonAction buttonActionToCheck = pressAndHoldEnabled ? - ButtonAction.UP : ButtonAction.DOWN; - - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == buttonActionToCheck - && IsSubdividing(); - } + edgeCutPercentage = (closestPoint - v1).magnitude / (v2 - v1).magnitude; + } - /// - /// Whether this matches a start snapping event. - /// - /// The controller event arguments. - /// True if the trigger is down. - private static bool IsStartSnapEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN; - } + /// + /// Given a face and an edge key, returns an array with 2 elements, containing the indices of + /// the verts in the edge in clockwise order as they appear on the face. + /// + private int[] GetFaceVertexIndicesForEdge(Face face, EdgeKey edge) + { + int lastIndex = face.vertexIds.Count - 1; + if (edge.ContainsVertex(face.vertexIds[0]) && edge.ContainsVertex(face.vertexIds[lastIndex])) + { + return new int[] { lastIndex, 0 }; + } + for (int i = 0; i < lastIndex; i++) + { + if (edge.ContainsVertex(face.vertexIds[i]) && edge.ContainsVertex(face.vertexIds[i + 1])) + { + return new int[] { i, i + 1 }; + } + } + Debug.LogError("Edge not in face: (" + edge.vertexId1 + ", " + edge.vertexId2 + "), in face " + face.id); + return null; + } - /// - /// Whether this matches an end snapping event. - /// - /// The controller event arguments. - /// True if the trigger is up. - private static bool IsEndSnapEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.UP; - } + private List FindInterestingFacePoints(List coplanarVertices) + { + // Add all the vertices to facePoints. + List facePoints = new List(coplanarVertices); - // Touchpad Hover - private bool IsSetUpHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && !IsSubdividing() - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.TOP; - } + // Add all the edge segment bisectors to facePoints. + facePoints.AddRange(MeshMath.CalculateEdgeBisectors(coplanarVertices)); - private bool IsSetDownHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && !IsSubdividing() - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.BOTTOM; - } + // Remove all colinear vertices from the face. + List corners = MeshMath.FindCornerVertices(coplanarVertices); - private bool IsSetLeftHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && !IsSubdividing() - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.LEFT; - } + // Find the edgeBisectors for a full edge. + foreach (Vector3 edgeBisector in MeshMath.CalculateEdgeBisectors(corners)) + { + if (!facePoints.Contains(edgeBisector)) + facePoints.Add(edgeBisector); + } - private bool IsSetRightHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && !IsSubdividing() - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.RIGHT; - } + // Find the center of the face. + facePoints.Add(MeshMath.CalculateGeometricCenter(corners)); - private static bool IsUnsetAllHoverTooltipsEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.NONE; - } + return facePoints; + } - /// - /// Subdivides the mesh according to the provided list of subdivisions. - /// - private void SubdivideFaces(List subdivisions) { - if (subdivisions.Count == 0) { - return; - } - - // Keys are mesh IDs, values are the actual meshes. - Dictionary modifiedMeshes = new Dictionary(); - - // Keys are mesh IDs, values are collections of IDs of created verts. - // This is needed to reuse verts created over multiple GeometryOperations without - // having to iterate over every vertex in the mesh. - Dictionary> reusableVertsDict = new Dictionary>(); - - foreach (Subdivision subdivision in subdivisions) { - - // If we haven't cloned the mesh associated with this subdivision, clone it, - // otherwise reuse the previously cloned mesh. - MMesh modifiedMesh; - if (!modifiedMeshes.TryGetValue(subdivision.mesh.id, out modifiedMesh)) { - modifiedMesh = subdivision.mesh.Clone(); - modifiedMeshes[subdivision.mesh.id] = modifiedMesh; + /// + /// Makes only the supplied tooltip visible and ensures the others are off. + /// + /// The tooltip text to activate. + /// The hover state. + private void SetHoverTooltip(GameObject tooltip, TouchpadHoverState state) + { + if (!tooltip.activeSelf) + { + UnsetAllHoverTooltips(); + tooltip.SetActive(true); + peltzerController.SetTouchpadHoverTexture(state); + peltzerController.TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_1, + 0.003f, + 0.15f + ); + } } - // Get the set of reusable vertices for this mesh, or create it. - HashSet reusableVertsForCurrentMesh; - if (!reusableVertsDict.TryGetValue(subdivision.mesh.id, out reusableVertsForCurrentMesh)) { - reusableVertsForCurrentMesh = new HashSet(); - reusableVertsDict[subdivision.mesh.id] = reusableVertsForCurrentMesh; + /// + /// Unset all of the touchpad hover text tooltips. + /// + private void UnsetAllHoverTooltips() + { + peltzerController.controllerGeometry.modifyTooltipUp.SetActive(false); + peltzerController.controllerGeometry.modifyTooltipLeft.SetActive(false); + peltzerController.controllerGeometry.modifyTooltipRight.SetActive(false); + peltzerController.controllerGeometry.resizeUpTooltip.SetActive(false); + peltzerController.controllerGeometry.resizeDownTooltip.SetActive(false); + peltzerController.SetTouchpadHoverTexture(TouchpadHoverState.NONE); } - MMesh.GeometryOperation subdivideOperation = modifiedMesh.StartOperation(); + public void ResetOverlays() + { + peltzerController.ShowModifyOverlays(); + peltzerController.ShowTooltips(); + } - Vector3 start = subdivision.startPoint; - Vector3 sliceDirection = subdivision.sliceDirection; + private void FinishSubdivide() + { + SubdivideFaces(activeSubdivisions); + ResetOverlays(); + ClearState(); + } - // Check this is a valid subdivision. - SubdividePoints points; - if (!GetSubdividePoints(subdivision.face, subdivision.mesh, start, sliceDirection.normalized, out points)) { - audioLibrary.PlayClip(audioLibrary.errorSound); - peltzerController.TriggerHapticFeedback(); - return; + /// + /// Resets the subdivide tool to its default state. + /// + public void ClearState() + { + PeltzerMain.Instance.highlightUtils.ClearTemporaryEdges(); + selectedMeshes.Clear(); + Destroy(guidanceMesh); + guidanceMesh = null; + activeSubdivisions.Clear(); + + if (Features.planeSubdivideEnabled) + { + currentMode = Mode.PLANE_SUBDIVIDE; + } + else + { + currentMode = Mode.SINGLE_FACE_SUBDIVIDE; + } } - Face face = subdivision.face; - subdivideOperation.DeleteFace(face.id); - selector.DeselectAll(); + private bool IsStartPressAndHoldEvent(ControllerEventArgs args) + { + return pressAndHoldEnabled + && args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN; + } - // To make this a little bit simpler, order our points to coincide with vertex order on face. - if (points.point1Index > points.point2Index) { - int tmp = points.point1Index; - points.point1Index = points.point2Index; - points.point2Index = tmp; + private bool IsFinishSubdivideEvent(ControllerEventArgs args) + { + // Normally, we want the operation to complete as soon as the trigger + // is down, but when press and hold is enabled we want to wait until + // the trigger is released. + ButtonAction buttonActionToCheck = pressAndHoldEnabled ? + ButtonAction.UP : ButtonAction.DOWN; + + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == buttonActionToCheck + && IsSubdividing(); + } - Vector3 pointTmp = points.point1; - points.point1 = points.point2; - points.point2 = pointTmp; + /// + /// Whether this matches a start snapping event. + /// + /// The controller event arguments. + /// True if the trigger is down. + private static bool IsStartSnapEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN; } - int startVertIdSeg1 = face.vertexIds[points.point1Index]; - int endVertIdSeg1 = face.vertexIds[(points.point1Index + 1) % face.vertexIds.Count]; - int startVertIdSeg2 = face.vertexIds[points.point2Index]; - int endVertIdSeg2 = face.vertexIds[(points.point2Index + 1) % face.vertexIds.Count]; - - // Find the nearest existing vertices within VERTEX_REUSE_DISTANCE_THRESHOLD. If we find one, we - // use the existing vertex instead of creating a new one. If we don't, we create new vertices. - Vertex newVertex1 = null; - Vertex newVertex2 = null; - float nearestSquaredDistanceToNewVertex1 = VERTEX_REUSE_SQUARED_DISTANCE_THRESHOLD; - float nearestSquaredDistanceToNewVertex2 = VERTEX_REUSE_SQUARED_DISTANCE_THRESHOLD; - - // The vertices we need to check (to reuse them) is the union of previously created verts - // (in previous subdivisions) and the vertices in the face of the current subdivision. - HashSet vertsToCheck = new HashSet(); - vertsToCheck.UnionWith(face.vertexIds); - vertsToCheck.UnionWith(reusableVertsForCurrentMesh); - - foreach (int vertexId in vertsToCheck) { - Vertex vertex = modifiedMesh.GetVertex(vertexId); - Vector3 vertexPos = modifiedMesh.VertexPositionInModelCoords(vertexId); - float squaredDistanceistanceToNewVertex1 = Vector3.SqrMagnitude(vertexPos - points.point1); - if (squaredDistanceistanceToNewVertex1 < nearestSquaredDistanceToNewVertex1) { - newVertex1 = vertex; - nearestSquaredDistanceToNewVertex1 = squaredDistanceistanceToNewVertex1; - } - float squaredDistanceToNewVertex2 = Vector3.SqrMagnitude(vertexPos - points.point2); - if (squaredDistanceToNewVertex2 < nearestSquaredDistanceToNewVertex2) { - newVertex2 = vertex; - nearestSquaredDistanceToNewVertex2 = squaredDistanceToNewVertex2; - } + /// + /// Whether this matches an end snapping event. + /// + /// The controller event arguments. + /// True if the trigger is up. + private static bool IsEndSnapEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.UP; } - bool vertex1AlreadyExisted = true; - bool vertex2AlreadyExisted = true; - if (newVertex1 == null) { - newVertex1 = subdivideOperation.AddVertexModelSpace(points.point1); - vertex1AlreadyExisted = false; - reusableVertsForCurrentMesh.Add(newVertex1.id); + // Touchpad Hover + private bool IsSetUpHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && !IsSubdividing() + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.TOP; } - if (newVertex2 == null) { - newVertex2 = subdivideOperation.AddVertexModelSpace(points.point2); - vertex2AlreadyExisted = false; - reusableVertsForCurrentMesh.Add(newVertex2.id); + + private bool IsSetDownHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && !IsSubdividing() + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.BOTTOM; } - // If the two subdivide points are an edge on the subdivide face, don't subdivide. - if (vertex1AlreadyExisted && vertex2AlreadyExisted && VerticesAreEdgeOnFace(newVertex1, newVertex2, face)) { - audioLibrary.PlayClip(audioLibrary.errorSound); - peltzerController.TriggerHapticFeedback(); - return; + private bool IsSetLeftHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && !IsSubdividing() + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.LEFT; } - // Create our new faces. Basically, wind around the face until we hit our "cut" points. When we hit - // one, we make a face. - List newFaceIndices = new List(); - for (int i = 0; i < face.vertexIds.Count; i++) { - if (face.vertexIds[i] != newVertex1.id && face.vertexIds[i] != newVertex2.id) { - newFaceIndices.Add(face.vertexIds[i]); - } - if (i == points.point1Index) { - newFaceIndices.Add(newVertex1.id); - newFaceIndices.Add(newVertex2.id); - for (int k = points.point2Index + 1; k < face.vertexIds.Count; k++) { - if (face.vertexIds[k] != newVertex1.id && face.vertexIds[k] != newVertex2.id) { - newFaceIndices.Add(face.vertexIds[k]); - } - } - Face newFace = subdivideOperation.AddFace(new List(newFaceIndices), face.properties); - // done with first face, start drawing our second face - newFaceIndices.Clear(); - newFaceIndices.Add(newVertex1.id); - } else if (i == points.point2Index) { - newFaceIndices.Add(newVertex2.id); - Face newFace = subdivideOperation.AddFace(new List(newFaceIndices), face.properties); - break; - } + private bool IsSetRightHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && !IsSubdividing() + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.RIGHT; } - // Split the segments of other faces where needed. - foreach (Face oldFace in new List(modifiedMesh.GetFaces())) { - if (oldFace.id == subdivision.face.id) { - continue; - } - Face fixedFace = oldFace; - // Try to replace the first segment. - if (!vertex1AlreadyExisted) { - List indices = MaybeInsertVert(fixedFace, startVertIdSeg1, endVertIdSeg1, newVertex1.id); - if (indices.Count > 0) { - subdivideOperation.ModifyFace(fixedFace.id, indices, fixedFace.properties); - } - } - // Try to replace the second segment. - if (!vertex2AlreadyExisted) { - List indices = MaybeInsertVert(fixedFace, startVertIdSeg2, endVertIdSeg2, newVertex2.id); - if (indices.Count > 0) { - subdivideOperation.ModifyFace(fixedFace.id, indices, fixedFace.properties); - } - } + private static bool IsUnsetAllHoverTooltipsEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.NONE; } - subdivideOperation.Commit(); + /// + /// Subdivides the mesh according to the provided list of subdivisions. + /// + private void SubdivideFaces(List subdivisions) + { + if (subdivisions.Count == 0) + { + return; + } - EdgeKey edgeKey = new EdgeKey(modifiedMesh.id, newVertex1.id, newVertex2.id); - PeltzerMain.Instance.highlightUtils.TurnOn(edgeKey, EDGE_HIGHLIGHT_ANIMATION_IN_TIME); + // Keys are mesh IDs, values are the actual meshes. + Dictionary modifiedMeshes = new Dictionary(); + + // Keys are mesh IDs, values are collections of IDs of created verts. + // This is needed to reuse verts created over multiple GeometryOperations without + // having to iterate over every vertex in the mesh. + Dictionary> reusableVertsDict = new Dictionary>(); + + foreach (Subdivision subdivision in subdivisions) + { + + // If we haven't cloned the mesh associated with this subdivision, clone it, + // otherwise reuse the previously cloned mesh. + MMesh modifiedMesh; + if (!modifiedMeshes.TryGetValue(subdivision.mesh.id, out modifiedMesh)) + { + modifiedMesh = subdivision.mesh.Clone(); + modifiedMeshes[subdivision.mesh.id] = modifiedMesh; + } + + // Get the set of reusable vertices for this mesh, or create it. + HashSet reusableVertsForCurrentMesh; + if (!reusableVertsDict.TryGetValue(subdivision.mesh.id, out reusableVertsForCurrentMesh)) + { + reusableVertsForCurrentMesh = new HashSet(); + reusableVertsDict[subdivision.mesh.id] = reusableVertsForCurrentMesh; + } + + MMesh.GeometryOperation subdivideOperation = modifiedMesh.StartOperation(); + + Vector3 start = subdivision.startPoint; + Vector3 sliceDirection = subdivision.sliceDirection; + + // Check this is a valid subdivision. + SubdividePoints points; + if (!GetSubdividePoints(subdivision.face, subdivision.mesh, start, sliceDirection.normalized, out points)) + { + audioLibrary.PlayClip(audioLibrary.errorSound); + peltzerController.TriggerHapticFeedback(); + return; + } + + Face face = subdivision.face; + subdivideOperation.DeleteFace(face.id); + selector.DeselectAll(); + + // To make this a little bit simpler, order our points to coincide with vertex order on face. + if (points.point1Index > points.point2Index) + { + int tmp = points.point1Index; + points.point1Index = points.point2Index; + points.point2Index = tmp; + + Vector3 pointTmp = points.point1; + points.point1 = points.point2; + points.point2 = pointTmp; + } + + int startVertIdSeg1 = face.vertexIds[points.point1Index]; + int endVertIdSeg1 = face.vertexIds[(points.point1Index + 1) % face.vertexIds.Count]; + int startVertIdSeg2 = face.vertexIds[points.point2Index]; + int endVertIdSeg2 = face.vertexIds[(points.point2Index + 1) % face.vertexIds.Count]; + + // Find the nearest existing vertices within VERTEX_REUSE_DISTANCE_THRESHOLD. If we find one, we + // use the existing vertex instead of creating a new one. If we don't, we create new vertices. + Vertex newVertex1 = null; + Vertex newVertex2 = null; + float nearestSquaredDistanceToNewVertex1 = VERTEX_REUSE_SQUARED_DISTANCE_THRESHOLD; + float nearestSquaredDistanceToNewVertex2 = VERTEX_REUSE_SQUARED_DISTANCE_THRESHOLD; + + // The vertices we need to check (to reuse them) is the union of previously created verts + // (in previous subdivisions) and the vertices in the face of the current subdivision. + HashSet vertsToCheck = new HashSet(); + vertsToCheck.UnionWith(face.vertexIds); + vertsToCheck.UnionWith(reusableVertsForCurrentMesh); + + foreach (int vertexId in vertsToCheck) + { + Vertex vertex = modifiedMesh.GetVertex(vertexId); + Vector3 vertexPos = modifiedMesh.VertexPositionInModelCoords(vertexId); + float squaredDistanceistanceToNewVertex1 = Vector3.SqrMagnitude(vertexPos - points.point1); + if (squaredDistanceistanceToNewVertex1 < nearestSquaredDistanceToNewVertex1) + { + newVertex1 = vertex; + nearestSquaredDistanceToNewVertex1 = squaredDistanceistanceToNewVertex1; + } + float squaredDistanceToNewVertex2 = Vector3.SqrMagnitude(vertexPos - points.point2); + if (squaredDistanceToNewVertex2 < nearestSquaredDistanceToNewVertex2) + { + newVertex2 = vertex; + nearestSquaredDistanceToNewVertex2 = squaredDistanceToNewVertex2; + } + } + + bool vertex1AlreadyExisted = true; + bool vertex2AlreadyExisted = true; + if (newVertex1 == null) + { + newVertex1 = subdivideOperation.AddVertexModelSpace(points.point1); + vertex1AlreadyExisted = false; + reusableVertsForCurrentMesh.Add(newVertex1.id); + } + if (newVertex2 == null) + { + newVertex2 = subdivideOperation.AddVertexModelSpace(points.point2); + vertex2AlreadyExisted = false; + reusableVertsForCurrentMesh.Add(newVertex2.id); + } + + // If the two subdivide points are an edge on the subdivide face, don't subdivide. + if (vertex1AlreadyExisted && vertex2AlreadyExisted && VerticesAreEdgeOnFace(newVertex1, newVertex2, face)) + { + audioLibrary.PlayClip(audioLibrary.errorSound); + peltzerController.TriggerHapticFeedback(); + return; + } + + // Create our new faces. Basically, wind around the face until we hit our "cut" points. When we hit + // one, we make a face. + List newFaceIndices = new List(); + for (int i = 0; i < face.vertexIds.Count; i++) + { + if (face.vertexIds[i] != newVertex1.id && face.vertexIds[i] != newVertex2.id) + { + newFaceIndices.Add(face.vertexIds[i]); + } + if (i == points.point1Index) + { + newFaceIndices.Add(newVertex1.id); + newFaceIndices.Add(newVertex2.id); + for (int k = points.point2Index + 1; k < face.vertexIds.Count; k++) + { + if (face.vertexIds[k] != newVertex1.id && face.vertexIds[k] != newVertex2.id) + { + newFaceIndices.Add(face.vertexIds[k]); + } + } + Face newFace = subdivideOperation.AddFace(new List(newFaceIndices), face.properties); + // done with first face, start drawing our second face + newFaceIndices.Clear(); + newFaceIndices.Add(newVertex1.id); + } + else if (i == points.point2Index) + { + newFaceIndices.Add(newVertex2.id); + Face newFace = subdivideOperation.AddFace(new List(newFaceIndices), face.properties); + break; + } + } + + // Split the segments of other faces where needed. + foreach (Face oldFace in new List(modifiedMesh.GetFaces())) + { + if (oldFace.id == subdivision.face.id) + { + continue; + } + Face fixedFace = oldFace; + // Try to replace the first segment. + if (!vertex1AlreadyExisted) + { + List indices = MaybeInsertVert(fixedFace, startVertIdSeg1, endVertIdSeg1, newVertex1.id); + if (indices.Count > 0) + { + subdivideOperation.ModifyFace(fixedFace.id, indices, fixedFace.properties); + } + } + // Try to replace the second segment. + if (!vertex2AlreadyExisted) + { + List indices = MaybeInsertVert(fixedFace, startVertIdSeg2, endVertIdSeg2, newVertex2.id); + if (indices.Count > 0) + { + subdivideOperation.ModifyFace(fixedFace.id, indices, fixedFace.properties); + } + } + } + + subdivideOperation.Commit(); + + EdgeKey edgeKey = new EdgeKey(modifiedMesh.id, newVertex1.id, newVertex2.id); + PeltzerMain.Instance.highlightUtils.TurnOn(edgeKey, EDGE_HIGHLIGHT_ANIMATION_IN_TIME); + + highlightsToTurnOff.Enqueue(new KeyValuePair(Time.time + EDGE_HIGHLIGHT_DURATION, edgeKey)); + PeltzerMain.Instance.subdividesCompleted++; + } - highlightsToTurnOff.Enqueue(new KeyValuePair(Time.time + EDGE_HIGHLIGHT_DURATION, edgeKey)); - PeltzerMain.Instance.subdividesCompleted++; - } + // Apply commands to create our new meshes, or error if we find an invalid operation. + bool errorOccurred = false; + foreach (var modifiedMesh in modifiedMeshes.Values) + { + if (model.CanAddMesh(modifiedMesh)) + { + model.ApplyCommand(new ReplaceMeshCommand(modifiedMesh.id, modifiedMesh)); + } + else + { + errorOccurred = true; + break; + } + } - // Apply commands to create our new meshes, or error if we find an invalid operation. - bool errorOccurred = false; - foreach (var modifiedMesh in modifiedMeshes.Values) { - if (model.CanAddMesh(modifiedMesh)) { - model.ApplyCommand(new ReplaceMeshCommand(modifiedMesh.id, modifiedMesh)); - } else { - errorOccurred = true; - break; + if (!errorOccurred) + { + audioLibrary.PlayClip(audioLibrary.subdivideSound); + peltzerController.TriggerHapticFeedback(); + } + else + { + audioLibrary.PlayClip(audioLibrary.errorSound); + peltzerController.TriggerHapticFeedback(); + } } - } - - if (!errorOccurred) { - audioLibrary.PlayClip(audioLibrary.subdivideSound); - peltzerController.TriggerHapticFeedback(); - } else { - audioLibrary.PlayClip(audioLibrary.errorSound); - peltzerController.TriggerHapticFeedback(); - } - } - /// - /// Check if two vertices are an edge of a face. - /// - /// True if the two vertices are an edge of the face. - private static bool VerticesAreEdgeOnFace(Vertex v1, Vertex v2, Face face) { - if (face.vertexIds.Count < 3) { - return false; - } - int prevId = face.vertexIds[face.vertexIds.Count - 1]; - foreach (int vertId in face.vertexIds) { - if ((prevId == v1.id && vertId == v2.id) || (prevId == v2.id && vertId == v1.id)) { - return true; + /// + /// Check if two vertices are an edge of a face. + /// + /// True if the two vertices are an edge of the face. + private static bool VerticesAreEdgeOnFace(Vertex v1, Vertex v2, Face face) + { + if (face.vertexIds.Count < 3) + { + return false; + } + int prevId = face.vertexIds[face.vertexIds.Count - 1]; + foreach (int vertId in face.vertexIds) + { + if ((prevId == v1.id && vertId == v2.id) || (prevId == v2.id && vertId == v1.id)) + { + return true; + } + prevId = vertId; + } + return false; } - prevId = vertId; - } - return false; - } - /// - /// Given start and end vertex ids of a segment, insert a vertex in that segment, if found. - /// - /// The face to update. - /// Vertex id of the segment start. - /// Vertex id of the segment end. - /// Vertex id to insert. - /// - /// The updated list of vertices for the new face if the segment was found, or empty list if no - /// segment was found. - /// - // Public for testing. - public static List MaybeInsertVert(Face face, int startId, int endId, int newVertId) { - for (int i = 0; i < face.vertexIds.Count; i++) { - // Look for the segment in either order. - if ((face.vertexIds[i] == startId && face.vertexIds[(i + 1) % face.vertexIds.Count] == endId) - || (face.vertexIds[i] == endId && face.vertexIds[(i + 1) % face.vertexIds.Count] == startId)) { - List newVertIds = new List(); - // Add the verts that came before the new point. - for (int j = 0; j <= i; j++) { - newVertIds.Add(face.vertexIds[j]); - } - newVertIds.Add(newVertId); - - // Copy the verts after the segment. - for (int j = i + 1; j < face.vertexIds.Count; j++) { - newVertIds.Add(face.vertexIds[j]); - } - // Return the vertices of the new face. - return newVertIds; + /// + /// Given start and end vertex ids of a segment, insert a vertex in that segment, if found. + /// + /// The face to update. + /// Vertex id of the segment start. + /// Vertex id of the segment end. + /// Vertex id to insert. + /// + /// The updated list of vertices for the new face if the segment was found, or empty list if no + /// segment was found. + /// + // Public for testing. + public static List MaybeInsertVert(Face face, int startId, int endId, int newVertId) + { + for (int i = 0; i < face.vertexIds.Count; i++) + { + // Look for the segment in either order. + if ((face.vertexIds[i] == startId && face.vertexIds[(i + 1) % face.vertexIds.Count] == endId) + || (face.vertexIds[i] == endId && face.vertexIds[(i + 1) % face.vertexIds.Count] == startId)) + { + List newVertIds = new List(); + // Add the verts that came before the new point. + for (int j = 0; j <= i; j++) + { + newVertIds.Add(face.vertexIds[j]); + } + newVertIds.Add(newVertId); + + // Copy the verts after the segment. + for (int j = i + 1; j < face.vertexIds.Count; j++) + { + newVertIds.Add(face.vertexIds[j]); + } + // Return the vertices of the new face. + return newVertIds; + } + } + // Segment wasn't found. No need to update the face. + return new List(); } - } - // Segment wasn't found. No need to update the face. - return new List(); - } - /// - /// An event handler that listens for controller input and delegates accordingly. - /// - private void ControllerEventHandler(object sender, ControllerEventArgs args) { - if (peltzerController.mode != ControllerMode.subdivideFace) - return; - - if (IsStartPressAndHoldEvent(args)) { - triggerHoldStartTime = Time.time; - isTriggerBeingHeld = true; - } else if (IsFinishSubdivideEvent(args)) { - if (isSnapping) { - // We snapped while modifying, so we have learned a bit more about snapping. - completedSnaps++; + /// + /// An event handler that listens for controller input and delegates accordingly. + /// + private void ControllerEventHandler(object sender, ControllerEventArgs args) + { + if (peltzerController.mode != ControllerMode.subdivideFace) + return; + + if (IsStartPressAndHoldEvent(args)) + { + triggerHoldStartTime = Time.time; + isTriggerBeingHeld = true; + } + else if (IsFinishSubdivideEvent(args)) + { + if (isSnapping) + { + // We snapped while modifying, so we have learned a bit more about snapping. + completedSnaps++; + } + FinishSubdivide(); + triggerHoldStartTime = 0; + isTriggerBeingHeld = false; + } + else if (IsStartSnapEvent(args) && !peltzerController.isBlockMode) + { + PeltzerMain.Instance.snappedInSubdivider = true; + isSnapping = true; + if (completedSnaps < SNAP_KNOW_HOW_COUNT) + { + PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); + } + PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.alignSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + } + else if (IsEndSnapEvent(args) && !peltzerController.isBlockMode) + { + isSnapping = false; + PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); + } + else if (IsSetUpHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadUpAllowed) + { + SetHoverTooltip(peltzerController.controllerGeometry.modifyTooltipUp, TouchpadHoverState.UP); + } + else if (IsSetLeftHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed) + { + SetHoverTooltip(peltzerController.controllerGeometry.modifyTooltipLeft, TouchpadHoverState.LEFT); + } + else if (IsSetRightHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadRightAllowed) + { + SetHoverTooltip(peltzerController.controllerGeometry.modifyTooltipRight, TouchpadHoverState.RIGHT); + } + else if (IsUnsetAllHoverTooltipsEvent(args)) + { + UnsetAllHoverTooltips(); + } } - FinishSubdivide(); - triggerHoldStartTime = 0; - isTriggerBeingHeld = false; - } else if (IsStartSnapEvent(args) && !peltzerController.isBlockMode) { - PeltzerMain.Instance.snappedInSubdivider = true; - isSnapping = true; - if (completedSnaps < SNAP_KNOW_HOW_COUNT) { - PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); + + /// + /// Gets intersection points with face boundary. + /// + /// Face to get points for. + /// Mesh to get points for. + /// 2 points of intersection with face boundary. + /// True if the intersection points were found. + private bool GetSubdividePoints( + Face face, MMesh mesh, Vector3 pointOfIntersection, Vector3 sliceDirection, out SubdividePoints subdividePoints) + { + + List faceVertices = new List(face.vertexIds.Count); + foreach (int id in face.vertexIds) + { + faceVertices.Add(new Vertex(id, mesh.VertexPositionInModelCoords(id))); + } + + Vector3 faceNormal = mesh.rotation * face.normal; + subdividePoints = new SubdividePoints(); + + Vector3 normalToSliceLine = Vector3.Cross(sliceDirection, faceNormal).normalized; + float slicePointDot = Vector3.Dot(pointOfIntersection, normalToSliceLine); + + bool faceEdgeIntersection1Found = false; + bool faceEdgeIntersection2Found = false; + Vertex prev = faceVertices[faceVertices.Count - 1]; + for (int i = 0; i < faceVertices.Count; i++) + { + Vertex curr = faceVertices[i]; + Vector3 edge = curr.loc - prev.loc; + float d1 = Vector3.Dot(prev.loc, normalToSliceLine); + float d2 = Vector3.Dot(curr.loc, normalToSliceLine); + + // The two following checks in the if and else are determining if curr and prev are above and below our + // slice line, which means our slice point is somewhere along the edge. + if (d1 <= slicePointDot && slicePointDot < d2) + { + // We can use the ratio of dot-products to determine how far along the edge the cut point is. + float ratio = (slicePointDot - d1) / (d2 - d1); + if (faceEdgeIntersection1Found) + { + faceEdgeIntersection2Found = true; + subdividePoints.point2 = prev.loc + edge * ratio; + subdividePoints.point2Index = i == 0 ? faceVertices.Count - 1 : i - 1; + subdividePoints.edge2 = new EdgeKey(mesh.id, prev.id, curr.id); + break; + } + else + { + faceEdgeIntersection1Found = true; + subdividePoints.point1 = prev.loc + edge * ratio; + subdividePoints.point1Index = i == 0 ? faceVertices.Count - 1 : i - 1; + subdividePoints.edge1 = new EdgeKey(mesh.id, prev.id, curr.id); + } + } + else if (d2 <= slicePointDot && slicePointDot < d1) + { + float ratio = (slicePointDot - d2) / (d1 - d2); + if (faceEdgeIntersection1Found) + { + faceEdgeIntersection2Found = true; + subdividePoints.point2 = curr.loc - edge * ratio; + subdividePoints.point2Index = i == 0 ? faceVertices.Count - 1 : i - 1; + subdividePoints.edge2 = new EdgeKey(mesh.id, prev.id, curr.id); + break; + } + else + { + faceEdgeIntersection1Found = true; + subdividePoints.point1 = curr.loc - edge * ratio; + subdividePoints.point1Index = i == 0 ? faceVertices.Count - 1 : i - 1; + subdividePoints.edge1 = new EdgeKey(mesh.id, prev.id, curr.id); + } + } + prev = curr; + } + return faceEdgeIntersection2Found; } - PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.alignSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - } else if (IsEndSnapEvent(args) && !peltzerController.isBlockMode) { - isSnapping = false; - PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); - } else if (IsSetUpHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadUpAllowed) { - SetHoverTooltip(peltzerController.controllerGeometry.modifyTooltipUp, TouchpadHoverState.UP); - } else if (IsSetLeftHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed) { - SetHoverTooltip(peltzerController.controllerGeometry.modifyTooltipLeft, TouchpadHoverState.LEFT); - } else if (IsSetRightHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadRightAllowed) { - SetHoverTooltip(peltzerController.controllerGeometry.modifyTooltipRight, TouchpadHoverState.RIGHT); - } else if (IsUnsetAllHoverTooltipsEvent(args)) { - UnsetAllHoverTooltips(); - } - } - /// - /// Gets intersection points with face boundary. - /// - /// Face to get points for. - /// Mesh to get points for. - /// 2 points of intersection with face boundary. - /// True if the intersection points were found. - private bool GetSubdividePoints( - Face face, MMesh mesh, Vector3 pointOfIntersection, Vector3 sliceDirection, out SubdividePoints subdividePoints) { - - List faceVertices = new List(face.vertexIds.Count); - foreach (int id in face.vertexIds) { - faceVertices.Add(new Vertex(id, mesh.VertexPositionInModelCoords(id))); - } - - Vector3 faceNormal = mesh.rotation * face.normal; - subdividePoints = new SubdividePoints(); - - Vector3 normalToSliceLine = Vector3.Cross(sliceDirection, faceNormal).normalized; - float slicePointDot = Vector3.Dot(pointOfIntersection, normalToSliceLine); - - bool faceEdgeIntersection1Found = false; - bool faceEdgeIntersection2Found = false; - Vertex prev = faceVertices[faceVertices.Count - 1]; - for (int i = 0; i < faceVertices.Count; i++) { - Vertex curr = faceVertices[i]; - Vector3 edge = curr.loc - prev.loc; - float d1 = Vector3.Dot(prev.loc, normalToSliceLine); - float d2 = Vector3.Dot(curr.loc, normalToSliceLine); - - // The two following checks in the if and else are determining if curr and prev are above and below our - // slice line, which means our slice point is somewhere along the edge. - if (d1 <= slicePointDot && slicePointDot < d2) { - // We can use the ratio of dot-products to determine how far along the edge the cut point is. - float ratio = (slicePointDot - d1) / (d2 - d1); - if (faceEdgeIntersection1Found) { - faceEdgeIntersection2Found = true; - subdividePoints.point2 = prev.loc + edge * ratio; - subdividePoints.point2Index = i == 0 ? faceVertices.Count - 1 : i - 1; - subdividePoints.edge2 = new EdgeKey(mesh.id, prev.id, curr.id); - break; - } else { - faceEdgeIntersection1Found = true; - subdividePoints.point1 = prev.loc + edge * ratio; - subdividePoints.point1Index = i == 0 ? faceVertices.Count - 1 : i - 1; - subdividePoints.edge1 = new EdgeKey(mesh.id, prev.id, curr.id); - } - } else if (d2 <= slicePointDot && slicePointDot < d1) { - float ratio = (slicePointDot - d2) / (d1 - d2); - if (faceEdgeIntersection1Found) { - faceEdgeIntersection2Found = true; - subdividePoints.point2 = curr.loc - edge * ratio; - subdividePoints.point2Index = i == 0 ? faceVertices.Count - 1 : i - 1; - subdividePoints.edge2 = new EdgeKey(mesh.id, prev.id, curr.id); - break; - } else { - faceEdgeIntersection1Found = true; - subdividePoints.point1 = curr.loc - edge * ratio; - subdividePoints.point1Index = i == 0 ? faceVertices.Count - 1 : i - 1; - subdividePoints.edge1 = new EdgeKey(mesh.id, prev.id, curr.id); - } + /// + /// Takes the points that the subdivide line interesects with the edges and snap these individually + /// to the nearest point of interest. + /// + private void SnapSubdivision(Subdivision subdivision, SubdividePoints edgeIntersectionPoints) + { + Face face = subdivision.face; + List currentFaceVertices = new List(face.vertexIds.Count); + for (int i = 0; i < face.vertexIds.Count; i++) + { + currentFaceVertices.Add(subdivision.mesh.VertexPositionInModelCoords(face.vertexIds[i])); + } + List interestingFacePoints = FindInterestingFacePoints(currentFaceVertices); + + Vector3 firstPoint = Math3d.NearestPoint(edgeIntersectionPoints.point1, interestingFacePoints); + Vector3 secondPoint = Math3d.NearestPoint(edgeIntersectionPoints.point2, interestingFacePoints); + + // Redefine the snapped subdivide line. + subdivision.startPoint = firstPoint; + subdivision.sliceDirection = secondPoint - firstPoint; } - prev = curr; - } - return faceEdgeIntersection2Found; - } - /// - /// Takes the points that the subdivide line interesects with the edges and snap these individually - /// to the nearest point of interest. - /// - private void SnapSubdivision(Subdivision subdivision, SubdividePoints edgeIntersectionPoints) { - Face face = subdivision.face; - List currentFaceVertices = new List(face.vertexIds.Count); - for (int i = 0; i < face.vertexIds.Count; i++) { - currentFaceVertices.Add(subdivision.mesh.VertexPositionInModelCoords(face.vertexIds[i])); - } - List interestingFacePoints = FindInterestingFacePoints(currentFaceVertices); - - Vector3 firstPoint = Math3d.NearestPoint(edgeIntersectionPoints.point1, interestingFacePoints); - Vector3 secondPoint = Math3d.NearestPoint(edgeIntersectionPoints.point2, interestingFacePoints); - - // Redefine the snapped subdivide line. - subdivision.startPoint = firstPoint; - subdivision.sliceDirection = secondPoint - firstPoint; - } + /// + /// Given a plane and a line segment defined by 2 vectors, finds the intersection point. + /// + bool GetSegmentPlaneIntersection(Plane plane, Vector3 p1, Vector3 p2, out Vector3 intersectionPoint) + { + float d1 = plane.GetDistanceToPoint(p1); + float d2 = plane.GetDistanceToPoint(p2); + + // Both points are on the same side of the plane, so no intersection. + if (d1 * d2 > 0) + { + intersectionPoint = Vector3.zero; + return false; + } - /// - /// Given a plane and a line segment defined by 2 vectors, finds the intersection point. - /// - bool GetSegmentPlaneIntersection(Plane plane, Vector3 p1, Vector3 p2, out Vector3 intersectionPoint) { - float d1 = plane.GetDistanceToPoint(p1); - float d2 = plane.GetDistanceToPoint(p2); - - // Both points are on the same side of the plane, so no intersection. - if (d1 * d2 > 0) { - intersectionPoint = Vector3.zero; - return false; - } - - // t is the normalized distance from p1 to p2 where the intersection happens. - float t = d1 / (d1 - d2); - intersectionPoint = p1 + t * (p2 - p1); - return true; - } + // t is the normalized distance from p1 to p2 where the intersection happens. + float t = d1 / (d1 - d2); + intersectionPoint = p1 + t * (p2 - p1); + return true; + } - /// - /// Given a plane and a face defined by an order list of vertices, finds out the points - /// where the plane intersects the face edges. - /// - void FindFacePlaneIntersection(Plane plane, - List vertices, out List outSegTips) { - Vector3 intersectionPoint; - outSegTips = new List(); - - Vector3 prev = vertices[vertices.Count - 1]; - for (int i = 0; i < vertices.Count; i++) { - Vector3 curr = vertices[i]; - if (GetSegmentPlaneIntersection(plane, curr, prev, out intersectionPoint)) { - outSegTips.Add(intersectionPoint); + /// + /// Given a plane and a face defined by an order list of vertices, finds out the points + /// where the plane intersects the face edges. + /// + void FindFacePlaneIntersection(Plane plane, + List vertices, out List outSegTips) + { + Vector3 intersectionPoint; + outSegTips = new List(); + + Vector3 prev = vertices[vertices.Count - 1]; + for (int i = 0; i < vertices.Count; i++) + { + Vector3 curr = vertices[i]; + if (GetSegmentPlaneIntersection(plane, curr, prev, out intersectionPoint)) + { + outSegTips.Add(intersectionPoint); + } + prev = curr; + } } - prev = curr; - } } - } } diff --git a/Assets/Scripts/tools/VolumeInserter.cs b/Assets/Scripts/tools/VolumeInserter.cs index b0a498a2..f0ffd092 100644 --- a/Assets/Scripts/tools/VolumeInserter.cs +++ b/Assets/Scripts/tools/VolumeInserter.cs @@ -24,785 +24,935 @@ using com.google.apps.peltzer.client.model.util; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.tools { - /// - /// A tool responsible for adding meshes to the scene. - /// An insert is composed of previewing, (potentially) scaling and/or moving, and triggering. - /// - public class VolumeInserter : MonoBehaviour { - // The default scale delta. - private static readonly int DEFAULT_SCALE_DELTA = 2; - // The default radius of primitives added to the scene. Twice the grid size. - private static readonly float DEFAULT_PRIMITIVE_SCALE = (GridUtils.GRID_SIZE / 2.0f); - // The position offset between the controller and the position of the preview; - public static readonly Vector3 PREVIEW_OFFSET = new Vector3(0f, 0f, 0.25f); - // How long to wait between scaling events. - private static readonly float MIN_TIME_BETWEEN_SCALING = 0.2f; - // The lowest scaleDelta can go, for primitives. This implies a cube of size 2-grid-units. - private static readonly int MIN_SCALE_DELTA = -4; - // The highest scaleDelta can go, for primitives. This implies that four (and a bit) such cubes could fit - // side-by-side within bounds. - private static readonly int MAX_SCALE_DELTA = 220; - // How long after an insert should it take for the preview mesh to once again show at full opacity. - private static readonly float PREVIEW_RESHOW_DURATION = 1.0f; +namespace com.google.apps.peltzer.client.tools +{ /// - /// The number of seconds the user scales an object continuously before we start increasing the rate of the - /// scaling process. + /// A tool responsible for adding meshes to the scene. + /// An insert is composed of previewing, (potentially) scaling and/or moving, and triggering. /// - private const float FAST_SCALE_THRESHOLD = 1f; - /// - /// If user is scaling for a what we consider a long time, we will increase the scaling rate by this amount. - /// - private const int LONG_TERM_SCALE_FACTOR = 1; - - // Other tools. - private ControllerMain controllerMain; - private PeltzerController peltzerController; - private Model model; - private SpatialIndex spatialIndex; - private AudioLibrary audioLibrary; - private WorldSpace worldSpace; - private MeshRepresentationCache meshRepresentationCache; - private float lastInsertTime = 0.0f; - - // We use the selector in copy mode to allow the user to select a set of meshes to use as a primitive. - private Selector selector; - - // Insertion. - // The mesh(es) currently being held which will be inserted on a trigger click. - private HeldMeshes heldMeshes; - - // Scaling. - // The delta from the default scale of the current preview. - public int scaleDelta { get; private set; } - // Whether the user is currently holding the scale button. - private ScaleType scaleType = ScaleType.NONE; - // The number of scale events received without the touchpad being released. - private int continuousScaleEvents = 0; - /// - // The benchmark we set to determine when the user has been scaling for a long time. - /// - private float longTermScaleStartTime = float.MaxValue; - // Keep track of changes to shape-menu-show-state for nice lerping. - private bool wasShowingShapesMenuLastFrame; - // Keep track of whether startSnap was called while the shapes menu was up; if so, start snapping - // as soon as the new mesh is begun. - private bool snapStartedWhileShapesMenuUp; - /// - /// Used to determine if we should show the snap tooltip or not. Don't show the tooltip if the user already - /// showed enough knowledge of how to snap. - /// - private int completedSnaps = 0; - private const int SNAP_KNOW_HOW_COUNT = 3; - - /// - /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. - /// As such, this setup method must be called before the tool is used for it to have a valid state. - /// - public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, - AudioLibrary audioLibrary, WorldSpace worldSpace, SpatialIndex spatialIndex, - MeshRepresentationCache meshRepresentationCache, Selector selector) { - this.model = model; - this.controllerMain = controllerMain; - this.peltzerController = peltzerController; - this.audioLibrary = audioLibrary; - this.worldSpace = worldSpace; - this.spatialIndex = spatialIndex; - this.meshRepresentationCache = meshRepresentationCache; - this.selector = selector; - this.lastInsertTime = -10.0f; - controllerMain.ControllerActionHandler += ControllerEventHandler; - peltzerController.MaterialChangedHandler += MaterialChangeHandler; - peltzerController.shapesMenu.ShapeMenuItemChangedHandler += ShapeChangedHandler; - peltzerController.ModeChangedHandler += ModeChangeEventHandler; - peltzerController.BlockModeChangedHandler += BlockModeChangedHandler; - - scaleDelta = DEFAULT_SCALE_DELTA; - - // Attach the preview mesh to the preview GameObject. - CreateNewVolumeMesh(); - } - - /// - /// Updates the location of the preview. - /// - private void Update() { - if (!PeltzerController.AcquireIfNecessary(ref peltzerController)) { - return; - } - - // If we are in "copy mode", use the selector to allow the user to hover/select meshes to copy. - // (we have to check if peltzerController.shapesMenu is null because VolumeInserter might be - // created before PeltzerController setup is done). - if (peltzerController.shapesMenu != null && - peltzerController.shapesMenu.CurrentItemId == ShapesMenu.COPY_MODE_ID) { - selector.SelectMeshAtPosition(peltzerController.LastPositionModel, Selector.MESHES_ONLY); - } - - bool activeMode = (peltzerController.mode == ControllerMode.insertVolume - || peltzerController.mode == ControllerMode.subtract) - && !PeltzerMain.Instance.peltzerController.isPointingAtMenu - && PeltzerMain.Instance.introChoreographer.introIsComplete; - - if (heldMeshes != null) { - if (activeMode) { - heldMeshes.UpdatePositions(); - heldMeshes.Unhide(); - } else { - heldMeshes.Hide(); - } - } - - // If we changed from showing the shapes menu to not, scale up from the menu-size to the desired size. - bool showingShapesMenuThisFrame = peltzerController.shapesMenu.showingShapeMenu; - if (wasShowingShapesMenuLastFrame && !showingShapesMenuThisFrame) { - CreateNewVolumeMesh(/* oldScaleToAnimateFrom */ 0); - } - - wasShowingShapesMenuLastFrame = showingShapesMenuThisFrame; - - //process held mesh fadeout - float timeSinceLastInsert = Time.time - this.lastInsertTime; - if (timeSinceLastInsert < PREVIEW_RESHOW_DURATION + 1f) { - float pctAnim = Mathf.Min(1f, timeSinceLastInsert / PREVIEW_RESHOW_DURATION); - float curvedAlpha = 0.1f + 0.9f * (pctAnim * pctAnim * pctAnim); - for (int i = 0; i < heldMeshes.heldMeshes.Count; i++) { - MeshWithMaterialRenderer renderer = - heldMeshes.heldMeshes[i].Preview.GetComponent(); - renderer.fade = 0.3f * curvedAlpha; - } - } - } - - /// - /// Update the positions of the held meshes immediately. - /// - public void UpdateHeldMeshesPositions() { - heldMeshes.UpdatePositions(); - } - - /// - /// Whether this matches the pattern of a 'start inserting' event. Or, in copy mode, - /// it's the "copy" event. - /// - /// The controller event arguments. - /// True if this is the start of an insert event, false otherwise. - private static bool IsStartInsertVolumeOrCopyEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN; - } + public class VolumeInserter : MonoBehaviour + { + // The default scale delta. + private static readonly int DEFAULT_SCALE_DELTA = 2; + // The default radius of primitives added to the scene. Twice the grid size. + private static readonly float DEFAULT_PRIMITIVE_SCALE = (GridUtils.GRID_SIZE / 2.0f); + // The position offset between the controller and the position of the preview; + public static readonly Vector3 PREVIEW_OFFSET = new Vector3(0f, 0f, 0.25f); + // How long to wait between scaling events. + private static readonly float MIN_TIME_BETWEEN_SCALING = 0.2f; + // The lowest scaleDelta can go, for primitives. This implies a cube of size 2-grid-units. + private static readonly int MIN_SCALE_DELTA = -4; + // The highest scaleDelta can go, for primitives. This implies that four (and a bit) such cubes could fit + // side-by-side within bounds. + private static readonly int MAX_SCALE_DELTA = 220; + // How long after an insert should it take for the preview mesh to once again show at full opacity. + private static readonly float PREVIEW_RESHOW_DURATION = 1.0f; + /// + /// The number of seconds the user scales an object continuously before we start increasing the rate of the + /// scaling process. + /// + private const float FAST_SCALE_THRESHOLD = 1f; + /// + /// If user is scaling for a what we consider a long time, we will increase the scaling rate by this amount. + /// + private const int LONG_TERM_SCALE_FACTOR = 1; + + // Other tools. + private ControllerMain controllerMain; + private PeltzerController peltzerController; + private Model model; + private SpatialIndex spatialIndex; + private AudioLibrary audioLibrary; + private WorldSpace worldSpace; + private MeshRepresentationCache meshRepresentationCache; + private float lastInsertTime = 0.0f; + + // We use the selector in copy mode to allow the user to select a set of meshes to use as a primitive. + private Selector selector; + + // Insertion. + // The mesh(es) currently being held which will be inserted on a trigger click. + private HeldMeshes heldMeshes; + + // Scaling. + // The delta from the default scale of the current preview. + public int scaleDelta { get; private set; } + // Whether the user is currently holding the scale button. + private ScaleType scaleType = ScaleType.NONE; + // The number of scale events received without the touchpad being released. + private int continuousScaleEvents = 0; + /// + // The benchmark we set to determine when the user has been scaling for a long time. + /// + private float longTermScaleStartTime = float.MaxValue; + // Keep track of changes to shape-menu-show-state for nice lerping. + private bool wasShowingShapesMenuLastFrame; + // Keep track of whether startSnap was called while the shapes menu was up; if so, start snapping + // as soon as the new mesh is begun. + private bool snapStartedWhileShapesMenuUp; + /// + /// Used to determine if we should show the snap tooltip or not. Don't show the tooltip if the user already + /// showed enough knowledge of how to snap. + /// + private int completedSnaps = 0; + private const int SNAP_KNOW_HOW_COUNT = 3; + + /// + /// Every tool is implemented as MonoBehaviour, which means it may do no work in its constructor. + /// As such, this setup method must be called before the tool is used for it to have a valid state. + /// + public void Setup(Model model, ControllerMain controllerMain, PeltzerController peltzerController, + AudioLibrary audioLibrary, WorldSpace worldSpace, SpatialIndex spatialIndex, + MeshRepresentationCache meshRepresentationCache, Selector selector) + { + this.model = model; + this.controllerMain = controllerMain; + this.peltzerController = peltzerController; + this.audioLibrary = audioLibrary; + this.worldSpace = worldSpace; + this.spatialIndex = spatialIndex; + this.meshRepresentationCache = meshRepresentationCache; + this.selector = selector; + this.lastInsertTime = -10.0f; + controllerMain.ControllerActionHandler += ControllerEventHandler; + peltzerController.MaterialChangedHandler += MaterialChangeHandler; + peltzerController.shapesMenu.ShapeMenuItemChangedHandler += ShapeChangedHandler; + peltzerController.ModeChangedHandler += ModeChangeEventHandler; + peltzerController.BlockModeChangedHandler += BlockModeChangedHandler; + + scaleDelta = DEFAULT_SCALE_DELTA; + + // Attach the preview mesh to the preview GameObject. + CreateNewVolumeMesh(); + } - /// - /// Whether this matches the pattern of an 'end inserting' event. - /// - /// The controller event arguments. - /// True if this is the end of an insert event, false otherwise. - private static bool IsEndInsertVolumeEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.UP; - } + /// + /// Updates the location of the preview. + /// + private void Update() + { + if (!PeltzerController.AcquireIfNecessary(ref peltzerController)) + { + return; + } + + // If we are in "copy mode", use the selector to allow the user to hover/select meshes to copy. + // (we have to check if peltzerController.shapesMenu is null because VolumeInserter might be + // created before PeltzerController setup is done). + if (peltzerController.shapesMenu != null && + peltzerController.shapesMenu.CurrentItemId == ShapesMenu.COPY_MODE_ID) + { + selector.SelectMeshAtPosition(peltzerController.LastPositionModel, Selector.MESHES_ONLY); + } + + bool activeMode = (peltzerController.mode == ControllerMode.insertVolume + || peltzerController.mode == ControllerMode.subtract) + && !PeltzerMain.Instance.peltzerController.isPointingAtMenu + && PeltzerMain.Instance.introChoreographer.introIsComplete; + + if (heldMeshes != null) + { + if (activeMode) + { + heldMeshes.UpdatePositions(); + heldMeshes.Unhide(); + } + else + { + heldMeshes.Hide(); + } + } + + // If we changed from showing the shapes menu to not, scale up from the menu-size to the desired size. + bool showingShapesMenuThisFrame = peltzerController.shapesMenu.showingShapeMenu; + if (wasShowingShapesMenuLastFrame && !showingShapesMenuThisFrame) + { + CreateNewVolumeMesh(/* oldScaleToAnimateFrom */ 0); + } + + wasShowingShapesMenuLastFrame = showingShapesMenuThisFrame; + + //process held mesh fadeout + float timeSinceLastInsert = Time.time - this.lastInsertTime; + if (timeSinceLastInsert < PREVIEW_RESHOW_DURATION + 1f) + { + float pctAnim = Mathf.Min(1f, timeSinceLastInsert / PREVIEW_RESHOW_DURATION); + float curvedAlpha = 0.1f + 0.9f * (pctAnim * pctAnim * pctAnim); + for (int i = 0; i < heldMeshes.heldMeshes.Count; i++) + { + MeshWithMaterialRenderer renderer = + heldMeshes.heldMeshes[i].Preview.GetComponent(); + renderer.fade = 0.3f * curvedAlpha; + } + } + } - /// - /// Whether this matches the pattern of a 'scale' event. - /// - /// The controller event arguments. - /// True if this is a scale event, false otherwise. - private bool IsScaleEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN - && (args.TouchpadLocation == TouchpadLocation.BOTTOM || args.TouchpadLocation == TouchpadLocation.TOP) - && args.TouchpadOverlay != TouchpadOverlay.RESET_ZOOM; - } + /// + /// Update the positions of the held meshes immediately. + /// + public void UpdateHeldMeshesPositions() + { + heldMeshes.UpdatePositions(); + } - /// - /// Whether this matches the pattern of a 'stop scaling' event. - /// - /// The controller event arguments. - /// True if this is a scale event, false otherwise. - private bool IsStopScalingEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.UP - && scaleType != ScaleType.NONE; - } + /// + /// Whether this matches the pattern of a 'start inserting' event. Or, in copy mode, + /// it's the "copy" event. + /// + /// The controller event arguments. + /// True if this is the start of an insert event, false otherwise. + private static bool IsStartInsertVolumeOrCopyEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN; + } - /// - /// Whether this matches a start snapping event. - /// - /// The controller event arguments. - /// True if the grip is down. - private static bool IsStartSnapEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN; - } + /// + /// Whether this matches the pattern of an 'end inserting' event. + /// + /// The controller event arguments. + /// True if this is the end of an insert event, false otherwise. + private static bool IsEndInsertVolumeEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.UP; + } - private static bool IsStartSnapDetectionEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.LIGHT_DOWN; - } + /// + /// Whether this matches the pattern of a 'scale' event. + /// + /// The controller event arguments. + /// True if this is a scale event, false otherwise. + private bool IsScaleEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN + && (args.TouchpadLocation == TouchpadLocation.BOTTOM || args.TouchpadLocation == TouchpadLocation.TOP) + && args.TouchpadOverlay != TouchpadOverlay.RESET_ZOOM; + } - private static bool IsStopSnapDetectionEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.LIGHT_UP; - } + /// + /// Whether this matches the pattern of a 'stop scaling' event. + /// + /// The controller event arguments. + /// True if this is a scale event, false otherwise. + private bool IsStopScalingEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.UP + && scaleType != ScaleType.NONE; + } - /// - /// Whether this matches an end snapping event. - /// - /// The controller event arguments. - /// True if the grip is up. - private static bool IsEndSnapEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PALETTE - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.UP; - } + /// + /// Whether this matches a start snapping event. + /// + /// The controller event arguments. + /// True if the grip is down. + private static bool IsStartSnapEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN; + } - public bool IsChangeShapeEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && (heldMeshes == null || (!heldMeshes.IsFilling && !heldMeshes.IsInserting)) - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.DOWN - && (args.TouchpadLocation == TouchpadLocation.LEFT || args.TouchpadLocation == TouchpadLocation.RIGHT) - && args.TouchpadOverlay != TouchpadOverlay.RESET_ZOOM; - } + private static bool IsStartSnapDetectionEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.LIGHT_DOWN; + } - /// - /// Whether this event corresponds to a 'toggle mode' pattern. - /// - private bool IsSwitchModeEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.ApplicationMenu - && args.Action == ButtonAction.DOWN - && Features.csgSubtractEnabled; - } + private static bool IsStopSnapDetectionEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.LIGHT_UP; + } - // Touchpad Hover Tests - private bool IsSetUpHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && (heldMeshes == null || (!heldMeshes.IsFilling && !heldMeshes.IsInserting)) - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.TOP; - } + /// + /// Whether this matches an end snapping event. + /// + /// The controller event arguments. + /// True if the grip is up. + private static bool IsEndSnapEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PALETTE + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.UP; + } - private bool IsSetDownHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && (heldMeshes == null || (!heldMeshes.IsFilling && !heldMeshes.IsInserting)) - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.BOTTOM; - } + public bool IsChangeShapeEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && (heldMeshes == null || (!heldMeshes.IsFilling && !heldMeshes.IsInserting)) + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.DOWN + && (args.TouchpadLocation == TouchpadLocation.LEFT || args.TouchpadLocation == TouchpadLocation.RIGHT) + && args.TouchpadOverlay != TouchpadOverlay.RESET_ZOOM; + } - private bool IsSetLeftHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && (heldMeshes == null || (!heldMeshes.IsFilling && !heldMeshes.IsInserting)) - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.LEFT; - } + /// + /// Whether this event corresponds to a 'toggle mode' pattern. + /// + private bool IsSwitchModeEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.ApplicationMenu + && args.Action == ButtonAction.DOWN + && Features.csgSubtractEnabled; + } - private bool IsSetRightHoverTooltipEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && (heldMeshes == null || (!heldMeshes.IsFilling && !heldMeshes.IsInserting)) - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.RIGHT; - } + // Touchpad Hover Tests + private bool IsSetUpHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && (heldMeshes == null || (!heldMeshes.IsFilling && !heldMeshes.IsInserting)) + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.TOP; + } - private static bool IsUnsetAllHoverTooltipsEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Touchpad - && args.Action == ButtonAction.NONE; - } + private bool IsSetDownHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && (heldMeshes == null || (!heldMeshes.IsFilling && !heldMeshes.IsInserting)) + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.BOTTOM; + } - public float GetScaleForScaleDelta(int delta) { - return (delta + 6) * DEFAULT_PRIMITIVE_SCALE; - } + private bool IsSetLeftHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && (heldMeshes == null || (!heldMeshes.IsFilling && !heldMeshes.IsInserting)) + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.LEFT; + } - /// - /// Creates a new mesh given the tool's current parameters and attaches it to the preview Game Object. - /// - /// If not null, this indicates the previous scaleDelta from - /// which to animate from. Note: this is only honored when there is a single held mesh. - private void CreateNewVolumeMesh(int? oldScaleDeltaToAnimateFrom = null) { - Vector3? oldPosToAnimateFrom = null; - Vector3? oldScaleToAnimateFrom = null; - if (oldScaleDeltaToAnimateFrom != null && heldMeshes != null && heldMeshes.heldMeshes.Count == 1) { - // Get the initial pos/scale. We will use these at the end of the method to set up the animation. - MeshWithMaterialRenderer renderer = - heldMeshes.heldMeshes[0].Preview.GetComponent(); - oldPosToAnimateFrom = renderer.GetPositionInModelSpace(); - // Adjust the scale by the current animated scale, if necessary (so that the animation - // doesn't jump if we rescale the mesh mid-animation). - Vector3 oldScaleAnimFactor = renderer.GetCurrentAnimatedScale(); - oldScaleToAnimateFrom = oldScaleAnimFactor * GetScaleForScaleDelta(oldScaleDeltaToAnimateFrom.Value); - // If the animation was from the shapes menu, invert the world-space too. - if (wasShowingShapesMenuLastFrame) { - oldScaleToAnimateFrom /= worldSpace.scale; - } - } - - // Create the primitive. - List newMeshes = new List(); - // TODO(bug) Replace pink with wireframe - int material = peltzerController.mode == ControllerMode.subtract ? - /* pink wireframe */ MaterialRegistry.PINK_WIREFRAME_ID : peltzerController.currentMaterial; - - Vector3 scale; - if (peltzerController.shapesMenu.showingShapeMenu) { - // Scale delta used here is 0 if the shapes menu is open: we want all shapes in the menu the - // same size. Further, we invert the worldSpace scale, as the shapes menu is at a constant scale. - scale = Vector3.one * GetScaleForScaleDelta(0) / worldSpace.scale; - } else { - scale = Vector3.one * GetScaleForScaleDelta(scaleDelta); - } - Primitives.Shape selectedVolumeShape = (Primitives.Shape)peltzerController.shapesMenu.CurrentItemId; - MMesh newMesh = Primitives.BuildPrimitive(selectedVolumeShape, scale, /* offset */ Vector3.zero, - model.GenerateMeshId(), material); - - newMesh.RecalcBounds(); - Vector3 baseBounds = newMesh.bounds.size; - - // If we're in block mode, leave the mesh unrotated (HeldMeshes will deal with it). If not in block - // mode, start with the controller's rotation (in MODEL space). - if (!peltzerController.isBlockMode) { - newMesh.rotation = peltzerController.LastRotationModel * newMesh.rotation; - } - newMesh.RecalcBounds(); - - if (peltzerController.isBlockMode) { - // For block mode, we start with the new mesh aligned in the position it would be if the controller - // were to be at Vector3.zero and pointing straight forward along the Z axis. - - // Note: we don't have to snap the position here if in block mode. Snapping the bounding box of the - // held meshes is done by the logic in HeldMeshes class. If we were to also do it here, the offset would - // be incorrect, as it would become a permanent offset of the held meshes (see bug). - - newMesh.offset = PREVIEW_OFFSET * newMesh.bounds.size.magnitude; - } else { - // Place the mesh in front of the controller (adjusting the position so that it doesn't overlap the - // controller -- the bigger the mesh, the further ahead we place it). - newMesh.offset = peltzerController.LastPositionModel + - peltzerController.LastRotationModel * (PREVIEW_OFFSET * newMesh.bounds.size.magnitude); - } - - Dictionary meshSize = new Dictionary(); - meshSize[newMesh.id] = baseBounds; - - // Set the new meshes as held. - ResetHeldMeshes(new List { newMesh }, meshSize); - - if (oldPosToAnimateFrom != null && oldScaleToAnimateFrom != null && heldMeshes.heldMeshes.Count > 0) { - // Set up animation. - MeshWithMaterialRenderer renderer = - heldMeshes.heldMeshes[0].Preview.GetComponent(); - renderer.AnimatePositionFrom(oldPosToAnimateFrom.Value); - // The scale is relative to the mesh's new size, so adjust accordingly: - float adjustedStartScale = oldScaleToAnimateFrom.Value.magnitude / - (Vector3.one * GetScaleForScaleDelta(scaleDelta)).magnitude; - renderer.AnimateScaleFrom(adjustedStartScale); - } else { - // Hide held meshes, they will unhide on next update if we are in the right mode. - heldMeshes.Hide(); - } - } + private bool IsSetRightHoverTooltipEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && (heldMeshes == null || (!heldMeshes.IsFilling && !heldMeshes.IsInserting)) + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.TOUCHPAD && args.TouchpadLocation == TouchpadLocation.RIGHT; + } - /// - /// Sets up the held meshes. - /// - /// The new meshes to be held, or null to indicate no held meshes. - private void ResetHeldMeshes(IEnumerable newMeshes, Dictionary sizes = null) { - if (heldMeshes != null) { - heldMeshes.DestroyPreviews(); - heldMeshes.HideSnapGuides(); - Destroy(heldMeshes); - heldMeshes = null; - } - if (newMeshes != null) { - heldMeshes = gameObject.AddComponent(); - heldMeshes.Setup( - newMeshes, - peltzerController.LastPositionModel, - peltzerController.LastRotationModel, - peltzerController, - worldSpace, - meshRepresentationCache, - peltzerController.isBlockMode ? HeldMeshes.PlacementMode.ABSOLUTE : HeldMeshes.PlacementMode.OFFSET, - null, - true, // use preview material - sizes); - - if (snapStartedWhileShapesMenuUp) { - snapStartedWhileShapesMenuUp = false; - heldMeshes.StartSnapping(model, spatialIndex); - } - } - } + private static bool IsUnsetAllHoverTooltipsEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Touchpad + && args.Action == ButtonAction.NONE; + } - /// - /// Sets the starting controller position, time and insertType to represent the start of an insert. - /// - private void StartInsertMesh() { - AssertOrThrow.NotNull(heldMeshes, "heldMeshes can't be null"); - heldMeshes.StartInserting(peltzerController.LastPositionModel); - peltzerController.SetVolumeOverlayActive(false); - } + public float GetScaleForScaleDelta(int delta) + { + return (delta + 6) * DEFAULT_PRIMITIVE_SCALE; + } - private void EndInsertMesh() { - AssertOrThrow.NotNull(heldMeshes, "heldMeshes can't be null"); - heldMeshes.FinishInserting(); - peltzerController.SetVolumeOverlayActive(true); - } + /// + /// Creates a new mesh given the tool's current parameters and attaches it to the preview Game Object. + /// + /// If not null, this indicates the previous scaleDelta from + /// which to animate from. Note: this is only honored when there is a single held mesh. + private void CreateNewVolumeMesh(int? oldScaleDeltaToAnimateFrom = null) + { + Vector3? oldPosToAnimateFrom = null; + Vector3? oldScaleToAnimateFrom = null; + if (oldScaleDeltaToAnimateFrom != null && heldMeshes != null && heldMeshes.heldMeshes.Count == 1) + { + // Get the initial pos/scale. We will use these at the end of the method to set up the animation. + MeshWithMaterialRenderer renderer = + heldMeshes.heldMeshes[0].Preview.GetComponent(); + oldPosToAnimateFrom = renderer.GetPositionInModelSpace(); + // Adjust the scale by the current animated scale, if necessary (so that the animation + // doesn't jump if we rescale the mesh mid-animation). + Vector3 oldScaleAnimFactor = renderer.GetCurrentAnimatedScale(); + oldScaleToAnimateFrom = oldScaleAnimFactor * GetScaleForScaleDelta(oldScaleDeltaToAnimateFrom.Value); + // If the animation was from the shapes menu, invert the world-space too. + if (wasShowingShapesMenuLastFrame) + { + oldScaleToAnimateFrom /= worldSpace.scale; + } + } + + // Create the primitive. + List newMeshes = new List(); + // TODO(bug) Replace pink with wireframe + int material = peltzerController.mode == ControllerMode.subtract ? + /* pink wireframe */ MaterialRegistry.PINK_WIREFRAME_ID : peltzerController.currentMaterial; + + Vector3 scale; + if (peltzerController.shapesMenu.showingShapeMenu) + { + // Scale delta used here is 0 if the shapes menu is open: we want all shapes in the menu the + // same size. Further, we invert the worldSpace scale, as the shapes menu is at a constant scale. + scale = Vector3.one * GetScaleForScaleDelta(0) / worldSpace.scale; + } + else + { + scale = Vector3.one * GetScaleForScaleDelta(scaleDelta); + } + Primitives.Shape selectedVolumeShape = (Primitives.Shape)peltzerController.shapesMenu.CurrentItemId; + MMesh newMesh = Primitives.BuildPrimitive(selectedVolumeShape, scale, /* offset */ Vector3.zero, + model.GenerateMeshId(), material); + + newMesh.RecalcBounds(); + Vector3 baseBounds = newMesh.bounds.size; + + // If we're in block mode, leave the mesh unrotated (HeldMeshes will deal with it). If not in block + // mode, start with the controller's rotation (in MODEL space). + if (!peltzerController.isBlockMode) + { + newMesh.rotation = peltzerController.LastRotationModel * newMesh.rotation; + } + newMesh.RecalcBounds(); + + if (peltzerController.isBlockMode) + { + // For block mode, we start with the new mesh aligned in the position it would be if the controller + // were to be at Vector3.zero and pointing straight forward along the Z axis. + + // Note: we don't have to snap the position here if in block mode. Snapping the bounding box of the + // held meshes is done by the logic in HeldMeshes class. If we were to also do it here, the offset would + // be incorrect, as it would become a permanent offset of the held meshes (see bug). + + newMesh.offset = PREVIEW_OFFSET * newMesh.bounds.size.magnitude; + } + else + { + // Place the mesh in front of the controller (adjusting the position so that it doesn't overlap the + // controller -- the bigger the mesh, the further ahead we place it). + newMesh.offset = peltzerController.LastPositionModel + + peltzerController.LastRotationModel * (PREVIEW_OFFSET * newMesh.bounds.size.magnitude); + } + + Dictionary meshSize = new Dictionary(); + meshSize[newMesh.id] = baseBounds; + + // Set the new meshes as held. + ResetHeldMeshes(new List { newMesh }, meshSize); + + if (oldPosToAnimateFrom != null && oldScaleToAnimateFrom != null && heldMeshes.heldMeshes.Count > 0) + { + // Set up animation. + MeshWithMaterialRenderer renderer = + heldMeshes.heldMeshes[0].Preview.GetComponent(); + renderer.AnimatePositionFrom(oldPosToAnimateFrom.Value); + // The scale is relative to the mesh's new size, so adjust accordingly: + float adjustedStartScale = oldScaleToAnimateFrom.Value.magnitude / + (Vector3.one * GetScaleForScaleDelta(scaleDelta)).magnitude; + renderer.AnimateScaleFrom(adjustedStartScale); + } + else + { + // Hide held meshes, they will unhide on next update if we are in the right mode. + heldMeshes.Hide(); + } + } - /// - /// Drops the previewed mesh into the scene at the controller's current location, - /// actually updating our data model. - /// - private void InsertVolumeMesh() { - AssertOrThrow.NotNull(heldMeshes, "heldMeshes can't be null"); - HeldMeshes.HeldMesh heldMesh = heldMeshes.GetFirstHeldMesh(); - MMesh meshToInsert = heldMesh.Mesh; - meshToInsert.ChangeId(model.GenerateMeshId()); - MeshWithMaterialRenderer meshWithMaterialRenderer = - heldMesh.Preview.GetComponent(); - meshToInsert.offset = meshWithMaterialRenderer.GetPositionInModelSpace(); - meshToInsert.rotation = meshWithMaterialRenderer.GetOrientationInModelSpace(); - // Abort entire volume insert on any error. - if (peltzerController.mode == ControllerMode.insertVolume && !model.CanAddMesh(meshToInsert)) { - audioLibrary.PlayClip(audioLibrary.errorSound); - peltzerController.TriggerHapticFeedback(); - CreateNewVolumeMesh(); - return; - } - - if (peltzerController.mode == ControllerMode.insertVolume) { - bool applyAddMeshEffect = peltzerController.currentMaterial != MaterialRegistry.GEM_ID && - peltzerController.currentMaterial != MaterialRegistry.GLASS_ID; - model.ApplyCommand(new AddMeshCommand(meshToInsert, applyAddMeshEffect)); - lastInsertTime = Time.time; - audioLibrary.PlayClip(audioLibrary.insertVolumeSound); - peltzerController.TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_3, /* durationSeconds */ 0.05f, /* strength */ 0.3f); - Primitives.Shape selectedShape = (Primitives.Shape)peltzerController.shapesMenu.CurrentItemId; - } else if (peltzerController.mode == ControllerMode.subtract) { - if (CsgOperations.SubtractMeshFromModel(model, spatialIndex, meshToInsert)) { - audioLibrary.PlayClip(audioLibrary.deleteSound); - peltzerController.TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_3, /* durationSeconds */ 0.05f, /* strength */ 0.3f); - } - } - - PeltzerMain.Instance.volumesInserted++; - CreateNewVolumeMesh(); - } + /// + /// Sets up the held meshes. + /// + /// The new meshes to be held, or null to indicate no held meshes. + private void ResetHeldMeshes(IEnumerable newMeshes, Dictionary sizes = null) + { + if (heldMeshes != null) + { + heldMeshes.DestroyPreviews(); + heldMeshes.HideSnapGuides(); + Destroy(heldMeshes); + heldMeshes = null; + } + if (newMeshes != null) + { + heldMeshes = gameObject.AddComponent(); + heldMeshes.Setup( + newMeshes, + peltzerController.LastPositionModel, + peltzerController.LastRotationModel, + peltzerController, + worldSpace, + meshRepresentationCache, + peltzerController.isBlockMode ? HeldMeshes.PlacementMode.ABSOLUTE : HeldMeshes.PlacementMode.OFFSET, + null, + true, // use preview material + sizes); + + if (snapStartedWhileShapesMenuUp) + { + snapStartedWhileShapesMenuUp = false; + heldMeshes.StartSnapping(model, spatialIndex); + } + } + } - /// - /// Explicitly set the scale delta to the given value. - /// - public void SetScaleTo(int delta) { - scaleDelta = delta; - CreateNewVolumeMesh(); - } + /// + /// Sets the starting controller position, time and insertType to represent the start of an insert. + /// + private void StartInsertMesh() + { + AssertOrThrow.NotNull(heldMeshes, "heldMeshes can't be null"); + heldMeshes.StartInserting(peltzerController.LastPositionModel); + peltzerController.SetVolumeOverlayActive(false); + } - /// - /// Increases or decreases the scale of the mesh to be inserted. - /// - /// Whether to scale up (if false, scale down). - /// How many steps to scale up or down. - public void ChangeScale(bool scaleUp, int steps) { - AssertOrThrow.NotNull(heldMeshes, "heldMeshes can't be null"); - if ((scaleUp && !PeltzerMain.Instance.restrictionManager.touchpadUpAllowed) - || (!scaleUp && !PeltzerMain.Instance.restrictionManager.touchpadDownAllowed)) { - return; - } - - // Don't allow rapid-scaling in the tutorial, per bug - if (PeltzerMain.Instance.tutorialManager.TutorialOccurring()) { - steps = 1; - } - - // Can't scale if the restriction manager doesn't allow us to. - if (!PeltzerMain.Instance.restrictionManager.scaleOnVolumeInsertionAllowed) { - return; - } - - // Can't scale whilst filling. - if (heldMeshes.IsFilling) { - return; - } - - // The 'scale from' should be 0 (default size) if the menu was open, or the previous size otherwise. - int oldScaleDelta = peltzerController.shapesMenu.showingShapeMenu ? 0 : scaleDelta; - - // Hide the shapes menu if the user starts scaling. - peltzerController.shapesMenu.Hide(); - - if (IsLongTermScale()) { - steps += LONG_TERM_SCALE_FACTOR; - } - // Just change the delta and regenerate it. - if (scaleUp) { - if (scaleDelta == MAX_SCALE_DELTA) { - // If we are already at the max scale, don't try and scale further. - StopScaling(); - audioLibrary.PlayClip(audioLibrary.errorSound); - return; - } else if (scaleDelta + steps > MAX_SCALE_DELTA) { - // If scaling by the specified amount would take us past the max scale, just scale up to the max scale. - scaleDelta = MAX_SCALE_DELTA; - scaleType = ScaleType.SCALE_UP; - audioLibrary.PlayClip(audioLibrary.incrementSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - } else { - scaleDelta += steps; - scaleType = ScaleType.SCALE_UP; - audioLibrary.PlayClip(audioLibrary.incrementSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - } - } else { - if (scaleDelta == MIN_SCALE_DELTA) { - // If we are already at the min scale, don't try and scale further. - StopScaling(); - audioLibrary.PlayClip(audioLibrary.errorSound); - return; - } else if (scaleDelta - steps < MIN_SCALE_DELTA) { - // If scaling by the specified amount would take us past the min scale, just scale down to the min scale. - scaleDelta = MIN_SCALE_DELTA; - scaleType = ScaleType.SCALE_DOWN; - audioLibrary.PlayClip(audioLibrary.decrementSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - } else { - scaleDelta -= steps; - scaleType = ScaleType.SCALE_DOWN; - audioLibrary.PlayClip(audioLibrary.decrementSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - } - } - peltzerController.TriggerHapticFeedback(); - CreateNewVolumeMesh(oldScaleDelta); - return; - } + private void EndInsertMesh() + { + AssertOrThrow.NotNull(heldMeshes, "heldMeshes can't be null"); + heldMeshes.FinishInserting(); + peltzerController.SetVolumeOverlayActive(true); + } - /// - /// Stop scaling. - /// - private void StopScaling() { - scaleType = ScaleType.NONE; - continuousScaleEvents = 0; - longTermScaleStartTime = float.MaxValue; - } + /// + /// Drops the previewed mesh into the scene at the controller's current location, + /// actually updating our data model. + /// + private void InsertVolumeMesh() + { + AssertOrThrow.NotNull(heldMeshes, "heldMeshes can't be null"); + HeldMeshes.HeldMesh heldMesh = heldMeshes.GetFirstHeldMesh(); + MMesh meshToInsert = heldMesh.Mesh; + meshToInsert.ChangeId(model.GenerateMeshId()); + MeshWithMaterialRenderer meshWithMaterialRenderer = + heldMesh.Preview.GetComponent(); + meshToInsert.offset = meshWithMaterialRenderer.GetPositionInModelSpace(); + meshToInsert.rotation = meshWithMaterialRenderer.GetOrientationInModelSpace(); + // Abort entire volume insert on any error. + if (peltzerController.mode == ControllerMode.insertVolume && !model.CanAddMesh(meshToInsert)) + { + audioLibrary.PlayClip(audioLibrary.errorSound); + peltzerController.TriggerHapticFeedback(); + CreateNewVolumeMesh(); + return; + } + + if (peltzerController.mode == ControllerMode.insertVolume) + { + bool applyAddMeshEffect = peltzerController.currentMaterial != MaterialRegistry.GEM_ID && + peltzerController.currentMaterial != MaterialRegistry.GLASS_ID; + model.ApplyCommand(new AddMeshCommand(meshToInsert, applyAddMeshEffect)); + lastInsertTime = Time.time; + audioLibrary.PlayClip(audioLibrary.insertVolumeSound); + peltzerController.TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_3, /* durationSeconds */ 0.05f, /* strength */ 0.3f); + Primitives.Shape selectedShape = (Primitives.Shape)peltzerController.shapesMenu.CurrentItemId; + } + else if (peltzerController.mode == ControllerMode.subtract) + { + if (CsgOperations.SubtractMeshFromModel(model, spatialIndex, meshToInsert)) + { + audioLibrary.PlayClip(audioLibrary.deleteSound); + peltzerController.TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_3, /* durationSeconds */ 0.05f, /* strength */ 0.3f); + } + } + + PeltzerMain.Instance.volumesInserted++; + CreateNewVolumeMesh(); + } - /// - /// Whether scaling has been happening continuously over the threshold set by FAST_SCALE_THRESHOLD. - /// - /// True if this is a long term scale event, false otherwise. - private bool IsLongTermScale() { - return Time.time > longTermScaleStartTime; - } + /// + /// Explicitly set the scale delta to the given value. + /// + public void SetScaleTo(int delta) + { + scaleDelta = delta; + CreateNewVolumeMesh(); + } - /// - /// An event handler that listens for controller input and delegates accordingly. - /// - /// The sender of the controller event. - /// The controller event arguments. - private void ControllerEventHandler(object sender, ControllerEventArgs args) { - // If we are not in insert or subtract mode, do nothing. - if ((peltzerController.mode != ControllerMode.insertVolume && peltzerController.mode != ControllerMode.subtract) - || PeltzerMain.Instance.peltzerController.isPointingAtMenu) { - return; - } - - // Check for "change shape" events. - if (IsChangeShapeEvent(args)) { - if (!PeltzerMain.Instance.restrictionManager.shapesMenuAllowed) { - return; - } - - bool forward = args.TouchpadLocation == TouchpadLocation.RIGHT; - if (forward) { - if (!peltzerController.shapesMenu.SelectNextShapesMenuItem()) { - audioLibrary.PlayClip(audioLibrary.shapeMenuEndSound); - peltzerController.TriggerHapticFeedback(); - } else { - audioLibrary.PlayClip(audioLibrary.swipeRightSound); - } - peltzerController.TriggerHapticFeedback(); - } else { - if (!peltzerController.shapesMenu.SelectPreviousShapesMenuItem()) { - audioLibrary.PlayClip(audioLibrary.shapeMenuEndSound); + /// + /// Increases or decreases the scale of the mesh to be inserted. + /// + /// Whether to scale up (if false, scale down). + /// How many steps to scale up or down. + public void ChangeScale(bool scaleUp, int steps) + { + AssertOrThrow.NotNull(heldMeshes, "heldMeshes can't be null"); + if ((scaleUp && !PeltzerMain.Instance.restrictionManager.touchpadUpAllowed) + || (!scaleUp && !PeltzerMain.Instance.restrictionManager.touchpadDownAllowed)) + { + return; + } + + // Don't allow rapid-scaling in the tutorial, per bug + if (PeltzerMain.Instance.tutorialManager.TutorialOccurring()) + { + steps = 1; + } + + // Can't scale if the restriction manager doesn't allow us to. + if (!PeltzerMain.Instance.restrictionManager.scaleOnVolumeInsertionAllowed) + { + return; + } + + // Can't scale whilst filling. + if (heldMeshes.IsFilling) + { + return; + } + + // The 'scale from' should be 0 (default size) if the menu was open, or the previous size otherwise. + int oldScaleDelta = peltzerController.shapesMenu.showingShapeMenu ? 0 : scaleDelta; + + // Hide the shapes menu if the user starts scaling. + peltzerController.shapesMenu.Hide(); + + if (IsLongTermScale()) + { + steps += LONG_TERM_SCALE_FACTOR; + } + // Just change the delta and regenerate it. + if (scaleUp) + { + if (scaleDelta == MAX_SCALE_DELTA) + { + // If we are already at the max scale, don't try and scale further. + StopScaling(); + audioLibrary.PlayClip(audioLibrary.errorSound); + return; + } + else if (scaleDelta + steps > MAX_SCALE_DELTA) + { + // If scaling by the specified amount would take us past the max scale, just scale up to the max scale. + scaleDelta = MAX_SCALE_DELTA; + scaleType = ScaleType.SCALE_UP; + audioLibrary.PlayClip(audioLibrary.incrementSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + } + else + { + scaleDelta += steps; + scaleType = ScaleType.SCALE_UP; + audioLibrary.PlayClip(audioLibrary.incrementSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + } + } + else + { + if (scaleDelta == MIN_SCALE_DELTA) + { + // If we are already at the min scale, don't try and scale further. + StopScaling(); + audioLibrary.PlayClip(audioLibrary.errorSound); + return; + } + else if (scaleDelta - steps < MIN_SCALE_DELTA) + { + // If scaling by the specified amount would take us past the min scale, just scale down to the min scale. + scaleDelta = MIN_SCALE_DELTA; + scaleType = ScaleType.SCALE_DOWN; + audioLibrary.PlayClip(audioLibrary.decrementSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + } + else + { + scaleDelta -= steps; + scaleType = ScaleType.SCALE_DOWN; + audioLibrary.PlayClip(audioLibrary.decrementSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + } + } peltzerController.TriggerHapticFeedback(); - } else { - audioLibrary.PlayClip(audioLibrary.swipeLeftSound); - } - peltzerController.TriggerHapticFeedback(); - } - } - - if (IsStartInsertVolumeOrCopyEvent(args)) { - // If the shapes menu was open, just close it instead of starting an insertion or copy.. - if (peltzerController.shapesMenu.showingShapeMenu) { - peltzerController.shapesMenu.Hide(); - return; - } - peltzerController.TriggerHapticFeedback(); - StartInsertMesh(); - - } else if (IsEndInsertVolumeEvent(args)) { - if (heldMeshes != null && (heldMeshes.IsInserting || heldMeshes.IsFilling)) { - heldMeshes.HideSnapGuides(); - InsertVolumeMesh(); - EndInsertMesh(); - } - } else if (Features.useContinuousSnapDetection && IsStartSnapDetectionEvent(args)) { - // Show the snap guides if the trigger is slightly pressed. - heldMeshes.DetectSnap(); - } else if (Features.useContinuousSnapDetection && IsStopSnapDetectionEvent(args)) { - // If we are previewing the snap guide with a half trigger press and then release the trigger, - // hide the guide. - heldMeshes.HideSnapGuides(); - } else if (IsStartSnapEvent(args)) { - // Close the shapes menu before starting a snap, and note that we started a snap while it was open, - // as closing the menu triggers heldMeshes to be reset in CreateNewVolumeMesh. - if (completedSnaps < SNAP_KNOW_HOW_COUNT) { - PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); - } - if (peltzerController.shapesMenu.showingShapeMenu) { - peltzerController.shapesMenu.Hide(); - snapStartedWhileShapesMenuUp = true; - } - if (heldMeshes != null) { - heldMeshes.StartSnapping(model, spatialIndex); - } - } else if (IsEndSnapEvent(args)) { - if (heldMeshes != null) { - heldMeshes.StopSnapping(); - heldMeshes.HideSnapGuides(); - PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); - completedSnaps++; - } - } else if (IsScaleEvent(args)) { - if (PeltzerMain.Instance.restrictionManager.scaleOnVolumeInsertionAllowed - && heldMeshes != null) { - if (scaleType == ScaleType.NONE) { - longTermScaleStartTime = Time.time + FAST_SCALE_THRESHOLD; - } - continuousScaleEvents++; - ChangeScale(args.TouchpadLocation == TouchpadLocation.TOP, continuousScaleEvents); - } - } else if (IsStopScalingEvent(args)) { - if (heldMeshes != null) { - StopScaling(); - } - } else { - if (args.TouchpadOverlay != TouchpadOverlay.VOLUME_INSERTER) { - UnsetAllHoverTooltips(); - } else { - if (IsSetUpHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadUpAllowed) { - SetHoverTooltip(peltzerController.controllerGeometry.volumeInserterTooltipUp, TouchpadHoverState.UP); - } else if (IsSetDownHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadDownAllowed) { - SetHoverTooltip(peltzerController.controllerGeometry.volumeInserterTooltipDown, TouchpadHoverState.DOWN); - } else if (IsSetLeftHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed) { - SetHoverTooltip(peltzerController.controllerGeometry.volumeInserterTooltipLeft, TouchpadHoverState.LEFT); - } else if (IsSetRightHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadRightAllowed) { - SetHoverTooltip(peltzerController.controllerGeometry.volumeInserterTooltipRight, TouchpadHoverState.RIGHT); - } else if (IsUnsetAllHoverTooltipsEvent(args)) { - UnsetAllHoverTooltips(); - } - } - } - - if (IsSwitchModeEvent(args)) { - if (peltzerController.mode == ControllerMode.insertVolume) { - peltzerController.ChangeMode(ControllerMode.subtract); - peltzerController.shapesMenu.ChangeShapesMenuMaterial(MaterialRegistry.PINK_WIREFRAME_ID); - } else if (peltzerController.mode == ControllerMode.subtract) { - peltzerController.ChangeMode(ControllerMode.insertVolume); - peltzerController.shapesMenu.ChangeShapesMenuMaterial(peltzerController.currentMaterial); - } - } - } + CreateNewVolumeMesh(oldScaleDelta); + return; + } - private void ModeChangeEventHandler(ControllerMode oldMode, ControllerMode newMode) { - if (oldMode == ControllerMode.insertVolume || oldMode == ControllerMode.subtract) { - peltzerController.shapesMenu.Hide(); - UnsetAllHoverTooltips(); - } + /// + /// Stop scaling. + /// + private void StopScaling() + { + scaleType = ScaleType.NONE; + continuousScaleEvents = 0; + longTermScaleStartTime = float.MaxValue; + } - if (newMode == ControllerMode.insertVolume || newMode == ControllerMode.subtract) { - CreateNewVolumeMesh(); + /// + /// Whether scaling has been happening continuously over the threshold set by FAST_SCALE_THRESHOLD. + /// + /// True if this is a long term scale event, false otherwise. + private bool IsLongTermScale() + { + return Time.time > longTermScaleStartTime; + } - if (completedSnaps < SNAP_KNOW_HOW_COUNT) { - PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); + /// + /// An event handler that listens for controller input and delegates accordingly. + /// + /// The sender of the controller event. + /// The controller event arguments. + private void ControllerEventHandler(object sender, ControllerEventArgs args) + { + // If we are not in insert or subtract mode, do nothing. + if ((peltzerController.mode != ControllerMode.insertVolume && peltzerController.mode != ControllerMode.subtract) + || PeltzerMain.Instance.peltzerController.isPointingAtMenu) + { + return; + } + + // Check for "change shape" events. + if (IsChangeShapeEvent(args)) + { + if (!PeltzerMain.Instance.restrictionManager.shapesMenuAllowed) + { + return; + } + + bool forward = args.TouchpadLocation == TouchpadLocation.RIGHT; + if (forward) + { + if (!peltzerController.shapesMenu.SelectNextShapesMenuItem()) + { + audioLibrary.PlayClip(audioLibrary.shapeMenuEndSound); + peltzerController.TriggerHapticFeedback(); + } + else + { + audioLibrary.PlayClip(audioLibrary.swipeRightSound); + } + peltzerController.TriggerHapticFeedback(); + } + else + { + if (!peltzerController.shapesMenu.SelectPreviousShapesMenuItem()) + { + audioLibrary.PlayClip(audioLibrary.shapeMenuEndSound); + peltzerController.TriggerHapticFeedback(); + } + else + { + audioLibrary.PlayClip(audioLibrary.swipeLeftSound); + } + peltzerController.TriggerHapticFeedback(); + } + } + + if (IsStartInsertVolumeOrCopyEvent(args)) + { + // If the shapes menu was open, just close it instead of starting an insertion or copy.. + if (peltzerController.shapesMenu.showingShapeMenu) + { + peltzerController.shapesMenu.Hide(); + return; + } + peltzerController.TriggerHapticFeedback(); + StartInsertMesh(); + + } + else if (IsEndInsertVolumeEvent(args)) + { + if (heldMeshes != null && (heldMeshes.IsInserting || heldMeshes.IsFilling)) + { + heldMeshes.HideSnapGuides(); + InsertVolumeMesh(); + EndInsertMesh(); + } + } + else if (Features.useContinuousSnapDetection && IsStartSnapDetectionEvent(args)) + { + // Show the snap guides if the trigger is slightly pressed. + heldMeshes.DetectSnap(); + } + else if (Features.useContinuousSnapDetection && IsStopSnapDetectionEvent(args)) + { + // If we are previewing the snap guide with a half trigger press and then release the trigger, + // hide the guide. + heldMeshes.HideSnapGuides(); + } + else if (IsStartSnapEvent(args)) + { + // Close the shapes menu before starting a snap, and note that we started a snap while it was open, + // as closing the menu triggers heldMeshes to be reset in CreateNewVolumeMesh. + if (completedSnaps < SNAP_KNOW_HOW_COUNT) + { + PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); + } + if (peltzerController.shapesMenu.showingShapeMenu) + { + peltzerController.shapesMenu.Hide(); + snapStartedWhileShapesMenuUp = true; + } + if (heldMeshes != null) + { + heldMeshes.StartSnapping(model, spatialIndex); + } + } + else if (IsEndSnapEvent(args)) + { + if (heldMeshes != null) + { + heldMeshes.StopSnapping(); + heldMeshes.HideSnapGuides(); + PeltzerMain.Instance.paletteController.HideSnapAssistanceTooltips(); + completedSnaps++; + } + } + else if (IsScaleEvent(args)) + { + if (PeltzerMain.Instance.restrictionManager.scaleOnVolumeInsertionAllowed + && heldMeshes != null) + { + if (scaleType == ScaleType.NONE) + { + longTermScaleStartTime = Time.time + FAST_SCALE_THRESHOLD; + } + continuousScaleEvents++; + ChangeScale(args.TouchpadLocation == TouchpadLocation.TOP, continuousScaleEvents); + } + } + else if (IsStopScalingEvent(args)) + { + if (heldMeshes != null) + { + StopScaling(); + } + } + else + { + if (args.TouchpadOverlay != TouchpadOverlay.VOLUME_INSERTER) + { + UnsetAllHoverTooltips(); + } + else + { + if (IsSetUpHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadUpAllowed) + { + SetHoverTooltip(peltzerController.controllerGeometry.volumeInserterTooltipUp, TouchpadHoverState.UP); + } + else if (IsSetDownHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadDownAllowed) + { + SetHoverTooltip(peltzerController.controllerGeometry.volumeInserterTooltipDown, TouchpadHoverState.DOWN); + } + else if (IsSetLeftHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed) + { + SetHoverTooltip(peltzerController.controllerGeometry.volumeInserterTooltipLeft, TouchpadHoverState.LEFT); + } + else if (IsSetRightHoverTooltipEvent(args) && PeltzerMain.Instance.restrictionManager.touchpadRightAllowed) + { + SetHoverTooltip(peltzerController.controllerGeometry.volumeInserterTooltipRight, TouchpadHoverState.RIGHT); + } + else if (IsUnsetAllHoverTooltipsEvent(args)) + { + UnsetAllHoverTooltips(); + } + } + } + + if (IsSwitchModeEvent(args)) + { + if (peltzerController.mode == ControllerMode.insertVolume) + { + peltzerController.ChangeMode(ControllerMode.subtract); + peltzerController.shapesMenu.ChangeShapesMenuMaterial(MaterialRegistry.PINK_WIREFRAME_ID); + } + else if (peltzerController.mode == ControllerMode.subtract) + { + peltzerController.ChangeMode(ControllerMode.insertVolume); + peltzerController.shapesMenu.ChangeShapesMenuMaterial(peltzerController.currentMaterial); + } + } } - } - } - private void MaterialChangeHandler(int newMaterialId) { - FaceProperties newFaceProperties = new FaceProperties(newMaterialId); - foreach (HeldMeshes.HeldMesh heldMesh in heldMeshes.heldMeshes) { - foreach (Face face in heldMesh.Mesh.GetFaces()) { - face.SetProperties(newFaceProperties); + private void ModeChangeEventHandler(ControllerMode oldMode, ControllerMode newMode) + { + if (oldMode == ControllerMode.insertVolume || oldMode == ControllerMode.subtract) + { + peltzerController.shapesMenu.Hide(); + UnsetAllHoverTooltips(); + } + + if (newMode == ControllerMode.insertVolume || newMode == ControllerMode.subtract) + { + CreateNewVolumeMesh(); + + if (completedSnaps < SNAP_KNOW_HOW_COUNT) + { + PeltzerMain.Instance.paletteController.ShowSnapAssistanceTooltip(); + } + } } - heldMesh.Preview.GetComponent().OverrideWithNewMaterial(newMaterialId); - } - } - private void ShapeChangedHandler(int newShapeMenuItemId) { - if (newShapeMenuItemId == ShapesMenu.COPY_MODE_ID) { - // Start copy mode. - // TODO(bug): show copy mode affordance (new tool head?). - ResetHeldMeshes(null); - } else { - selector.DeselectAll(); - CreateNewVolumeMesh(); - } - } + private void MaterialChangeHandler(int newMaterialId) + { + FaceProperties newFaceProperties = new FaceProperties(newMaterialId); + foreach (HeldMeshes.HeldMesh heldMesh in heldMeshes.heldMeshes) + { + foreach (Face face in heldMesh.Mesh.GetFaces()) + { + face.SetProperties(newFaceProperties); + } + heldMesh.Preview.GetComponent().OverrideWithNewMaterial(newMaterialId); + } + } - private void BlockModeChangedHandler(bool isBlockMode) { - if (peltzerController.mode == ControllerMode.insertVolume || peltzerController.mode == ControllerMode.subtract) { - CreateNewVolumeMesh(); - } - } + private void ShapeChangedHandler(int newShapeMenuItemId) + { + if (newShapeMenuItemId == ShapesMenu.COPY_MODE_ID) + { + // Start copy mode. + // TODO(bug): show copy mode affordance (new tool head?). + ResetHeldMeshes(null); + } + else + { + selector.DeselectAll(); + CreateNewVolumeMesh(); + } + } - public bool IsFilling() { - return heldMeshes.IsFilling; - } + private void BlockModeChangedHandler(bool isBlockMode) + { + if (peltzerController.mode == ControllerMode.insertVolume || peltzerController.mode == ControllerMode.subtract) + { + CreateNewVolumeMesh(); + } + } - /// - /// Makes only the supplied tooltip visible and ensures the others are off. - /// - /// The tooltip text to activate. - /// The hover state. - private void SetHoverTooltip(GameObject tooltip, TouchpadHoverState state) { - if (!tooltip.activeSelf) { - UnsetAllHoverTooltips(); - tooltip.SetActive(true); - peltzerController.SetTouchpadHoverTexture(state); - peltzerController.TriggerHapticFeedback( - HapticFeedback.HapticFeedbackType.FEEDBACK_1, - 0.003f, - 0.15f - ); - } - } + public bool IsFilling() + { + return heldMeshes.IsFilling; + } - /// - /// Unset all of the touchpad hover text tooltips. - /// - private void UnsetAllHoverTooltips() { - peltzerController.controllerGeometry.volumeInserterTooltipUp.SetActive(false); - peltzerController.controllerGeometry.volumeInserterTooltipDown.SetActive(false); - peltzerController.controllerGeometry.volumeInserterTooltipLeft.SetActive(false); - peltzerController.controllerGeometry.volumeInserterTooltipRight.SetActive(false); - peltzerController.SetTouchpadHoverTexture(TouchpadHoverState.NONE); - } + /// + /// Makes only the supplied tooltip visible and ensures the others are off. + /// + /// The tooltip text to activate. + /// The hover state. + private void SetHoverTooltip(GameObject tooltip, TouchpadHoverState state) + { + if (!tooltip.activeSelf) + { + UnsetAllHoverTooltips(); + tooltip.SetActive(true); + peltzerController.SetTouchpadHoverTexture(state); + peltzerController.TriggerHapticFeedback( + HapticFeedback.HapticFeedbackType.FEEDBACK_1, + 0.003f, + 0.15f + ); + } + } - public void ClearState() { - scaleDelta = DEFAULT_SCALE_DELTA; - snapStartedWhileShapesMenuUp = false; - CreateNewVolumeMesh(); + /// + /// Unset all of the touchpad hover text tooltips. + /// + private void UnsetAllHoverTooltips() + { + peltzerController.controllerGeometry.volumeInserterTooltipUp.SetActive(false); + peltzerController.controllerGeometry.volumeInserterTooltipDown.SetActive(false); + peltzerController.controllerGeometry.volumeInserterTooltipLeft.SetActive(false); + peltzerController.controllerGeometry.volumeInserterTooltipRight.SetActive(false); + peltzerController.SetTouchpadHoverTexture(TouchpadHoverState.NONE); + } + + public void ClearState() + { + scaleDelta = DEFAULT_SCALE_DELTA; + snapStartedWhileShapesMenuUp = false; + CreateNewVolumeMesh(); + } } - } } diff --git a/Assets/Scripts/tools/utils/GridHighlightComponent.cs b/Assets/Scripts/tools/utils/GridHighlightComponent.cs index 555055b5..0224c4c7 100644 --- a/Assets/Scripts/tools/utils/GridHighlightComponent.cs +++ b/Assets/Scripts/tools/utils/GridHighlightComponent.cs @@ -16,55 +16,62 @@ using com.google.apps.peltzer.client.model.main; using UnityEngine; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// This component is responsible for managing the relationship between Poly and the rendered version of the - /// universal grid. Currently it sets up the grid, adjusts its scale based on zoom level, and kicks off the render. - /// - public class GridHighlightComponent : MonoBehaviour { - // Number of verts to draw in each row of the grid - private int numVertsInRow = 7; - // Scale of the grid - a value of 4 will have every visible unity represent 4 grid units for instance. - private int gridScale = 4; - private Material gridMaterial; - private WorldSpace worldSpace; - private PeltzerController peltzerController; - private GridHighlighter gridHighlight; - - private float origWorldSpaceSpacing; - public void Setup(MaterialLibrary materialLibrary, WorldSpace worldSpace, PeltzerController peltzerController) { - this.gridMaterial = new Material(materialLibrary.gridMaterial); - this.worldSpace = worldSpace; - this.peltzerController = peltzerController; - this.gridHighlight = new GridHighlighter(); - this.gridHighlight.InitGrid(numVertsInRow, gridScale); - this.origWorldSpaceSpacing = GridUtils.GRID_SIZE * gridScale; - } - +namespace com.google.apps.peltzer.client.tools.utils +{ /// - /// Renders the grid if we're in block mode. + /// This component is responsible for managing the relationship between Poly and the rendered version of the + /// universal grid. Currently it sets up the grid, adjusts its scale based on zoom level, and kicks off the render. /// - public void LateUpdate() { - if (peltzerController.isBlockMode) { - // Resize the grid if our current zoom has caused it to get either too coarse or too fine grained. - // TODO(bug): We'll need to tweak these based on feedback. + public class GridHighlightComponent : MonoBehaviour + { + // Number of verts to draw in each row of the grid + private int numVertsInRow = 7; + // Scale of the grid - a value of 4 will have every visible unity represent 4 grid units for instance. + private int gridScale = 4; + private Material gridMaterial; + private WorldSpace worldSpace; + private PeltzerController peltzerController; + private GridHighlighter gridHighlight; - if (worldSpace.scale * GridUtils.GRID_SIZE * gridScale < 0.75f * origWorldSpaceSpacing) { - gridScale = gridScale * 2; + private float origWorldSpaceSpacing; + public void Setup(MaterialLibrary materialLibrary, WorldSpace worldSpace, PeltzerController peltzerController) + { + this.gridMaterial = new Material(materialLibrary.gridMaterial); + this.worldSpace = worldSpace; + this.peltzerController = peltzerController; + this.gridHighlight = new GridHighlighter(); + this.gridHighlight.InitGrid(numVertsInRow, gridScale); + this.origWorldSpaceSpacing = GridUtils.GRID_SIZE * gridScale; } - else if (worldSpace.scale * GridUtils.GRID_SIZE * gridScale > 1.5f * origWorldSpaceSpacing) { - gridScale = Mathf.Max(1, gridScale / 2); + + /// + /// Renders the grid if we're in block mode. + /// + public void LateUpdate() + { + if (peltzerController.isBlockMode) + { + // Resize the grid if our current zoom has caused it to get either too coarse or too fine grained. + // TODO(bug): We'll need to tweak these based on feedback. + + if (worldSpace.scale * GridUtils.GRID_SIZE * gridScale < 0.75f * origWorldSpaceSpacing) + { + gridScale = gridScale * 2; + } + else if (worldSpace.scale * GridUtils.GRID_SIZE * gridScale > 1.5f * origWorldSpaceSpacing) + { + gridScale = Mathf.Max(1, gridScale / 2); + } + Vector3 curPos = peltzerController.LastPositionModel; + Vector3 curPosWorld = worldSpace.ModelToWorld(curPos); + // Set the shader uniforms for center of grid and fade radius + gridMaterial.SetVector("_GridCenterWorld", new Vector4(curPosWorld.x, curPosWorld.y, curPosWorld.z)); + float worldSpaceRadius = 0.66f * worldSpace.scale + * Mathf.Floor(numVertsInRow / 2) * GridUtils.GRID_SIZE * gridScale; + gridMaterial.SetFloat("_GridRenderRadius", worldSpaceRadius); + // Have the grid render itself + gridHighlight.Render(curPos, worldSpace.modelToWorld, gridMaterial, gridScale); + } } - Vector3 curPos = peltzerController.LastPositionModel; - Vector3 curPosWorld = worldSpace.ModelToWorld(curPos); - // Set the shader uniforms for center of grid and fade radius - gridMaterial.SetVector("_GridCenterWorld", new Vector4(curPosWorld.x, curPosWorld.y, curPosWorld.z)); - float worldSpaceRadius = 0.66f * worldSpace.scale - * Mathf.Floor(numVertsInRow / 2) * GridUtils.GRID_SIZE * gridScale; - gridMaterial.SetFloat("_GridRenderRadius", worldSpaceRadius); - // Have the grid render itself - gridHighlight.Render(curPos, worldSpace.modelToWorld, gridMaterial, gridScale); - } } - } } \ No newline at end of file diff --git a/Assets/Scripts/tools/utils/GridHighlighter.cs b/Assets/Scripts/tools/utils/GridHighlighter.cs index 9d3c7f7a..7f824d5d 100644 --- a/Assets/Scripts/tools/utils/GridHighlighter.cs +++ b/Assets/Scripts/tools/utils/GridHighlighter.cs @@ -17,51 +17,58 @@ using com.google.apps.peltzer.client.model.main; using UnityEngine; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// Constructs and renders a grid of points, snapped to the universal grid. - /// - public class GridHighlighter { - public static readonly int NO_SHADOWS_LAYER = 9; // NoShadowsLayer -- won't cast a shadow +namespace com.google.apps.peltzer.client.tools.utils +{ + /// + /// Constructs and renders a grid of points, snapped to the universal grid. + /// + public class GridHighlighter + { + public static readonly int NO_SHADOWS_LAYER = 9; // NoShadowsLayer -- won't cast a shadow - private Mesh gridMesh; + private Mesh gridMesh; - public void InitGrid(int numVertsPerRow, int gridSkip = 1) { - gridMesh = new Mesh(); - gridMesh.Clear(); + public void InitGrid(int numVertsPerRow, int gridSkip = 1) + { + gridMesh = new Mesh(); + gridMesh.Clear(); - List indexList = new List(); - List vertexList = new List(); - int x, y, z; - float curX, curY, curZ; - int index = 0; - for (x = 0; x < numVertsPerRow; x++) { - curX = (x - (Mathf.FloorToInt(numVertsPerRow / 2))) * GridUtils.GRID_SIZE; - for (y = 0; y < numVertsPerRow; y++) { - curY = (y - (Mathf.FloorToInt(numVertsPerRow / 2))) * GridUtils.GRID_SIZE; - for (z = 0; z < numVertsPerRow; z++) { - curZ = (z - (Mathf.FloorToInt(numVertsPerRow / 2))) * GridUtils.GRID_SIZE; - vertexList.Add(new Vector3(curX, curY, curZ)); - indexList.Add(index); - index++; - } - } - } - int[] indices = indexList.ToArray(); - Vector3[] vertices = vertexList.ToArray(); + List indexList = new List(); + List vertexList = new List(); + int x, y, z; + float curX, curY, curZ; + int index = 0; + for (x = 0; x < numVertsPerRow; x++) + { + curX = (x - (Mathf.FloorToInt(numVertsPerRow / 2))) * GridUtils.GRID_SIZE; + for (y = 0; y < numVertsPerRow; y++) + { + curY = (y - (Mathf.FloorToInt(numVertsPerRow / 2))) * GridUtils.GRID_SIZE; + for (z = 0; z < numVertsPerRow; z++) + { + curZ = (z - (Mathf.FloorToInt(numVertsPerRow / 2))) * GridUtils.GRID_SIZE; + vertexList.Add(new Vector3(curX, curY, curZ)); + indexList.Add(index); + index++; + } + } + } + int[] indices = indexList.ToArray(); + Vector3[] vertices = vertexList.ToArray(); - gridMesh.vertices = vertices; - // Since we're using a point geometry shader we need to set the mesh up to supply data as points. - gridMesh.SetIndices(indices, MeshTopology.Points, 0 /* submesh id */, true /* recalculate bounds */); - } + gridMesh.vertices = vertices; + // Since we're using a point geometry shader we need to set the mesh up to supply data as points. + gridMesh.SetIndices(indices, MeshTopology.Points, 0 /* submesh id */, true /* recalculate bounds */); + } - public void Render(Vector3 unsnappedGridCenter, Matrix4x4 objectToWorld, Material renderMat, int scale) { - // Scale to the correct grid granularity, then translate to the correct model position, then apply model to - // world transform. This should result in the correct model->world matrix for the grid's vertices. - Vector3 gridCenter = GridUtils.SnapToGrid(unsnappedGridCenter/scale) * scale; - Matrix4x4 gridTransform = objectToWorld * Matrix4x4.TRS(gridCenter, Quaternion.identity, Vector3.one) - * Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(scale, scale, scale)); - Graphics.DrawMesh(gridMesh, gridTransform, renderMat, NO_SHADOWS_LAYER); + public void Render(Vector3 unsnappedGridCenter, Matrix4x4 objectToWorld, Material renderMat, int scale) + { + // Scale to the correct grid granularity, then translate to the correct model position, then apply model to + // world transform. This should result in the correct model->world matrix for the grid's vertices. + Vector3 gridCenter = GridUtils.SnapToGrid(unsnappedGridCenter / scale) * scale; + Matrix4x4 gridTransform = objectToWorld * Matrix4x4.TRS(gridCenter, Quaternion.identity, Vector3.one) + * Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(scale, scale, scale)); + Graphics.DrawMesh(gridMesh, gridTransform, renderMat, NO_SHADOWS_LAYER); + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/tools/utils/GridUtils.cs b/Assets/Scripts/tools/utils/GridUtils.cs index 68e1d021..0c872632 100644 --- a/Assets/Scripts/tools/utils/GridUtils.cs +++ b/Assets/Scripts/tools/utils/GridUtils.cs @@ -16,197 +16,218 @@ using com.google.apps.peltzer.client.model.core; using UnityEngine; -namespace com.google.apps.peltzer.client.tools.utils { - public class GridUtils { - /// - /// The size of each grid unit. This is in Unity units, where 1.0f = 1 meter by default. - /// - public const float GRID_SIZE = 0.01f; - - /// - /// The size of each angle grid unit, in degrees. - /// - public static float ANGLE_GRID_SIZE = 45f; - - /// - /// The threshold for deviating from the snapped axis while center snapping. - /// - public static float CENTER_DEVIATION_THRESHOLD = 0.040f; - - /// - /// The threshold for center snapping at the start of a snap. - /// - public static float CENTER_SNAP_THRESHOLD = 0.075f; - - /// - /// The threshold for center snapping. - /// - public static float CENTER_THRESHOLD = 0.025f; - - /// - /// Finds which axis of a mesh is closest to a given vector. - /// - /// The vector being compared to the axis. - /// The rotation of the mesh. - /// The nearest axis as a normalized vector. - public static Vector3 FindNearestLocalMeshAxis(Vector3 vector, Quaternion meshRotation) { - Vector3 nearestUnitVector = FindNearestAxis(Quaternion.Inverse(meshRotation) * vector); - return meshRotation * nearestUnitVector; +namespace com.google.apps.peltzer.client.tools.utils +{ + public class GridUtils + { + /// + /// The size of each grid unit. This is in Unity units, where 1.0f = 1 meter by default. + /// + public const float GRID_SIZE = 0.01f; + + /// + /// The size of each angle grid unit, in degrees. + /// + public static float ANGLE_GRID_SIZE = 45f; + + /// + /// The threshold for deviating from the snapped axis while center snapping. + /// + public static float CENTER_DEVIATION_THRESHOLD = 0.040f; + + /// + /// The threshold for center snapping at the start of a snap. + /// + public static float CENTER_SNAP_THRESHOLD = 0.075f; + + /// + /// The threshold for center snapping. + /// + public static float CENTER_THRESHOLD = 0.025f; + + /// + /// Finds which axis of a mesh is closest to a given vector. + /// + /// The vector being compared to the axis. + /// The rotation of the mesh. + /// The nearest axis as a normalized vector. + public static Vector3 FindNearestLocalMeshAxis(Vector3 vector, Quaternion meshRotation) + { + Vector3 nearestUnitVector = FindNearestAxis(Quaternion.Inverse(meshRotation) * vector); + return meshRotation * nearestUnitVector; + } + + /// + /// Finds the nearest positive universal axis that a vector is closest to. + /// + /// The vector being used to find the nearest axis. + /// The nearest axis represented as a vector. + public static Vector3 FindNearestAxis(Vector3 vector) + { + float maxDimension = Mathf.Max(Mathf.Abs(vector.x), Mathf.Abs(vector.y), Mathf.Abs(vector.z)); + + if (maxDimension == Mathf.Abs(vector.x)) + { + return Vector3.right; + } + else if (maxDimension == Mathf.Abs(vector.y)) + { + return Vector3.up; + } + else + { + return Vector3.forward; + } + } + + /// + /// Snaps a given point centered at a point to the grid. + /// + /// The center of the bounds. + /// The bounds. + /// The new center of the snapped bounds. + public static Vector3 SnapToGrid(Vector3 center, Bounds bounds) + { + float x = SnapPointAndDeltaToGrid(center.x, bounds.extents.x); + float y = SnapPointAndDeltaToGrid(center.y, bounds.extents.y); + float z = SnapPointAndDeltaToGrid(center.z, bounds.extents.z); + return new Vector3(x, y, z); + } + + /// + /// Snaps a float representing one axis of a mesh's centerpoint to the grid, by pushing it + /// along either the positive or negative value of the cardinal axis and snapping to whichever + /// is already closest to the grid. + /// Effectively this snaps one axis of the mesh with the minimal amount of movement. + /// + /// One axis of the mesh's centerpoint. + /// The extent of the mesh's bounding box along the given axis. + /// The snapped value of meshAxis. + private static float SnapPointAndDeltaToGrid(float meshAxis, float boundsExtent) + { + float moveNegativePosition = meshAxis - boundsExtent; + float moveNegativeSnapped = SnapToGrid(moveNegativePosition); + float movePositivePosition = meshAxis + boundsExtent; + float movePositiveSnapped = SnapToGrid(movePositivePosition); + + if (Mathf.Abs(movePositiveSnapped - movePositivePosition) < + Mathf.Abs(moveNegativeSnapped - moveNegativePosition)) + return movePositiveSnapped - boundsExtent; + + return moveNegativeSnapped + boundsExtent; + } + + /// + /// Takes a position and projects it onto a line, rounds the resultant vector's length to grid-units, + /// and then returns the position moved along the vector. Optionally takes an arbitrary grid-size. + /// + /// The position to project and then snap onto the line. + /// The vector of the line being snapped to. + /// A reference point on the line being snapped to. + /// The grid-size to be used (optional). + public static Vector3 ProjectPointOntoLine(Vector3 point, Vector3 lineVector, + Vector3 lineOrigin, float gridSize = GRID_SIZE) + { + // Find the distance from the origin to the projectedToSnap position. + float projectedDistance = + Mathf.Cos(Vector3.Angle(point - lineOrigin, lineVector) * Mathf.Deg2Rad) * Vector3.Distance(lineOrigin, point); + // Round this distance to grid-units. + float snappedProjectedDistance = Mathf.Round(projectedDistance / gridSize) * gridSize; + // Find the position that is snappedProjectedDistance from the origin. + return lineOrigin + (lineVector.normalized * snappedProjectedDistance); + } + + /// + /// Snaps a given vector onto the nearest point in the grid. + /// + /// The Vector3 to snap. + /// + public static Vector3 SnapToGrid(Vector3 toSnap) + { + return new Vector3( + SnapToGrid(toSnap.x), + SnapToGrid(toSnap.y), + SnapToGrid(toSnap.z)); + } + + /// + /// Snaps a given vector onto the nearest point in the grid. + /// + /// The Vector3 to snap. + /// + public static Vector3 SnapToGrid(Vector3 toSnap, Vector3 offset) + { + return new Vector3( + SnapToGrid(toSnap.x, offset.x), + SnapToGrid(toSnap.y, offset.y), + SnapToGrid(toSnap.z, offset.z)); + } + + /// + /// Snaps a float representing a position on a cardinal axis to the grid. + /// + /// + /// + public static float SnapToGrid(float f) + { + return Mathf.Round(f / GRID_SIZE) * GRID_SIZE; + } + + /// + /// Snaps a float representing a position on a cardinal axis to the grid. + /// + /// The float to snap. + /// The distance to move the snap to be on the offset grid. + /// The snapped and offset float. + public static float SnapToGrid(float f, float offset) + { + return (Mathf.Round(f / GRID_SIZE) * GRID_SIZE) + offset; + } + + public static Quaternion SnapToNearest(Quaternion rot, Quaternion referenceRot, float angle) + { + Quaternion identityRot = Quaternion.Inverse(referenceRot) * rot; + identityRot = Quaternion.Euler(SnapAngleToGrid(identityRot.eulerAngles, angle)); + return referenceRot * identityRot; + } + + /// + /// Snaps Euler angles, as a Vector3, to the nearest angle on the grid. + /// + /// The grid-snapped rotation, as a Vector3. + public static Vector3 SnapAngleToGrid(Vector3 r) + { + return new Vector3(SnapAngleToGrid(r.x), SnapAngleToGrid(r.y), SnapAngleToGrid(r.z)); + } + + /// + /// Snaps Euler angles, as a Vector3, to the nearest angle on a given grid. + /// + /// The grid-snapped rotation, as a Vector3. + public static Vector3 SnapAngleToGrid(Vector3 r, float gridSize) + { + return new Vector3( + SnapAngleToGrid(r.x, gridSize), + SnapAngleToGrid(r.y, gridSize), + SnapAngleToGrid(r.z, gridSize)); + } + + /// + /// Snaps a single angle, as a float, to the nearest angle on the grid. + /// + /// The grid-snapped angle, as a float. + public static float SnapAngleToGrid(float f) + { + return SnapAngleToGrid(f, ANGLE_GRID_SIZE); + } + + /// + /// Snaps a single angle, as a float, to the nearest angle on a given grid. + /// + /// The grid-snapped angle, as a float. + public static float SnapAngleToGrid(float f, float gridSize) + { + return Mathf.Round(f / gridSize) * gridSize; + } } - - /// - /// Finds the nearest positive universal axis that a vector is closest to. - /// - /// The vector being used to find the nearest axis. - /// The nearest axis represented as a vector. - public static Vector3 FindNearestAxis(Vector3 vector) { - float maxDimension = Mathf.Max(Mathf.Abs(vector.x), Mathf.Abs(vector.y), Mathf.Abs(vector.z)); - - if (maxDimension == Mathf.Abs(vector.x)) { - return Vector3.right; - } else if (maxDimension == Mathf.Abs(vector.y)) { - return Vector3.up; - } else { - return Vector3.forward; - } - } - - /// - /// Snaps a given point centered at a point to the grid. - /// - /// The center of the bounds. - /// The bounds. - /// The new center of the snapped bounds. - public static Vector3 SnapToGrid(Vector3 center, Bounds bounds) { - float x = SnapPointAndDeltaToGrid(center.x, bounds.extents.x); - float y = SnapPointAndDeltaToGrid(center.y, bounds.extents.y); - float z = SnapPointAndDeltaToGrid(center.z, bounds.extents.z); - return new Vector3(x, y, z); - } - - /// - /// Snaps a float representing one axis of a mesh's centerpoint to the grid, by pushing it - /// along either the positive or negative value of the cardinal axis and snapping to whichever - /// is already closest to the grid. - /// Effectively this snaps one axis of the mesh with the minimal amount of movement. - /// - /// One axis of the mesh's centerpoint. - /// The extent of the mesh's bounding box along the given axis. - /// The snapped value of meshAxis. - private static float SnapPointAndDeltaToGrid(float meshAxis, float boundsExtent) { - float moveNegativePosition = meshAxis - boundsExtent; - float moveNegativeSnapped = SnapToGrid(moveNegativePosition); - float movePositivePosition = meshAxis + boundsExtent; - float movePositiveSnapped = SnapToGrid(movePositivePosition); - - if (Mathf.Abs(movePositiveSnapped - movePositivePosition) < - Mathf.Abs(moveNegativeSnapped - moveNegativePosition)) - return movePositiveSnapped - boundsExtent; - - return moveNegativeSnapped + boundsExtent; - } - - /// - /// Takes a position and projects it onto a line, rounds the resultant vector's length to grid-units, - /// and then returns the position moved along the vector. Optionally takes an arbitrary grid-size. - /// - /// The position to project and then snap onto the line. - /// The vector of the line being snapped to. - /// A reference point on the line being snapped to. - /// The grid-size to be used (optional). - public static Vector3 ProjectPointOntoLine(Vector3 point, Vector3 lineVector, - Vector3 lineOrigin, float gridSize = GRID_SIZE) { - // Find the distance from the origin to the projectedToSnap position. - float projectedDistance = - Mathf.Cos(Vector3.Angle(point - lineOrigin, lineVector) * Mathf.Deg2Rad) * Vector3.Distance(lineOrigin, point); - // Round this distance to grid-units. - float snappedProjectedDistance = Mathf.Round(projectedDistance / gridSize) * gridSize; - // Find the position that is snappedProjectedDistance from the origin. - return lineOrigin + (lineVector.normalized * snappedProjectedDistance); - } - - /// - /// Snaps a given vector onto the nearest point in the grid. - /// - /// The Vector3 to snap. - /// - public static Vector3 SnapToGrid(Vector3 toSnap) { - return new Vector3( - SnapToGrid(toSnap.x), - SnapToGrid(toSnap.y), - SnapToGrid(toSnap.z)); - } - - /// - /// Snaps a given vector onto the nearest point in the grid. - /// - /// The Vector3 to snap. - /// - public static Vector3 SnapToGrid(Vector3 toSnap, Vector3 offset) { - return new Vector3( - SnapToGrid(toSnap.x, offset.x), - SnapToGrid(toSnap.y, offset.y), - SnapToGrid(toSnap.z, offset.z)); - } - - /// - /// Snaps a float representing a position on a cardinal axis to the grid. - /// - /// - /// - public static float SnapToGrid(float f) { - return Mathf.Round(f / GRID_SIZE) * GRID_SIZE; - } - - /// - /// Snaps a float representing a position on a cardinal axis to the grid. - /// - /// The float to snap. - /// The distance to move the snap to be on the offset grid. - /// The snapped and offset float. - public static float SnapToGrid(float f, float offset) { - return (Mathf.Round(f / GRID_SIZE) * GRID_SIZE) + offset; - } - - public static Quaternion SnapToNearest(Quaternion rot, Quaternion referenceRot, float angle) { - Quaternion identityRot = Quaternion.Inverse(referenceRot) * rot; - identityRot = Quaternion.Euler(SnapAngleToGrid(identityRot.eulerAngles, angle)); - return referenceRot * identityRot; - } - - /// - /// Snaps Euler angles, as a Vector3, to the nearest angle on the grid. - /// - /// The grid-snapped rotation, as a Vector3. - public static Vector3 SnapAngleToGrid(Vector3 r) { - return new Vector3(SnapAngleToGrid(r.x), SnapAngleToGrid(r.y), SnapAngleToGrid(r.z)); - } - - /// - /// Snaps Euler angles, as a Vector3, to the nearest angle on a given grid. - /// - /// The grid-snapped rotation, as a Vector3. - public static Vector3 SnapAngleToGrid(Vector3 r, float gridSize) { - return new Vector3( - SnapAngleToGrid(r.x, gridSize), - SnapAngleToGrid(r.y, gridSize), - SnapAngleToGrid(r.z, gridSize)); - } - - /// - /// Snaps a single angle, as a float, to the nearest angle on the grid. - /// - /// The grid-snapped angle, as a float. - public static float SnapAngleToGrid(float f) { - return SnapAngleToGrid(f, ANGLE_GRID_SIZE); - } - - /// - /// Snaps a single angle, as a float, to the nearest angle on a given grid. - /// - /// The grid-snapped angle, as a float. - public static float SnapAngleToGrid(float f, float gridSize) { - return Mathf.Round(f / gridSize) * gridSize; - } - } } diff --git a/Assets/Scripts/tools/utils/HeldMeshes.cs b/Assets/Scripts/tools/utils/HeldMeshes.cs index 55e430c5..20826647 100644 --- a/Assets/Scripts/tools/utils/HeldMeshes.cs +++ b/Assets/Scripts/tools/utils/HeldMeshes.cs @@ -23,1217 +23,1357 @@ using TMPro; using UnityEngine; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// A collection of meshes that are tracking the position of the controller, for use in multiple tools who - /// can end up with a mesh as the 'toolhead'. - /// - public class HeldMeshes : MonoBehaviour { +namespace com.google.apps.peltzer.client.tools.utils +{ /// - /// A single held mesh. + /// A collection of meshes that are tracking the position of the controller, for use in multiple tools who + /// can end up with a mesh as the 'toolhead'. /// - public class HeldMesh { - // A clone of the MMesh being held. - public MMesh Mesh { private set; get; } - // A Unity GameObject preview of the MMesh being held. - public GameObject Preview { private set; get; } - - // The original locations of the vertices of the held mesh (so we don't have to use deltas when filling). - public Dictionary originalVertexLocations; - // The original size of the mesh, useful when filling. - public Vector3 originalSize; - - // The original offset of the MMesh. - public Vector3 originalOffset; - // The original rotation of the MMesh. - public Quaternion originalRotation = Quaternion.identity; - // The offset between the tip of the controller and the center of the preview. - public Vector3 grabOffset; - // The offset between the controller rotation and the rotation of the held mesh. - public Quaternion rotationOffsetSelf = Quaternion.identity; - - // Whether the mesh is a primitive, and should therefore do things like display size when dragging. - public bool isPrimitive; - - // The extents of the bounds around the mesh when dropped into the scene at the start of InsertType.FILL. - internal Vector3 fillStartExtents; - // The position of the transform for the mesh when dropped into the scene at the start of InsertType.FILL. - internal Vector3 fillStartPosition; - // The rotation of the primitive when inserted into the scene at the start of InsertType.FILL. - internal Quaternion fillStartRotation; - - /// - /// A mesh that is tracking the position of the controller in some way. - /// - /// - /// - /// - /// - /// - /// An optional size override, for when we have a better idea of the mesh's size than that - /// provided by its AABB. - public HeldMesh(MMesh originalMesh, Vector3 grabPoint, Quaternion rotationOffsetSelf, WorldSpace worldSpace, - MeshRepresentationCache meshComponentsCache, Dictionary sizes = null) { - originalVertexLocations = new Dictionary(originalMesh.vertexCount); - foreach (Vertex v in originalMesh.GetVertices()) { - originalVertexLocations.Add(v.id, v.loc); - } + public class HeldMeshes : MonoBehaviour + { + /// + /// A single held mesh. + /// + public class HeldMesh + { + // A clone of the MMesh being held. + public MMesh Mesh { private set; get; } + // A Unity GameObject preview of the MMesh being held. + public GameObject Preview { private set; get; } + + // The original locations of the vertices of the held mesh (so we don't have to use deltas when filling). + public Dictionary originalVertexLocations; + // The original size of the mesh, useful when filling. + public Vector3 originalSize; + + // The original offset of the MMesh. + public Vector3 originalOffset; + // The original rotation of the MMesh. + public Quaternion originalRotation = Quaternion.identity; + // The offset between the tip of the controller and the center of the preview. + public Vector3 grabOffset; + // The offset between the controller rotation and the rotation of the held mesh. + public Quaternion rotationOffsetSelf = Quaternion.identity; + + // Whether the mesh is a primitive, and should therefore do things like display size when dragging. + public bool isPrimitive; + + // The extents of the bounds around the mesh when dropped into the scene at the start of InsertType.FILL. + internal Vector3 fillStartExtents; + // The position of the transform for the mesh when dropped into the scene at the start of InsertType.FILL. + internal Vector3 fillStartPosition; + // The rotation of the primitive when inserted into the scene at the start of InsertType.FILL. + internal Quaternion fillStartRotation; + + /// + /// A mesh that is tracking the position of the controller in some way. + /// + /// + /// + /// + /// + /// + /// An optional size override, for when we have a better idea of the mesh's size than that + /// provided by its AABB. + public HeldMesh(MMesh originalMesh, Vector3 grabPoint, Quaternion rotationOffsetSelf, WorldSpace worldSpace, + MeshRepresentationCache meshComponentsCache, Dictionary sizes = null) + { + originalVertexLocations = new Dictionary(originalMesh.vertexCount); + foreach (Vertex v in originalMesh.GetVertices()) + { + originalVertexLocations.Add(v.id, v.loc); + } + + Mesh = originalMesh; + if (sizes != null) + { + originalSize = sizes[originalMesh.id]; + isPrimitive = true; + } + else + { + originalSize = originalMesh.bounds.size; + isPrimitive = false; + } + grabOffset = Mesh.offset - grabPoint; + this.rotationOffsetSelf = rotationOffsetSelf; + originalOffset = originalMesh.offset; + originalRotation = originalMesh.rotation; + if (meshComponentsCache != null) + { + Preview = meshComponentsCache.GeneratePreview(originalMesh); + } + else + { + Preview = MeshHelper.GameObjectFromMMesh(worldSpace, originalMesh); + } + Mesh.offset = Vector3.zero; + Mesh.rotation = Quaternion.identity; + } - Mesh = originalMesh; - if (sizes != null) { - originalSize = sizes[originalMesh.id]; - isPrimitive = true; - } - else { - originalSize = originalMesh.bounds.size; - isPrimitive = false; - } - grabOffset = Mesh.offset - grabPoint; - this.rotationOffsetSelf = rotationOffsetSelf; - originalOffset = originalMesh.offset; - originalRotation = originalMesh.rotation; - if (meshComponentsCache != null) { - Preview = meshComponentsCache.GeneratePreview(originalMesh); - } else { - Preview = MeshHelper.GameObjectFromMMesh(worldSpace, originalMesh); - } - Mesh.offset = Vector3.zero; - Mesh.rotation = Quaternion.identity; - } - - internal void SetUsePreviewMaterial(bool usePreview) { - MeshWithMaterialRenderer renderer = Preview.GetComponent(); - if (renderer) { - renderer.UsePreviewShader(usePreview); + internal void SetUsePreviewMaterial(bool usePreview) + { + MeshWithMaterialRenderer renderer = Preview.GetComponent(); + if (renderer) + { + renderer.UsePreviewShader(usePreview); + } + } + + internal void SetOriginalPositionsForFilling() + { + fillStartPosition = Preview.GetComponent().GetPositionInModelSpace(); + fillStartRotation = Preview.GetComponent().GetOrientationInModelSpace(); + fillStartExtents = Mesh.bounds.extents; + } } - } - internal void SetOriginalPositionsForFilling() { - fillStartPosition = Preview.GetComponent().GetPositionInModelSpace(); - fillStartRotation = Preview.GetComponent().GetOrientationInModelSpace(); - fillStartExtents = Mesh.bounds.extents; - } - } + // Length of time to do smoothing necessary for transitioning between modes. + private const float TRANSITION_DURATION = 0.1f; + + // The mode that we're operating in. (ie, are we inserting, filling, block move, snap move, or free move.) + private enum HoldMode { INSERT, FILL, BLOCK, SNAP, FREE }; + + /// + /// Defines how mesh placement is calculated. + /// + public enum PlacementMode + { + // The controller's position and orientation directly defines the placement/rotation of the held meshes. + // This is normally used when inserting new meshes, as they don't have an "original pose". + ABSOLUTE, + // The meshes are given in their original position/rotation. The meshes are translated and rotated by an offset + // defined by how much the controller moved/rotated since the operation started. This is normally used for + // moving existing meshes. + OFFSET + }; + + // If the held meshes are 'hidden' the preview will be inactive and they won't update in position. + private bool isHidden; + + // Meshes. + internal List heldMeshes; + // Snapping. + private bool isSnapping; + private SnapGrid snapGrid; + // A reference to the vertices that make up the preview face. + private List coplanarPreviewFaceVerticesAtOrigin; + // The face on the previewMesh being used for snapping. + private Face previewFace; + // The preview mesh for the game object the held mesh is being snapped to. + private GameObject snapTargetPreview; + private SnapSpace snapSpace; + private SnapDetector snapDetector; + + // Filling. + // Time limit to detect insertion of a simple shape before we start to insert a fill. + public static readonly float INSERT_TIME_LIMIT = 0.3f; + private float insertStartTime; + // First a user begins inserting... + public bool IsInserting; + // ...then if they keep holding the trigger, they're filling. + public bool IsFilling; + // The corner of the fill volume that is locked in world space. + private Vector3 lockedCorner; + // The quadrant that the diagonal, created by the controllers start (centered at the origin) and current + // position, is in. + private Vector3 currentQuadrant; + // The position of the controller at the start of insertion. + private Vector3 controllerStartPosition; + // The most-recent scale factor used when fill-scaling these HeldMeshes, or null. This is grid-aligned. + Vector3? lastFillingScaleFactor; + + // Controls sticky snapping for rotation. This causes the snapped rotation to only change when the user + // rotates the controller significantly away from the snapped rotation, to avoid constant orientation changes + // at the edge of two snap regions. + private StickyRotationSnapper stickyRotationSnapper; + + // Tools. + private PeltzerController peltzerController; + private WorldSpace worldSpace; + + // Moving. + // Keep track of where the mesh 'started' so we can track movement, avoiding floating-point error from + // continually adding deltas. + public Vector3 centroidStartPositionModel; + // The position of the controller at creation, in MODEL space. + public Vector3 controllerStartPositionModel; + // The rotation of the controller at creation, in MODEL space. + private Quaternion controllerStartRotationModel; + // The extents of the imaginary bounding box around all held meshes. + public Vector3 extentsOfHeldMeshes; + + // The slerp operation for the parent transform when operating in block mode. + private Slerpee blockSlerpee = null; + + // The time of the last hold mode transition - needed for determining when to stop slerping when transitioning + // out of snapping mode. + private float lastTransitionTime = 0f; + + // The most recent hold mode. + private HoldMode lastMode = HoldMode.FREE; + + private FaceSnapEffect currentFaceSnapEffect; + + private bool setupDone; + + private GameObject paletteRuler; + private GameObject ruler; + // X on front + private GameObject rulerXFrontBack; + private TextMeshPro XFrontText; + // Y on front + private GameObject rulerYFrontBack; + private TextMeshPro YFrontText; + // Z on top + private GameObject rulerZTopBottom; + private TextMeshPro ZTopText; + // X on top + private GameObject rulerXTopBottom; + private TextMeshPro XTopText; + // Y on side + private GameObject rulerYSide; + private TextMeshPro YSideText; + // Z on side + private GameObject rulerZSide; + private TextMeshPro ZSideText; + + private TextMeshPro paletteRulerText; + + // A set of cubes which are used to delineate what the ruler is measuring (useful for primitives other than cube. + private GameObject pXpYpZ; + private GameObject pXpYnZ; + private GameObject pXnYpZ; + private GameObject pXnYnZ; + private GameObject nXpYpZ; + private GameObject nXpYnZ; + private GameObject nXnYpZ; + private GameObject nXnYnZ; + + + // Note this method is copied below with slightly different behavior. + public void Setup(IEnumerable meshes, Vector3 controllerPositionAtCreation, + Quaternion controllerRotationAtCreation, PeltzerController peltzerController, WorldSpace worldSpace, + MeshRepresentationCache meshComponentsCache, PlacementMode placementMode = PlacementMode.OFFSET, + Dictionary oldPreviews = null, bool renderAsPreviewMeshes = false, + Dictionary sizes = null) + { + this.controllerStartPositionModel = (placementMode == PlacementMode.ABSOLUTE) ? + Vector3.zero : controllerPositionAtCreation; + this.controllerStartRotationModel = (placementMode == PlacementMode.ABSOLUTE) ? + Quaternion.identity : controllerRotationAtCreation; + this.peltzerController = peltzerController; + this.worldSpace = worldSpace; + heldMeshes = new List(meshes.Count()); + stickyRotationSnapper = new StickyRotationSnapper(controllerStartRotationModel); + + centroidStartPositionModel = Math3d.FindCentroid(meshes.Select(m => m.offset)); + Bounds overallBounds = new Bounds(); + overallBounds.center = centroidStartPositionModel; + + foreach (MMesh mesh in meshes) + { + overallBounds.Encapsulate(mesh.bounds); + Quaternion rotationOffsetSelf = Math3d.Normalize((placementMode == PlacementMode.ABSOLUTE) ? Quaternion.identity : + (Quaternion.Inverse(controllerRotationAtCreation) * mesh.rotation)); + HeldMesh oldHeldMesh; + if (oldPreviews != null && oldPreviews.TryGetValue(mesh, out oldHeldMesh)) + { + rotationOffsetSelf = oldHeldMesh.rotationOffsetSelf; + } + if (sizes != null) + { + heldMeshes.Add(new HeldMesh(mesh.Clone(), this.controllerStartPositionModel, + rotationOffsetSelf, + worldSpace, meshComponentsCache, sizes)); + } + else + { + heldMeshes.Add(new HeldMesh(mesh.Clone(), this.controllerStartPositionModel, + rotationOffsetSelf, + worldSpace, meshComponentsCache)); + } + } + if (renderAsPreviewMeshes) + { + for (int i = 0; i < heldMeshes.Count; i++) + { + heldMeshes[i].SetUsePreviewMaterial(true); + } + } - // Length of time to do smoothing necessary for transitioning between modes. - private const float TRANSITION_DURATION = 0.1f; + extentsOfHeldMeshes = overallBounds.extents; + + ruler = ObjectFinder.ObjectById("ID_Ruler3"); + rulerXFrontBack = ObjectFinder.ObjectById("ID_XText"); + rulerYFrontBack = ObjectFinder.ObjectById("ID_YText"); + rulerZTopBottom = ObjectFinder.ObjectById("ID_ZText"); + rulerXTopBottom = ObjectFinder.ObjectById("ID_XText2"); + rulerYSide = ObjectFinder.ObjectById("ID_YText2"); + rulerZSide = ObjectFinder.ObjectById("ID_ZText2"); + paletteRuler = ObjectFinder.ObjectById("ID_PaletteRuler"); + pXpYpZ = ObjectFinder.ObjectById("ID_pXpYpZ"); + pXpYnZ = ObjectFinder.ObjectById("ID_pXpYnZ"); + pXnYpZ = ObjectFinder.ObjectById("ID_pXnYpZ"); + pXnYnZ = ObjectFinder.ObjectById("ID_pXnYnZ"); + nXpYpZ = ObjectFinder.ObjectById("ID_nXpYpZ"); + nXpYnZ = ObjectFinder.ObjectById("ID_nXpYnZ"); + nXnYpZ = ObjectFinder.ObjectById("ID_nXnYpZ"); + nXnYnZ = ObjectFinder.ObjectById("ID_nXnYnZ"); + + paletteRulerText = paletteRuler.GetComponent(); + XFrontText = rulerXFrontBack.GetComponent(); + XTopText = rulerXTopBottom.GetComponent(); + YFrontText = rulerYFrontBack.GetComponent(); + YSideText = rulerYSide.GetComponent(); + ZTopText = rulerZTopBottom.GetComponent(); + ZSideText = rulerZSide.GetComponent(); + + if (snapDetector == null) + { + snapDetector = new SnapDetector(); + } + // Force an update to get everything in the right position. + UpdatePositions(); + setupDone = true; + } - // The mode that we're operating in. (ie, are we inserting, filling, block move, snap move, or free move.) - private enum HoldMode { INSERT, FILL, BLOCK, SNAP, FREE }; + // As above, but avoids cloning the mesh, and will not use the preview cache. + // Duplicated to avoid any performance overhead from e.g. cloning meshes into a temporary collection. + public void SetupWithNoCloneOrCache(IEnumerable meshes, Vector3 controllerPositionAtCreation, + PeltzerController peltzerController, WorldSpace worldSpace, + Dictionary oldPreviews = null) + { + this.controllerStartPositionModel = controllerPositionAtCreation; + this.controllerStartRotationModel = peltzerController.LastRotationModel; + this.peltzerController = peltzerController; + this.worldSpace = worldSpace; + heldMeshes = new List(); + stickyRotationSnapper = new StickyRotationSnapper(controllerStartRotationModel); + + centroidStartPositionModel = Math3d.FindCentroid(meshes.Select(m => m.offset)); + Bounds overallBounds = new Bounds(); + overallBounds.center = centroidStartPositionModel; + + foreach (MMesh mesh in meshes) + { + overallBounds.Encapsulate(mesh.bounds); + Quaternion rotationOffsetCentroid = Quaternion.Inverse(peltzerController.LastRotationModel); + Quaternion rotationOffsetSelf = Quaternion.Inverse(peltzerController.LastRotationModel) * mesh.rotation; + HeldMesh oldHeldMesh; + if (oldPreviews != null && oldPreviews.TryGetValue(mesh, out oldHeldMesh)) + { + rotationOffsetSelf = oldHeldMesh.rotationOffsetSelf; + } + + heldMeshes.Add(new HeldMesh(mesh, peltzerController.LastPositionModel, rotationOffsetSelf, + worldSpace, /* cache */ null)); + } - /// - /// Defines how mesh placement is calculated. - /// - public enum PlacementMode { - // The controller's position and orientation directly defines the placement/rotation of the held meshes. - // This is normally used when inserting new meshes, as they don't have an "original pose". - ABSOLUTE, - // The meshes are given in their original position/rotation. The meshes are translated and rotated by an offset - // defined by how much the controller moved/rotated since the operation started. This is normally used for - // moving existing meshes. - OFFSET - }; - - // If the held meshes are 'hidden' the preview will be inactive and they won't update in position. - private bool isHidden; - - // Meshes. - internal List heldMeshes; - // Snapping. - private bool isSnapping; - private SnapGrid snapGrid; - // A reference to the vertices that make up the preview face. - private List coplanarPreviewFaceVerticesAtOrigin; - // The face on the previewMesh being used for snapping. - private Face previewFace; - // The preview mesh for the game object the held mesh is being snapped to. - private GameObject snapTargetPreview; - private SnapSpace snapSpace; - private SnapDetector snapDetector; - - // Filling. - // Time limit to detect insertion of a simple shape before we start to insert a fill. - public static readonly float INSERT_TIME_LIMIT = 0.3f; - private float insertStartTime; - // First a user begins inserting... - public bool IsInserting; - // ...then if they keep holding the trigger, they're filling. - public bool IsFilling; - // The corner of the fill volume that is locked in world space. - private Vector3 lockedCorner; - // The quadrant that the diagonal, created by the controllers start (centered at the origin) and current - // position, is in. - private Vector3 currentQuadrant; - // The position of the controller at the start of insertion. - private Vector3 controllerStartPosition; - // The most-recent scale factor used when fill-scaling these HeldMeshes, or null. This is grid-aligned. - Vector3? lastFillingScaleFactor; - - // Controls sticky snapping for rotation. This causes the snapped rotation to only change when the user - // rotates the controller significantly away from the snapped rotation, to avoid constant orientation changes - // at the edge of two snap regions. - private StickyRotationSnapper stickyRotationSnapper; - - // Tools. - private PeltzerController peltzerController; - private WorldSpace worldSpace; - - // Moving. - // Keep track of where the mesh 'started' so we can track movement, avoiding floating-point error from - // continually adding deltas. - public Vector3 centroidStartPositionModel; - // The position of the controller at creation, in MODEL space. - public Vector3 controllerStartPositionModel; - // The rotation of the controller at creation, in MODEL space. - private Quaternion controllerStartRotationModel; - // The extents of the imaginary bounding box around all held meshes. - public Vector3 extentsOfHeldMeshes; - - // The slerp operation for the parent transform when operating in block mode. - private Slerpee blockSlerpee = null; - - // The time of the last hold mode transition - needed for determining when to stop slerping when transitioning - // out of snapping mode. - private float lastTransitionTime = 0f; - - // The most recent hold mode. - private HoldMode lastMode = HoldMode.FREE; - - private FaceSnapEffect currentFaceSnapEffect; - - private bool setupDone; - - private GameObject paletteRuler; - private GameObject ruler; - // X on front - private GameObject rulerXFrontBack; - private TextMeshPro XFrontText; - // Y on front - private GameObject rulerYFrontBack; - private TextMeshPro YFrontText; - // Z on top - private GameObject rulerZTopBottom; - private TextMeshPro ZTopText; - // X on top - private GameObject rulerXTopBottom; - private TextMeshPro XTopText; - // Y on side - private GameObject rulerYSide; - private TextMeshPro YSideText; - // Z on side - private GameObject rulerZSide; - private TextMeshPro ZSideText; - - private TextMeshPro paletteRulerText; - - // A set of cubes which are used to delineate what the ruler is measuring (useful for primitives other than cube. - private GameObject pXpYpZ; - private GameObject pXpYnZ; - private GameObject pXnYpZ; - private GameObject pXnYnZ; - private GameObject nXpYpZ; - private GameObject nXpYnZ; - private GameObject nXnYpZ; - private GameObject nXnYnZ; - - - // Note this method is copied below with slightly different behavior. - public void Setup(IEnumerable meshes, Vector3 controllerPositionAtCreation, - Quaternion controllerRotationAtCreation, PeltzerController peltzerController, WorldSpace worldSpace, - MeshRepresentationCache meshComponentsCache, PlacementMode placementMode = PlacementMode.OFFSET, - Dictionary oldPreviews = null, bool renderAsPreviewMeshes = false, - Dictionary sizes = null) { - this.controllerStartPositionModel = (placementMode == PlacementMode.ABSOLUTE) ? - Vector3.zero : controllerPositionAtCreation; - this.controllerStartRotationModel = (placementMode == PlacementMode.ABSOLUTE) ? - Quaternion.identity : controllerRotationAtCreation; - this.peltzerController = peltzerController; - this.worldSpace = worldSpace; - heldMeshes = new List(meshes.Count()); - stickyRotationSnapper = new StickyRotationSnapper(controllerStartRotationModel); - - centroidStartPositionModel = Math3d.FindCentroid(meshes.Select(m => m.offset)); - Bounds overallBounds = new Bounds(); - overallBounds.center = centroidStartPositionModel; - - foreach (MMesh mesh in meshes) { - overallBounds.Encapsulate(mesh.bounds); - Quaternion rotationOffsetSelf = Math3d.Normalize((placementMode == PlacementMode.ABSOLUTE) ? Quaternion.identity : - (Quaternion.Inverse(controllerRotationAtCreation) * mesh.rotation)); - HeldMesh oldHeldMesh; - if (oldPreviews != null && oldPreviews.TryGetValue(mesh, out oldHeldMesh)) { - rotationOffsetSelf = oldHeldMesh.rotationOffsetSelf; - } - if (sizes != null) { - heldMeshes.Add(new HeldMesh(mesh.Clone(), this.controllerStartPositionModel, - rotationOffsetSelf, - worldSpace, meshComponentsCache, sizes)); - } - else { - heldMeshes.Add(new HeldMesh(mesh.Clone(), this.controllerStartPositionModel, - rotationOffsetSelf, - worldSpace, meshComponentsCache)); - } - } - if (renderAsPreviewMeshes) { - for (int i = 0; i < heldMeshes.Count; i++) { - heldMeshes[i].SetUsePreviewMaterial(true); - } - } - - extentsOfHeldMeshes = overallBounds.extents; - - ruler = ObjectFinder.ObjectById("ID_Ruler3"); - rulerXFrontBack = ObjectFinder.ObjectById("ID_XText"); - rulerYFrontBack = ObjectFinder.ObjectById("ID_YText"); - rulerZTopBottom = ObjectFinder.ObjectById("ID_ZText"); - rulerXTopBottom = ObjectFinder.ObjectById("ID_XText2"); - rulerYSide = ObjectFinder.ObjectById("ID_YText2"); - rulerZSide = ObjectFinder.ObjectById("ID_ZText2"); - paletteRuler = ObjectFinder.ObjectById("ID_PaletteRuler"); - pXpYpZ = ObjectFinder.ObjectById("ID_pXpYpZ"); - pXpYnZ = ObjectFinder.ObjectById("ID_pXpYnZ"); - pXnYpZ = ObjectFinder.ObjectById("ID_pXnYpZ"); - pXnYnZ = ObjectFinder.ObjectById("ID_pXnYnZ"); - nXpYpZ = ObjectFinder.ObjectById("ID_nXpYpZ"); - nXpYnZ = ObjectFinder.ObjectById("ID_nXpYnZ"); - nXnYpZ = ObjectFinder.ObjectById("ID_nXnYpZ"); - nXnYnZ = ObjectFinder.ObjectById("ID_nXnYnZ"); - - paletteRulerText = paletteRuler.GetComponent(); - XFrontText = rulerXFrontBack.GetComponent(); - XTopText = rulerXTopBottom.GetComponent(); - YFrontText = rulerYFrontBack.GetComponent(); - YSideText = rulerYSide.GetComponent(); - ZTopText = rulerZTopBottom.GetComponent(); - ZSideText = rulerZSide.GetComponent(); - - if (snapDetector == null) { - snapDetector = new SnapDetector(); - } - // Force an update to get everything in the right position. - UpdatePositions(); - setupDone = true; - } + extentsOfHeldMeshes = overallBounds.extents; - // As above, but avoids cloning the mesh, and will not use the preview cache. - // Duplicated to avoid any performance overhead from e.g. cloning meshes into a temporary collection. - public void SetupWithNoCloneOrCache(IEnumerable meshes, Vector3 controllerPositionAtCreation, - PeltzerController peltzerController, WorldSpace worldSpace, - Dictionary oldPreviews = null) { - this.controllerStartPositionModel = controllerPositionAtCreation; - this.controllerStartRotationModel = peltzerController.LastRotationModel; - this.peltzerController = peltzerController; - this.worldSpace = worldSpace; - heldMeshes = new List(); - stickyRotationSnapper = new StickyRotationSnapper(controllerStartRotationModel); - - centroidStartPositionModel = Math3d.FindCentroid(meshes.Select(m => m.offset)); - Bounds overallBounds = new Bounds(); - overallBounds.center = centroidStartPositionModel; - - foreach (MMesh mesh in meshes) { - overallBounds.Encapsulate(mesh.bounds); - Quaternion rotationOffsetCentroid = Quaternion.Inverse(peltzerController.LastRotationModel); - Quaternion rotationOffsetSelf = Quaternion.Inverse(peltzerController.LastRotationModel) * mesh.rotation; - HeldMesh oldHeldMesh; - if (oldPreviews != null && oldPreviews.TryGetValue(mesh, out oldHeldMesh)) { - rotationOffsetSelf = oldHeldMesh.rotationOffsetSelf; + // Force an update to get everything in the right position. + UpdatePositions(); } - heldMeshes.Add(new HeldMesh(mesh, peltzerController.LastPositionModel, rotationOffsetSelf, - worldSpace, /* cache */ null)); - } - - extentsOfHeldMeshes = overallBounds.extents; + /// + /// Detects what would snap at this given moment for the heldMeshes. + /// + public void DetectSnap() + { + // TODO (bug): Snap multiple meshes. + if (heldMeshes.Count > 1 || isSnapping) return; + MMesh mesh = heldMeshes[0].Mesh; + GameObject preview = heldMeshes[0].Preview; + MeshWithMaterialRenderer renderMesh = preview.transform.GetComponent(); + Vector3 previewMeshOffset = renderMesh.GetPositionInModelSpace(); + Quaternion previewMeshRotation = renderMesh.GetOrientationInModelSpace(); + + snapDetector.DetectSnap(mesh, previewMeshOffset, previewMeshRotation); + } - // Force an update to get everything in the right position. - UpdatePositions(); - } + // Hide the rope guides. + public void HideSnapGuides() + { + if (Features.useContinuousSnapDetection) + { + if (snapDetector != null) + { + snapDetector.HideGuides(); + } + + if (snapSpace != null) + { + snapSpace.StopSnap(); + } + } + } - /// - /// Detects what would snap at this given moment for the heldMeshes. - /// - public void DetectSnap() { - // TODO (bug): Snap multiple meshes. - if (heldMeshes.Count > 1 || isSnapping) return; - MMesh mesh = heldMeshes[0].Mesh; - GameObject preview = heldMeshes[0].Preview; - MeshWithMaterialRenderer renderMesh = preview.transform.GetComponent(); - Vector3 previewMeshOffset = renderMesh.GetPositionInModelSpace(); - Quaternion previewMeshRotation = renderMesh.GetOrientationInModelSpace(); - - snapDetector.DetectSnap(mesh, previewMeshOffset, previewMeshRotation); - } + /// + /// To the user there is only one way to engage snapping. However, there are actually multiple snap modes. + /// Currently there is UNIVERSAL and MESH snapping. MESH snapping takes priority over UNIVERSAL snapping. + /// If there is a mesh in the selector we will use this mesh as a reference to the new grid we should be + /// snapping to. If there is no mesh nearby we will snap to the universal grid. + /// + public void StartSnapping(Model model, SpatialIndex spatialIndex) + { + if (isSnapping || !PeltzerMain.Instance.restrictionManager.snappingAllowed) + return; + + // We won't even try and snap multiple selected meshes, behaviour is ill-defined and our code below won't + // support it, bug + // We can snap multiple meshes with the snapGrid implementation. + if (heldMeshes.Count > 1) + return; + + isSnapping = true; + + if (Features.useContinuousSnapDetection) + { + MMesh mesh = heldMeshes[0].Mesh; + GameObject preview = heldMeshes[0].Preview; + MeshWithMaterialRenderer renderMesh = preview.transform.GetComponent(); + Vector3 previewMeshOffset = renderMesh.GetPositionInModelSpace(); + Quaternion previewMeshRotation = renderMesh.GetOrientationInModelSpace(); + + snapSpace = snapDetector.ExecuteSnap(mesh, previewMeshOffset, previewMeshRotation); + PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.alignSound); + PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); + } + else + { + MMesh mesh = heldMeshes[0].Mesh; + GameObject preview = heldMeshes[0].Preview; + MeshWithMaterialRenderer renderMesh = preview.transform.GetComponent(); + Vector3 previewMeshOffset = renderMesh.GetPositionInModelSpace(); + Quaternion previewMeshRotation = renderMesh.GetOrientationInModelSpace(); + Quaternion finalVolumePreviewMeshRotation = Quaternion.identity; + int snapTargetId; + snapGrid = new SnapGrid( + mesh, + previewMeshOffset, + previewMeshRotation, + model, + spatialIndex, + worldSpace, + peltzerController.mode == ControllerMode.subtract, + out finalVolumePreviewMeshRotation, + out previewFace, + out coplanarPreviewFaceVerticesAtOrigin, + out snapTargetId); + if (snapGrid.snapType == SnapGrid.SnapType.FACE) + { + if (snapTargetId != -1) + { + currentFaceSnapEffect = new FaceSnapEffect(snapTargetId); + UXEffectManager.GetEffectManager().StartEffect(currentFaceSnapEffect); + } + } + renderMesh.SetOrientationModelSpace(finalVolumePreviewMeshRotation, /* smooth */ true); + if (snapGrid.snapType == SnapGrid.SnapType.UNIVERSAL) + { + PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.alignSound); + } + else + { + PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.snapSound); + } + } + } - // Hide the rope guides. - public void HideSnapGuides() { - if (Features.useContinuousSnapDetection) { - if (snapDetector != null) { - snapDetector.HideGuides(); + /// + /// Exit snapping mode and reset the grid, snapType and the selector. + /// + public void StopSnapping() + { + if (currentFaceSnapEffect != null) + { + currentFaceSnapEffect.Finish(); + currentFaceSnapEffect = null; + } + if (!isSnapping) + return; + + // Reset snapping. + isSnapping = false; + if (Features.useContinuousSnapDetection) + { + HideSnapGuides(); + snapSpace = null; + } + else + { + snapGrid.snapType = SnapGrid.SnapType.NONE; + } } - if (snapSpace != null) { - snapSpace.StopSnap(); + public void StartInserting(Vector3 controllerStartPosition) + { + HideSnapGuides(); + this.controllerStartPosition = controllerStartPosition; + insertStartTime = Time.time; + IsInserting = true; } - } - } - /// - /// To the user there is only one way to engage snapping. However, there are actually multiple snap modes. - /// Currently there is UNIVERSAL and MESH snapping. MESH snapping takes priority over UNIVERSAL snapping. - /// If there is a mesh in the selector we will use this mesh as a reference to the new grid we should be - /// snapping to. If there is no mesh nearby we will snap to the universal grid. - /// - public void StartSnapping(Model model, SpatialIndex spatialIndex) { - if (isSnapping || !PeltzerMain.Instance.restrictionManager.snappingAllowed) - return; - - // We won't even try and snap multiple selected meshes, behaviour is ill-defined and our code below won't - // support it, bug - // We can snap multiple meshes with the snapGrid implementation. - if (heldMeshes.Count > 1) - return; - - isSnapping = true; - - if (Features.useContinuousSnapDetection) { - MMesh mesh = heldMeshes[0].Mesh; - GameObject preview = heldMeshes[0].Preview; - MeshWithMaterialRenderer renderMesh = preview.transform.GetComponent(); - Vector3 previewMeshOffset = renderMesh.GetPositionInModelSpace(); - Quaternion previewMeshRotation = renderMesh.GetOrientationInModelSpace(); - - snapSpace = snapDetector.ExecuteSnap(mesh, previewMeshOffset, previewMeshRotation); - PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.alignSound); - PeltzerMain.Instance.peltzerController.TriggerHapticFeedback(); - } else { - MMesh mesh = heldMeshes[0].Mesh; - GameObject preview = heldMeshes[0].Preview; - MeshWithMaterialRenderer renderMesh = preview.transform.GetComponent(); - Vector3 previewMeshOffset = renderMesh.GetPositionInModelSpace(); - Quaternion previewMeshRotation = renderMesh.GetOrientationInModelSpace(); - Quaternion finalVolumePreviewMeshRotation = Quaternion.identity; - int snapTargetId; - snapGrid = new SnapGrid( - mesh, - previewMeshOffset, - previewMeshRotation, - model, - spatialIndex, - worldSpace, - peltzerController.mode == ControllerMode.subtract, - out finalVolumePreviewMeshRotation, - out previewFace, - out coplanarPreviewFaceVerticesAtOrigin, - out snapTargetId); - if (snapGrid.snapType == SnapGrid.SnapType.FACE) { - if (snapTargetId != -1) { - currentFaceSnapEffect = new FaceSnapEffect(snapTargetId); - UXEffectManager.GetEffectManager().StartEffect(currentFaceSnapEffect); - } + public void FinishInserting() + { + IsInserting = false; + IsFilling = false; + lastFillingScaleFactor = null; + snapSpace = null; } - renderMesh.SetOrientationModelSpace(finalVolumePreviewMeshRotation, /* smooth */ true); - if (snapGrid.snapType == SnapGrid.SnapType.UNIVERSAL) { - PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.alignSound); - } else { - PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.snapSound); + + /// + /// Returns a dictionary that associates held meshes and their previews. + /// + /// A dictionary where the keys are the held meshes and the corresponding value is + /// the held mesh's preview object. + public Dictionary GetHeldMeshesAndPreviews() + { + Dictionary result = new Dictionary(); + foreach (HeldMesh heldMesh in heldMeshes) + { + result[heldMesh.Mesh] = heldMesh.Preview; + } + return result; } - } - } - /// - /// Exit snapping mode and reset the grid, snapType and the selector. - /// - public void StopSnapping() { - if (currentFaceSnapEffect != null) { - currentFaceSnapEffect.Finish(); - currentFaceSnapEffect = null; - } - if (!isSnapping) - return; - - // Reset snapping. - isSnapping = false; - if (Features.useContinuousSnapDetection) { - HideSnapGuides(); - snapSpace = null; - } else { - snapGrid.snapType = SnapGrid.SnapType.NONE; - } - } + /// + /// Sets all the variables needed for filling a volume. + /// + private void StartFillingVolume() + { + // If volume filling is not allowed at this point in time, return. + if (!PeltzerMain.Instance.restrictionManager.volumeFillingAllowed) + { + return; + } - public void StartInserting(Vector3 controllerStartPosition) { - HideSnapGuides(); - this.controllerStartPosition = controllerStartPosition; - insertStartTime = Time.time; - IsInserting = true; - } + IsFilling = true; + IsInserting = false; - public void FinishInserting() { - IsInserting = false; - IsFilling = false; - lastFillingScaleFactor = null; - snapSpace = null; - } + currentQuadrant = Vector3.zero; + foreach (HeldMesh heldMesh in heldMeshes) + { + heldMesh.SetOriginalPositionsForFilling(); + } + } - /// - /// Returns a dictionary that associates held meshes and their previews. - /// - /// A dictionary where the keys are the held meshes and the corresponding value is - /// the held mesh's preview object. - public Dictionary GetHeldMeshesAndPreviews() { - Dictionary result = new Dictionary(); - foreach (HeldMesh heldMesh in heldMeshes) { - result[heldMesh.Mesh] = heldMesh.Preview; - } - return result; - } + /// + /// Update the position of the mesh whilst in 'block mode'. + /// In block mode, we snap the bounding box of the entire selection. + /// + private void UpdatePositionBlockMode() + { + Vector3 grabOffset = centroidStartPositionModel - controllerStartPositionModel; + Vector3 controllerDelta = peltzerController.LastPositionModel - controllerStartPositionModel; + Vector3 newCentroid = centroidStartPositionModel + controllerDelta; + newCentroid = Math3d.RotatePointAroundPivot(newCentroid, peltzerController.LastPositionModel + grabOffset, + peltzerController.LastRotationModel); + Vector3 centroidDelta = newCentroid - centroidStartPositionModel; + + // Compute how much the controller has rotated in MODEL space. Note that we don't have to add any special + // behavior to handle world rotation, because rotating the world is EQUIVALENT to rotating the controller + // in the opposite way in model space. So, to us, there's only rotation in model space. + Quaternion unsnappedRotation = + peltzerController.LastRotationModel * Quaternion.Inverse(controllerStartRotationModel); + Quaternion baseSnappedRotation = stickyRotationSnapper.UpdateRotation(unsnappedRotation); + + // Initialize slerp for parent transform if it hasn't already been initialized. + if (blockSlerpee == null) + { + blockSlerpee = new Slerpee(unsnappedRotation); + } - /// - /// Sets all the variables needed for filling a volume. - /// - private void StartFillingVolume() { - // If volume filling is not allowed at this point in time, return. - if (!PeltzerMain.Instance.restrictionManager.volumeFillingAllowed) { - return; - } - - IsFilling = true; - IsInserting = false; - - currentQuadrant = Vector3.zero; - foreach (HeldMesh heldMesh in heldMeshes) { - heldMesh.SetOriginalPositionsForFilling(); - } - } + // If we're transitioning into block mode, don't slerp the parent transform and instead slerp the child + // transforms to smooth into block mode. Once that completes, slerp the parent transform and not the child + // transforms to ensure that grouped meshes rotate correctly with each other. + Quaternion parentSlerpedSnappedRotation; + bool inTransitionToBlockMode = TRANSITION_DURATION >= (Time.time - lastTransitionTime); + if (inTransitionToBlockMode) + { + // Still transitioning in, instantly update parent. Individual meshes will slerp during this period. + parentSlerpedSnappedRotation = blockSlerpee.UpdateOrientationInstantly(baseSnappedRotation); + } + else + { + // Done transitioning, start slerping parent, individual meshes will update immediately based on + // parent slerp. + parentSlerpedSnappedRotation = blockSlerpee.StartOrUpdateSlerp(baseSnappedRotation); + } - /// - /// Update the position of the mesh whilst in 'block mode'. - /// In block mode, we snap the bounding box of the entire selection. - /// - private void UpdatePositionBlockMode() { - Vector3 grabOffset = centroidStartPositionModel - controllerStartPositionModel; - Vector3 controllerDelta = peltzerController.LastPositionModel - controllerStartPositionModel; - Vector3 newCentroid = centroidStartPositionModel + controllerDelta; - newCentroid = Math3d.RotatePointAroundPivot(newCentroid, peltzerController.LastPositionModel + grabOffset, - peltzerController.LastRotationModel); - Vector3 centroidDelta = newCentroid - centroidStartPositionModel; - - // Compute how much the controller has rotated in MODEL space. Note that we don't have to add any special - // behavior to handle world rotation, because rotating the world is EQUIVALENT to rotating the controller - // in the opposite way in model space. So, to us, there's only rotation in model space. - Quaternion unsnappedRotation = - peltzerController.LastRotationModel * Quaternion.Inverse(controllerStartRotationModel); - Quaternion baseSnappedRotation = stickyRotationSnapper.UpdateRotation(unsnappedRotation); - - // Initialize slerp for parent transform if it hasn't already been initialized. - if (blockSlerpee == null) { - blockSlerpee = new Slerpee(unsnappedRotation); - } - - // If we're transitioning into block mode, don't slerp the parent transform and instead slerp the child - // transforms to smooth into block mode. Once that completes, slerp the parent transform and not the child - // transforms to ensure that grouped meshes rotate correctly with each other. - Quaternion parentSlerpedSnappedRotation; - bool inTransitionToBlockMode = TRANSITION_DURATION >= (Time.time - lastTransitionTime); - if (inTransitionToBlockMode) { - // Still transitioning in, instantly update parent. Individual meshes will slerp during this period. - parentSlerpedSnappedRotation = blockSlerpee.UpdateOrientationInstantly(baseSnappedRotation); - } else { - // Done transitioning, start slerping parent, individual meshes will update immediately based on - // parent slerp. - parentSlerpedSnappedRotation = blockSlerpee.StartOrUpdateSlerp(baseSnappedRotation); - } - - Bounds tempBounds = new Bounds(); - foreach (HeldMesh heldMesh in heldMeshes) { - - // There's still a small misalignment that is causing groups of meshes to not rotate in rigid formation. - // As it's only visually noticeable when slerping is slowed down 10x, fixing this isn't urgent, but be aware - // that there's currently a small amount of error here. The final snapped positions are correct. - Vector3 smoothedPosition = Math3d.RotatePointAroundPivot(heldMesh.originalOffset + centroidDelta, - peltzerController.LastPositionModel, parentSlerpedSnappedRotation); - Vector3 snappedPosition = Math3d.RotatePointAroundPivot(heldMesh.originalOffset + centroidDelta, - peltzerController.LastPositionModel, baseSnappedRotation); - - MeshWithMaterialRenderer renderMesh = heldMesh.Preview.GetComponent(); - - // Linear position smoothing makes groups of objects rotate incorrectly, but is needed to transition in and - // out of snapping. Since we don't snap groups, only smooth when we're holding a single mesh. - if (heldMeshes.Count == 1) { - // Note: we don't use smoothing when doing Setup (before setupDone == true) because in ABSOLUTE - // positioning mode, the first update (driven by Setup()) will move the meshes from the origin to the - // correct position. - renderMesh.SetPositionModelSpace(snappedPosition, /* smooth */ setupDone); - } else { - renderMesh.SetPositionWithDisplayOverrideModelSpace(snappedPosition, smoothedPosition); - } - // Only smooth orientations while we're transitioning into block mode - otherwise we want to use the parent - // transform smoothing. Doing normalization because in testing floating point drift caused issues here. - // Also, don't smooth if we are in the process of doing setup (setupDone == false), as we don't want - // the mesh to animate from the identity rotation to its initial rotation. - renderMesh.SetOrientationWithDisplayOverrideModelSpace( - Math3d.Normalize(baseSnappedRotation * heldMesh.originalRotation), - Math3d.Normalize(parentSlerpedSnappedRotation * heldMesh.originalRotation), - /* smooth */ setupDone && inTransitionToBlockMode); - - // Encapsulate the mesh bounds. - Bounds meshBounds = heldMesh.Mesh.bounds; - // The mesh offset may be wrong, so we explicitly set the bounds center here. - // First, compute the difference between the preview's position and the mesh's position. - Vector3 delta = renderMesh.positionModelSpace - heldMesh.Mesh.offset; - // Apply that delta to the bounding box center to obtain the current bounding box. - // Remember that bounding boxes are represented in model space, not in mesh space. - meshBounds.center = heldMesh.Mesh.bounds.center + delta; - if (tempBounds.extents == Vector3.zero) { - tempBounds = meshBounds; - } else { - tempBounds.Encapsulate(meshBounds); + Bounds tempBounds = new Bounds(); + foreach (HeldMesh heldMesh in heldMeshes) + { + + // There's still a small misalignment that is causing groups of meshes to not rotate in rigid formation. + // As it's only visually noticeable when slerping is slowed down 10x, fixing this isn't urgent, but be aware + // that there's currently a small amount of error here. The final snapped positions are correct. + Vector3 smoothedPosition = Math3d.RotatePointAroundPivot(heldMesh.originalOffset + centroidDelta, + peltzerController.LastPositionModel, parentSlerpedSnappedRotation); + Vector3 snappedPosition = Math3d.RotatePointAroundPivot(heldMesh.originalOffset + centroidDelta, + peltzerController.LastPositionModel, baseSnappedRotation); + + MeshWithMaterialRenderer renderMesh = heldMesh.Preview.GetComponent(); + + // Linear position smoothing makes groups of objects rotate incorrectly, but is needed to transition in and + // out of snapping. Since we don't snap groups, only smooth when we're holding a single mesh. + if (heldMeshes.Count == 1) + { + // Note: we don't use smoothing when doing Setup (before setupDone == true) because in ABSOLUTE + // positioning mode, the first update (driven by Setup()) will move the meshes from the origin to the + // correct position. + renderMesh.SetPositionModelSpace(snappedPosition, /* smooth */ setupDone); + } + else + { + renderMesh.SetPositionWithDisplayOverrideModelSpace(snappedPosition, smoothedPosition); + } + // Only smooth orientations while we're transitioning into block mode - otherwise we want to use the parent + // transform smoothing. Doing normalization because in testing floating point drift caused issues here. + // Also, don't smooth if we are in the process of doing setup (setupDone == false), as we don't want + // the mesh to animate from the identity rotation to its initial rotation. + renderMesh.SetOrientationWithDisplayOverrideModelSpace( + Math3d.Normalize(baseSnappedRotation * heldMesh.originalRotation), + Math3d.Normalize(parentSlerpedSnappedRotation * heldMesh.originalRotation), + /* smooth */ setupDone && inTransitionToBlockMode); + + // Encapsulate the mesh bounds. + Bounds meshBounds = heldMesh.Mesh.bounds; + // The mesh offset may be wrong, so we explicitly set the bounds center here. + // First, compute the difference between the preview's position and the mesh's position. + Vector3 delta = renderMesh.positionModelSpace - heldMesh.Mesh.offset; + // Apply that delta to the bounding box center to obtain the current bounding box. + // Remember that bounding boxes are represented in model space, not in mesh space. + meshBounds.center = heldMesh.Mesh.bounds.center + delta; + if (tempBounds.extents == Vector3.zero) + { + tempBounds = meshBounds; + } + else + { + tempBounds.Encapsulate(meshBounds); + } + } + + // Find how far the bounding box needs to be moved to be snapped to the grid. + Vector3 snappedDelta = GridUtils.SnapToGrid(tempBounds.center, tempBounds) - tempBounds.center; + + // Apply the snappedDelta to each mesh. + foreach (HeldMesh heldMesh in heldMeshes) + { + MeshWithMaterialRenderer renderer = heldMesh.Preview.GetComponent(); + renderer.SetPositionModelSpace( + renderer.GetPositionInModelSpace() + snappedDelta, + /* smooth */ true); + } } - } - - // Find how far the bounding box needs to be moved to be snapped to the grid. - Vector3 snappedDelta = GridUtils.SnapToGrid(tempBounds.center, tempBounds) - tempBounds.center; - - // Apply the snappedDelta to each mesh. - foreach (HeldMesh heldMesh in heldMeshes) { - MeshWithMaterialRenderer renderer = heldMesh.Preview.GetComponent(); - renderer.SetPositionModelSpace( - renderer.GetPositionInModelSpace() + snappedDelta, - /* smooth */ true); - } - } - /// - /// Update the position of the mesh whilst in 'snapping' mode. - /// - private void UpdatePositionSnapping() { - foreach (HeldMesh heldMesh in heldMeshes) { - MeshWithMaterialRenderer renderMesh = - heldMesh.Preview.transform.GetComponent(); - - Vector3 previewOffset = peltzerController.LastPositionModel + heldMesh.grabOffset; - previewOffset = Math3d.RotatePointAroundPivot(previewOffset, peltzerController.LastPositionModel, - peltzerController.LastRotationModel * Quaternion.Inverse(controllerStartRotationModel)); - Quaternion previewRotation = renderMesh.GetOrientationInModelSpace(); - Vector3 positionToSnap = Vector3.zero; - int previewFaceId = 0; - - Quaternion rotationDeltaModel = - peltzerController.LastRotationModel * Quaternion.Inverse(controllerStartRotationModel); - - SnapTransform snappedTransform = null; - - if (Features.useContinuousSnapDetection) { - Vector3 newPositionModel = Math3d.RotatePointAroundPivot( - peltzerController.LastPositionModel + heldMesh.grabOffset, - peltzerController.LastPositionModel, rotationDeltaModel); - - Quaternion newRotationModel = peltzerController.LastRotationModel * heldMesh.rotationOffsetSelf; - snappedTransform = snapSpace.Snap(previewOffset, newRotationModel); - snapDetector.UpdateHints(snapSpace, heldMesh.Mesh, previewOffset, previewRotation); - renderMesh.SetPositionModelSpace(snappedTransform.position, /* smooth */ true); - renderMesh.SetOrientationModelSpace(snappedTransform.rotation, /* smooth */ true); - } else { - if (snapGrid.snapType == SnapGrid.SnapType.VERTEX - || snapGrid.snapType == SnapGrid.SnapType.FACE) { - List vertices = new List(coplanarPreviewFaceVerticesAtOrigin.Count); - foreach (Vector3 vertex in coplanarPreviewFaceVerticesAtOrigin) { - vertices.Add((previewRotation * vertex) + previewOffset); + /// + /// Update the position of the mesh whilst in 'snapping' mode. + /// + private void UpdatePositionSnapping() + { + foreach (HeldMesh heldMesh in heldMeshes) + { + MeshWithMaterialRenderer renderMesh = + heldMesh.Preview.transform.GetComponent(); + + Vector3 previewOffset = peltzerController.LastPositionModel + heldMesh.grabOffset; + previewOffset = Math3d.RotatePointAroundPivot(previewOffset, peltzerController.LastPositionModel, + peltzerController.LastRotationModel * Quaternion.Inverse(controllerStartRotationModel)); + Quaternion previewRotation = renderMesh.GetOrientationInModelSpace(); + Vector3 positionToSnap = Vector3.zero; + int previewFaceId = 0; + + Quaternion rotationDeltaModel = + peltzerController.LastRotationModel * Quaternion.Inverse(controllerStartRotationModel); + + SnapTransform snappedTransform = null; + + if (Features.useContinuousSnapDetection) + { + Vector3 newPositionModel = Math3d.RotatePointAroundPivot( + peltzerController.LastPositionModel + heldMesh.grabOffset, + peltzerController.LastPositionModel, rotationDeltaModel); + + Quaternion newRotationModel = peltzerController.LastRotationModel * heldMesh.rotationOffsetSelf; + snappedTransform = snapSpace.Snap(previewOffset, newRotationModel); + snapDetector.UpdateHints(snapSpace, heldMesh.Mesh, previewOffset, previewRotation); + renderMesh.SetPositionModelSpace(snappedTransform.position, /* smooth */ true); + renderMesh.SetOrientationModelSpace(snappedTransform.rotation, /* smooth */ true); + } + else + { + if (snapGrid.snapType == SnapGrid.SnapType.VERTEX + || snapGrid.snapType == SnapGrid.SnapType.FACE) + { + List vertices = new List(coplanarPreviewFaceVerticesAtOrigin.Count); + foreach (Vector3 vertex in coplanarPreviewFaceVerticesAtOrigin) + { + vertices.Add((previewRotation * vertex) + previewOffset); + } + positionToSnap = MeshMath.CalculateGeometricCenter(vertices); + previewFaceId = previewFace.id; + } + SnapInfo snapInfo = snapGrid + .snapToGrid( + positionToSnap, + previewFaceId, + previewOffset, + previewRotation, + heldMesh.Mesh); + snappedTransform = snapInfo.transform; + + if (snapGrid.snapType == SnapGrid.SnapType.FACE && currentFaceSnapEffect != null) + { + currentFaceSnapEffect.UpdateSnapEffect(snapInfo); + } + + renderMesh.SetPositionModelSpace(snappedTransform.position, /* smooth */ true); + renderMesh.SetOrientationModelSpace(snappedTransform.rotation, /* smooth */ true); + } } - positionToSnap = MeshMath.CalculateGeometricCenter(vertices); - previewFaceId = previewFace.id; - } - SnapInfo snapInfo = snapGrid - .snapToGrid( - positionToSnap, - previewFaceId, - previewOffset, - previewRotation, - heldMesh.Mesh); - snappedTransform = snapInfo.transform; - - if (snapGrid.snapType == SnapGrid.SnapType.FACE && currentFaceSnapEffect != null) { - currentFaceSnapEffect.UpdateSnapEffect(snapInfo); - } - - renderMesh.SetPositionModelSpace(snappedTransform.position, /* smooth */ true); - renderMesh.SetOrientationModelSpace(snappedTransform.rotation, /* smooth */ true); } - } - } - /// - /// Update the position of the mesh whilst in neither 'grid mode' nor 'snapping' mode. - /// - private void UpdatePositionFreely() { - bool inTransitionToFreeMode = TRANSITION_DURATION >= (Time.time - lastTransitionTime); - - // Calculate how the controller has rotated in model space since the start of the operation. - Quaternion rotationDeltaModel = - peltzerController.LastRotationModel * Quaternion.Inverse(controllerStartRotationModel); - - foreach (HeldMesh heldMesh in heldMeshes) { - // Figure out the new position for this mesh based on how the controller has moved/rotated. - Vector3 newPositionModel = Math3d.RotatePointAroundPivot( - peltzerController.LastPositionModel + heldMesh.grabOffset, - peltzerController.LastPositionModel, rotationDeltaModel); - - MeshWithMaterialRenderer meshRenderer = - heldMesh.Preview.transform.GetComponent(); - meshRenderer.SetPositionModelSpace(newPositionModel, /* smooth */ false); - meshRenderer.SetOrientationModelSpace(peltzerController.LastRotationModel * heldMesh.rotationOffsetSelf, - /* smooth */ inTransitionToFreeMode); - } - } + /// + /// Update the position of the mesh whilst in neither 'grid mode' nor 'snapping' mode. + /// + private void UpdatePositionFreely() + { + bool inTransitionToFreeMode = TRANSITION_DURATION >= (Time.time - lastTransitionTime); + + // Calculate how the controller has rotated in model space since the start of the operation. + Quaternion rotationDeltaModel = + peltzerController.LastRotationModel * Quaternion.Inverse(controllerStartRotationModel); + + foreach (HeldMesh heldMesh in heldMeshes) + { + // Figure out the new position for this mesh based on how the controller has moved/rotated. + Vector3 newPositionModel = Math3d.RotatePointAroundPivot( + peltzerController.LastPositionModel + heldMesh.grabOffset, + peltzerController.LastPositionModel, rotationDeltaModel); + + MeshWithMaterialRenderer meshRenderer = + heldMesh.Preview.transform.GetComponent(); + meshRenderer.SetPositionModelSpace(newPositionModel, /* smooth */ false); + meshRenderer.SetOrientationModelSpace(peltzerController.LastRotationModel * heldMesh.rotationOffsetSelf, + /* smooth */ inTransitionToFreeMode); + } + } - public void UpdatePositions() { - // No need to update positions when pointing at the menu. - if (PeltzerMain.Instance.peltzerController.isPointingAtMenu) { - return; - } + public void UpdatePositions() + { + // No need to update positions when pointing at the menu. + if (PeltzerMain.Instance.peltzerController.isPointingAtMenu) + { + return; + } - if (IsInserting) { - if (lastMode != HoldMode.INSERT) { - HandleModeTransition(HoldMode.INSERT); - lastMode = HoldMode.INSERT; + if (IsInserting) + { + if (lastMode != HoldMode.INSERT) + { + HandleModeTransition(HoldMode.INSERT); + lastMode = HoldMode.INSERT; + } + + // Wait to see if we're filling, and also freeze the previews. + // TODO (bug): Determine if time, or distance from start click is the correct way to determine when to + // switch between insert types. + if (Time.time - insertStartTime > INSERT_TIME_LIMIT) + { + StartFillingVolume(); + } + } + else if (IsFilling) + { + if (lastMode != HoldMode.FILL) + { + HandleModeTransition(HoldMode.FILL); + lastMode = HoldMode.FILL; + } + ScalePreviewsToFill(); + } + else if (peltzerController.isBlockMode && !isSnapping) + { + if (lastMode != HoldMode.BLOCK) + { + HandleModeTransition(HoldMode.BLOCK); + lastMode = HoldMode.BLOCK; + } + UpdatePositionBlockMode(); + } + else if (isSnapping) + { + if (lastMode != HoldMode.SNAP) + { + HandleModeTransition(HoldMode.SNAP); + lastMode = HoldMode.SNAP; + } + UpdatePositionSnapping(); + } + else + { + if (lastMode != HoldMode.FREE) + { + HandleModeTransition(HoldMode.FREE); + lastMode = HoldMode.FREE; + } + UpdatePositionFreely(); + } } - // Wait to see if we're filling, and also freeze the previews. - // TODO (bug): Determine if time, or distance from start click is the correct way to determine when to - // switch between insert types. - if (Time.time - insertStartTime > INSERT_TIME_LIMIT) { - StartFillingVolume(); - } - } else if (IsFilling) { - if (lastMode != HoldMode.FILL) { - HandleModeTransition(HoldMode.FILL); - lastMode = HoldMode.FILL; - } - ScalePreviewsToFill(); - } else if (peltzerController.isBlockMode && !isSnapping) { - if (lastMode != HoldMode.BLOCK) { - HandleModeTransition(HoldMode.BLOCK); - lastMode = HoldMode.BLOCK; - } - UpdatePositionBlockMode(); - } else if (isSnapping) { - if (lastMode != HoldMode.SNAP) { - HandleModeTransition(HoldMode.SNAP); - lastMode = HoldMode.SNAP; + /// + /// Does any required bookkeeping when transitioning between modes. + /// + private void HandleModeTransition(HoldMode curMode) + { + lastTransitionTime = Time.time; + if (lastMode == HoldMode.BLOCK) + { + blockSlerpee = null; + } } - UpdatePositionSnapping(); - } else { - if (lastMode != HoldMode.FREE) { - HandleModeTransition(HoldMode.FREE); - lastMode = HoldMode.FREE; + + private static int spacer = 0; + + /// + /// Adjust the scale of the preview based on the distance between controllerStartPosition to + /// the controller's current position. + /// + private void ScalePreviewsToFill() + { + Vector3 currentDelta; + if (peltzerController.isBlockMode && isSnapping) + { + // Fill in grid units and produce "correct" x, y, and z ratios, meaning ratios that produce a + // uniformly scaled primitive when being used in volume insertion; for example, click and + // dragging a cube will only produce a perfect cube. + currentDelta = GetGridFillDiagonal(/*uniformScale*/ true); + } + else if (peltzerController.isBlockMode) + { + // Fill in grid units. + currentDelta = GetGridFillDiagonal(/*uniformScale*/ false); + } + else if (isSnapping) + { + // Fill smoothly and produce "correct" x, y, and z ratios, meaning ratios that produce a + // uniformly scaled primitive when being used in volume insertion; for example, click and + // dragging a cube will only produce a perfect cube. + currentDelta = GetSmoothFillDiagonal(/*uniformScale*/ true); + } + else + { + // Just fill smoothly. + currentDelta = GetSmoothFillDiagonal(/*uniformScale*/ false); + } + Vector3 newQuadrant = GetQuadrant(peltzerController.LastPositionModel - controllerStartPosition); + + // If the quadrant has changed reset the locked corner. + bool quadrantChanged = newQuadrant != currentQuadrant; + if (quadrantChanged) + { + currentQuadrant = newQuadrant; + lockedCorner = FindLockedCorner(); + } + + foreach (HeldMesh heldMesh in heldMeshes) + { + // Move the vertices. + MMesh.GeometryOperation vertScaleOperation = heldMesh.Mesh.StartOperation(); + Vector3 newOffset = Vector3.zero; + Dictionary newVertexLocations = new Dictionary(); + + Vector3 scaleFactor = Vector3.one + + new Vector3(currentDelta.x / heldMesh.originalSize.x, + currentDelta.y / heldMesh.originalSize.y, + currentDelta.z / heldMesh.originalSize.z); + + // The only way to get from a scale factor of 1.0 to a scale factor of >1.1 is to first have passed a + // scale factor of 1.2, or to already be at 1.1. Essentially, you must drag to the 20% increase mark + // before you can hit the 10% increase mark. This hysteresis helps users drag along a dominant axis without + // accidentally adding noise on a minor axis. See bug for more details. + if (scaleFactor.x < 1.2f && (lastFillingScaleFactor == null || lastFillingScaleFactor.Value.x < 1.1f)) + { + scaleFactor.x = 1f; + } + if (scaleFactor.y < 1.2f && (lastFillingScaleFactor == null || lastFillingScaleFactor.Value.y < 1.1f)) + { + scaleFactor.y = 1f; + } + if (scaleFactor.z < 1.2f && (lastFillingScaleFactor == null || lastFillingScaleFactor.Value.z < 1.1f)) + { + scaleFactor.z = 1f; + } + + lastFillingScaleFactor = scaleFactor; + + foreach (KeyValuePair pair in heldMesh.originalVertexLocations) + { + int id = pair.Key; + Vector3 originalLoc = pair.Value; + Vector3 scaledVertexLoc = Vector3.Scale(originalLoc, scaleFactor); + + vertScaleOperation.ModifyVertexMeshSpace(id, scaledVertexLoc); + newOffset += scaledVertexLoc; + + } + + newOffset /= heldMesh.originalVertexLocations.Count; + + // Move the offset to the center, and move the vertices in the opposite direction to keep them in the right place. + Dictionary newVertices = new Dictionary(); + Vector3 offsetDelta = newOffset - heldMesh.Mesh.offset; + foreach (KeyValuePair pair in heldMesh.originalVertexLocations) + { + vertScaleOperation.ModifyVertexMeshSpace(pair.Key, + vertScaleOperation.GetCurrentVertexPositionMeshSpace(pair.Key) - offsetDelta); + } + + vertScaleOperation.Commit(); + // And update the offset and the vertices. + heldMesh.Mesh.offset = newOffset; + heldMesh.Mesh.RecalcBounds(); + + // And regenerate the previews. + MMesh.AttachMeshToGameObject(worldSpace, heldMesh.Preview, heldMesh.Mesh, /* updateOnly */ true); + + // Then re-position them to be locked to one corner. + MeshWithMaterialRenderer renderMesh = heldMesh.Preview.GetComponent(); + Vector3 updatedPosition = + ResetVolumePosition(heldMesh.fillStartPosition, heldMesh.fillStartExtents, heldMesh.Mesh.bounds.extents); + + renderMesh.SetPositionModelSpace(updatedPosition, /* smooth */ false); + renderMesh.SetOrientationModelSpace(heldMesh.fillStartRotation, /* smooth */ false); + + bool useRuler = heldMesh.isPrimitive && Features.showVolumeInserterRuler; + if (useRuler) DisplayRuler(heldMesh, scaleFactor); + } } - UpdatePositionFreely(); - } - } - /// - /// Does any required bookkeeping when transitioning between modes. - /// - private void HandleModeTransition(HoldMode curMode) { - lastTransitionTime = Time.time; - if (lastMode == HoldMode.BLOCK) { - blockSlerpee = null; - } - } + // Displays a ruler for drag-sizing a primitive. Most of this is calculations for properly aligning text depending + // on where the user drags. + private void DisplayRuler(HeldMesh heldMesh, Vector3 scaleFactor) + { + ruler.SetActive(true); + paletteRuler.SetActive(true); + + // Dimensions and half dimensions of the mesh we're measuring, using its oriented bounding box rather than AABB. + float dimX = heldMesh.originalSize.x; + float dimY = heldMesh.originalSize.y; + float dimZ = heldMesh.originalSize.z; + float hDimX = dimX / 2f; + float hDimY = dimY / 2f; + float hDimZ = dimZ / 2f; + + // Mesh space coords of the extents of the oriented bounding box - used for text positioning. + float leftX, rightX; + float bottomY, topY; + float frontZ, backZ; + + // model space x coord immediately to the left of the primitive + leftX = currentQuadrant.x == 1 + ? -hDimX + : -dimX * scaleFactor.x + hDimX; + + // model space x coord for the right side on the primitive + rightX = currentQuadrant.x != 1 + ? hDimX + : dimX * scaleFactor.x - hDimX; + + // model space y coord immediately on top of the primitive + bottomY = currentQuadrant.y == 1 + ? -hDimY + : -dimY * scaleFactor.y + hDimY; + + // model space y coord immediately on top of the primitive + topY = currentQuadrant.y != 1 + ? hDimY + : dimY * scaleFactor.y - hDimY; + + // model space z coord to the front of the model + frontZ = currentQuadrant.z == 1 + ? -hDimZ + : -dimZ * scaleFactor.z + hDimZ; + + // model space z coord to the front of the model + backZ = currentQuadrant.z != 1 + ? hDimZ + : dimZ * scaleFactor.z - hDimZ; + + XFrontText.text = (Mathf.Round(heldMesh.originalSize.x * scaleFactor.x * 100) / 100).ToString("0.00") + "m"; + XTopText.text = XFrontText.text; + float XTwidth2 = ruler.transform.localScale.x * XFrontText.textBounds.size.x / 1f; + float XTheight2 = ruler.transform.localScale.x * XFrontText.textBounds.size.y / 2f; + + YFrontText.text = (Mathf.Round(heldMesh.originalSize.y * scaleFactor.y * 100) / 100).ToString("0.00") + "m"; + YSideText.text = YFrontText.text; + float YTwidth2 = ruler.transform.localScale.x * YFrontText.textBounds.size.x / 1f; + float YTheight2 = ruler.transform.localScale.x * YFrontText.textBounds.size.y / 2f; + + ZTopText.text = (Mathf.Round(heldMesh.originalSize.z * scaleFactor.z * 100) / 100).ToString("0.00") + "m"; + ZSideText.text = ZTopText.text; + float ZTwidth2 = ruler.transform.localScale.x * ZTopText.textBounds.size.x / 1f; + float ZTheight2 = ruler.transform.localScale.x * ZTopText.textBounds.size.y / 2f; + + Vector3 XFrontPos, XTopPos, YFrontPos, YSidePos, ZTopPos, ZSidePos; + Quaternion XFrontRot, XTopRot, YFrontRot, YSideRot, ZTopRot, ZSideRot; + + // Win Z-Fights + float epsilon = 0.001f; + + + // This section figures out which side of the box the user is most facing, and ensures that we display + // measurements on it even if controller position would indicate otherwise. + + // Using vector from headset to BB center - raw view direction is more unpredictable + Vector3 center = heldMesh.fillStartPosition + heldMesh.fillStartRotation * + new Vector3((leftX + rightX) * 0.5f, (topY + bottomY) * 0.5f, (frontZ + backZ) * 0.5f); + Vector3 modelSpaceCameraForward = (center - worldSpace.WorldToModel(Camera.main.transform.position)).normalized; + //Determine which side of the bounding box the user is most facing + Vector3 bbUp, bbForward, bbRight; + bbUp = heldMesh.fillStartRotation * Vector3.up; + bbForward = heldMesh.fillStartRotation * Vector3.forward; + bbRight = heldMesh.fillStartRotation * Vector3.right; + + // Order + // 0 Right + // 1 Left + // 2 Top + // 3 Bottom + // 4 Back + // 5 Front + float[] dots = new float[6]; + + dots[0] = Vector3.Dot(bbRight.normalized, modelSpaceCameraForward); + dots[1] = Vector3.Dot(-bbRight.normalized, modelSpaceCameraForward); + dots[2] = Vector3.Dot(bbUp.normalized, modelSpaceCameraForward); + dots[3] = Vector3.Dot(-bbUp.normalized, modelSpaceCameraForward); + dots[4] = Vector3.Dot(bbForward.normalized, modelSpaceCameraForward); + dots[5] = Vector3.Dot(-bbForward.normalized, modelSpaceCameraForward); + //Pick the most negative dot product - that face is most facing the view direction + int mostFacingFace = 0; + + for (int i = 1; i <= 5; i++) + { + if (dots[i] < dots[mostFacingFace]) + { + mostFacingFace = i; + } + } - private static int spacer = 0; + // Handle left/right alignment + if ((currentQuadrant.x > 0 || mostFacingFace == 0) && mostFacingFace != 1) + { + // Align right + ZSideRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation + * Quaternion.AngleAxis(-90f, Vector3.up)); + YSideRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation + * Quaternion.AngleAxis(90f, Vector3.right) * Quaternion.AngleAxis(-90f, Vector3.up)); + + YSidePos.x = rightX + epsilon; + ZSidePos.x = rightX + epsilon; + XFrontPos.x = rightX - XTwidth2; + YFrontPos.x = rightX - YTheight2; + XTopPos.x = rightX - XTwidth2; + ZTopPos.x = rightX - ZTheight2; + } + else + { + // Align left + ZSideRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation + * Quaternion.AngleAxis(90f, Vector3.up)); + YSideRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation + * Quaternion.AngleAxis(90f, Vector3.right) * Quaternion.AngleAxis(90f, Vector3.up)); + + XFrontPos.x = leftX + XTwidth2; + YFrontPos.x = leftX + YTheight2; + YSidePos.x = leftX - epsilon; + ZSidePos.x = leftX - epsilon; + XTopPos.x = leftX + XTwidth2; + ZTopPos.x = leftX + ZTheight2; + } - /// - /// Adjust the scale of the preview based on the distance between controllerStartPosition to - /// the controller's current position. - /// - private void ScalePreviewsToFill() { - Vector3 currentDelta; - if (peltzerController.isBlockMode && isSnapping) { - // Fill in grid units and produce "correct" x, y, and z ratios, meaning ratios that produce a - // uniformly scaled primitive when being used in volume insertion; for example, click and - // dragging a cube will only produce a perfect cube. - currentDelta = GetGridFillDiagonal(/*uniformScale*/ true); - } else if (peltzerController.isBlockMode) { - // Fill in grid units. - currentDelta = GetGridFillDiagonal(/*uniformScale*/ false); - } else if (isSnapping) { - // Fill smoothly and produce "correct" x, y, and z ratios, meaning ratios that produce a - // uniformly scaled primitive when being used in volume insertion; for example, click and - // dragging a cube will only produce a perfect cube. - currentDelta = GetSmoothFillDiagonal(/*uniformScale*/ true); - } else { - // Just fill smoothly. - currentDelta = GetSmoothFillDiagonal(/*uniformScale*/ false); - } - Vector3 newQuadrant = GetQuadrant(peltzerController.LastPositionModel - controllerStartPosition); - - // If the quadrant has changed reset the locked corner. - bool quadrantChanged = newQuadrant != currentQuadrant; - if (quadrantChanged) { - currentQuadrant = newQuadrant; - lockedCorner = FindLockedCorner(); - } - - foreach (HeldMesh heldMesh in heldMeshes) { - // Move the vertices. - MMesh.GeometryOperation vertScaleOperation = heldMesh.Mesh.StartOperation(); - Vector3 newOffset = Vector3.zero; - Dictionary newVertexLocations = new Dictionary(); - - Vector3 scaleFactor = Vector3.one + - new Vector3(currentDelta.x / heldMesh.originalSize.x, - currentDelta.y / heldMesh.originalSize.y, - currentDelta.z / heldMesh.originalSize.z); - - // The only way to get from a scale factor of 1.0 to a scale factor of >1.1 is to first have passed a - // scale factor of 1.2, or to already be at 1.1. Essentially, you must drag to the 20% increase mark - // before you can hit the 10% increase mark. This hysteresis helps users drag along a dominant axis without - // accidentally adding noise on a minor axis. See bug for more details. - if (scaleFactor.x < 1.2f && (lastFillingScaleFactor == null || lastFillingScaleFactor.Value.x < 1.1f)) { - scaleFactor.x = 1f; - } - if (scaleFactor.y < 1.2f && (lastFillingScaleFactor == null || lastFillingScaleFactor.Value.y < 1.1f)) { - scaleFactor.y = 1f; - } - if (scaleFactor.z < 1.2f && (lastFillingScaleFactor == null || lastFillingScaleFactor.Value.z < 1.1f)) { - scaleFactor.z = 1f; - } + // Handle top/bottom alignment + if ((currentQuadrant.y > 0 || mostFacingFace == 2) && mostFacingFace != 3) + { + // Align top + XTopRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation + * Quaternion.AngleAxis(90f, Vector3.right)); + ZTopRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation + * Quaternion.AngleAxis(90f, Vector3.up) * Quaternion.AngleAxis(90f, Vector3.right)); + + YSidePos.y = topY - YTwidth2; + ZSidePos.y = topY - ZTheight2; + XFrontPos.y = topY - XTheight2; + YFrontPos.y = topY - YTwidth2; + XTopPos.y = topY + epsilon; + ZTopPos.y = topY + epsilon; + } + else + { + // Align bottom + XTopRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation + * Quaternion.AngleAxis(-90f, Vector3.right)); + ZTopRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation + * Quaternion.AngleAxis(90f, Vector3.up) * Quaternion.AngleAxis(-90f, Vector3.right)); + + YSidePos.y = bottomY + YTwidth2; + ZSidePos.y = bottomY + ZTheight2; + XFrontPos.y = bottomY + XTheight2; + YFrontPos.y = bottomY + YTwidth2; + XTopPos.y = bottomY - epsilon; + ZTopPos.y = bottomY - epsilon; + } - lastFillingScaleFactor = scaleFactor; - - foreach (KeyValuePair pair in heldMesh.originalVertexLocations) { - int id = pair.Key; - Vector3 originalLoc = pair.Value; - Vector3 scaledVertexLoc = Vector3.Scale(originalLoc, scaleFactor); - - vertScaleOperation.ModifyVertexMeshSpace(id, scaledVertexLoc); - newOffset += scaledVertexLoc; - - } - - newOffset /= heldMesh.originalVertexLocations.Count; - - // Move the offset to the center, and move the vertices in the opposite direction to keep them in the right place. - Dictionary newVertices = new Dictionary(); - Vector3 offsetDelta = newOffset - heldMesh.Mesh.offset; - foreach (KeyValuePair pair in heldMesh.originalVertexLocations) { - vertScaleOperation.ModifyVertexMeshSpace(pair.Key, - vertScaleOperation.GetCurrentVertexPositionMeshSpace(pair.Key) - offsetDelta); - } + // Handle front/back alignment + if ((currentQuadrant.z > 0 || mostFacingFace == 4) && mostFacingFace != 5) + { + // Align back + XFrontRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation + * Quaternion.AngleAxis(180f, Vector3.up)); + YFrontRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation + * Quaternion.AngleAxis(180f, Vector3.up) * Quaternion.AngleAxis(90f, Vector3.forward)); + + YSidePos.z = backZ - YTheight2; + ZSidePos.z = backZ - ZTwidth2; + XFrontPos.z = backZ + epsilon; + YFrontPos.z = backZ + epsilon; + XTopPos.z = backZ - XTheight2; + ZTopPos.z = backZ - ZTwidth2; + } + else + { + // Align front + XFrontRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation); + YFrontRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation * + Quaternion.AngleAxis(90f, Vector3.forward)); + + YSidePos.z = frontZ + YTheight2; + ZSidePos.z = frontZ + ZTwidth2; + XFrontPos.z = frontZ - epsilon; + YFrontPos.z = frontZ - epsilon; + XTopPos.z = frontZ + XTheight2; + ZTopPos.z = frontZ + ZTwidth2; + } - vertScaleOperation.Commit(); - // And update the offset and the vertices. - heldMesh.Mesh.offset = newOffset; - heldMesh.Mesh.RecalcBounds(); - - // And regenerate the previews. - MMesh.AttachMeshToGameObject(worldSpace, heldMesh.Preview, heldMesh.Mesh, /* updateOnly */ true); - - // Then re-position them to be locked to one corner. - MeshWithMaterialRenderer renderMesh = heldMesh.Preview.GetComponent(); - Vector3 updatedPosition = - ResetVolumePosition(heldMesh.fillStartPosition, heldMesh.fillStartExtents, heldMesh.Mesh.bounds.extents); - - renderMesh.SetPositionModelSpace(updatedPosition, /* smooth */ false); - renderMesh.SetOrientationModelSpace(heldMesh.fillStartRotation, /* smooth */ false); - - bool useRuler = heldMesh.isPrimitive && Features.showVolumeInserterRuler; - if (useRuler) DisplayRuler(heldMesh, scaleFactor); - } - } - // Displays a ruler for drag-sizing a primitive. Most of this is calculations for properly aligning text depending - // on where the user drags. - private void DisplayRuler(HeldMesh heldMesh, Vector3 scaleFactor) { - ruler.SetActive(true); - paletteRuler.SetActive(true); - - // Dimensions and half dimensions of the mesh we're measuring, using its oriented bounding box rather than AABB. - float dimX = heldMesh.originalSize.x; - float dimY = heldMesh.originalSize.y; - float dimZ = heldMesh.originalSize.z; - float hDimX = dimX / 2f; - float hDimY = dimY / 2f; - float hDimZ = dimZ / 2f; - - // Mesh space coords of the extents of the oriented bounding box - used for text positioning. - float leftX, rightX; - float bottomY, topY; - float frontZ, backZ; - - // model space x coord immediately to the left of the primitive - leftX = currentQuadrant.x == 1 - ? -hDimX - : -dimX * scaleFactor.x + hDimX; - - // model space x coord for the right side on the primitive - rightX = currentQuadrant.x != 1 - ? hDimX - : dimX * scaleFactor.x - hDimX; - - // model space y coord immediately on top of the primitive - bottomY = currentQuadrant.y == 1 - ? -hDimY - : -dimY * scaleFactor.y + hDimY; - - // model space y coord immediately on top of the primitive - topY = currentQuadrant.y != 1 - ? hDimY - : dimY * scaleFactor.y - hDimY; - - // model space z coord to the front of the model - frontZ = currentQuadrant.z == 1 - ? -hDimZ - : -dimZ * scaleFactor.z + hDimZ; - - // model space z coord to the front of the model - backZ = currentQuadrant.z != 1 - ? hDimZ - : dimZ * scaleFactor.z - hDimZ; - - XFrontText.text = (Mathf.Round(heldMesh.originalSize.x * scaleFactor.x * 100) / 100).ToString("0.00") + "m"; - XTopText.text = XFrontText.text; - float XTwidth2 = ruler.transform.localScale.x * XFrontText.textBounds.size.x / 1f; - float XTheight2 = ruler.transform.localScale.x * XFrontText.textBounds.size.y / 2f; - - YFrontText.text = (Mathf.Round(heldMesh.originalSize.y * scaleFactor.y * 100) / 100).ToString("0.00") + "m"; - YSideText.text = YFrontText.text; - float YTwidth2 = ruler.transform.localScale.x * YFrontText.textBounds.size.x / 1f; - float YTheight2 = ruler.transform.localScale.x * YFrontText.textBounds.size.y / 2f; - - ZTopText.text = (Mathf.Round(heldMesh.originalSize.z * scaleFactor.z * 100) / 100).ToString("0.00") + "m"; - ZSideText.text = ZTopText.text; - float ZTwidth2 = ruler.transform.localScale.x * ZTopText.textBounds.size.x / 1f; - float ZTheight2 = ruler.transform.localScale.x * ZTopText.textBounds.size.y / 2f; - - Vector3 XFrontPos, XTopPos, YFrontPos, YSidePos, ZTopPos, ZSidePos; - Quaternion XFrontRot, XTopRot, YFrontRot, YSideRot, ZTopRot, ZSideRot; - - // Win Z-Fights - float epsilon = 0.001f; - - - // This section figures out which side of the box the user is most facing, and ensures that we display - // measurements on it even if controller position would indicate otherwise. - - // Using vector from headset to BB center - raw view direction is more unpredictable - Vector3 center = heldMesh.fillStartPosition + heldMesh.fillStartRotation * - new Vector3((leftX + rightX) * 0.5f, (topY + bottomY) * 0.5f, (frontZ + backZ) * 0.5f); - Vector3 modelSpaceCameraForward = (center - worldSpace.WorldToModel(Camera.main.transform.position)).normalized; - //Determine which side of the bounding box the user is most facing - Vector3 bbUp, bbForward, bbRight; - bbUp = heldMesh.fillStartRotation * Vector3.up; - bbForward = heldMesh.fillStartRotation * Vector3.forward; - bbRight = heldMesh.fillStartRotation * Vector3.right; - - // Order - // 0 Right - // 1 Left - // 2 Top - // 3 Bottom - // 4 Back - // 5 Front - float[] dots = new float[6]; - - dots[0] = Vector3.Dot(bbRight.normalized, modelSpaceCameraForward); - dots[1] = Vector3.Dot(-bbRight.normalized, modelSpaceCameraForward); - dots[2] = Vector3.Dot(bbUp.normalized, modelSpaceCameraForward); - dots[3] = Vector3.Dot(-bbUp.normalized, modelSpaceCameraForward); - dots[4] = Vector3.Dot(bbForward.normalized, modelSpaceCameraForward); - dots[5] = Vector3.Dot(-bbForward.normalized, modelSpaceCameraForward); - //Pick the most negative dot product - that face is most facing the view direction - int mostFacingFace = 0; - - for (int i = 1; i <= 5; i++) { - if (dots[i] < dots[mostFacingFace]) { - mostFacingFace = i; + // Position the cubes that frame the dimensions we are measuring - this essentially frames an oriented bounding + // box for our primitives (even though their real bounding box is AABB). + SetBoundingBoxCubeXForm(pXpYpZ, heldMesh, new Vector3(rightX, topY, backZ)); + SetBoundingBoxCubeXForm(pXpYnZ, heldMesh, new Vector3(rightX, topY, frontZ)); + SetBoundingBoxCubeXForm(pXnYpZ, heldMesh, new Vector3(rightX, bottomY, backZ)); + SetBoundingBoxCubeXForm(pXnYnZ, heldMesh, new Vector3(rightX, bottomY, frontZ)); + SetBoundingBoxCubeXForm(nXpYpZ, heldMesh, new Vector3(leftX, topY, backZ)); + SetBoundingBoxCubeXForm(nXpYnZ, heldMesh, new Vector3(leftX, topY, frontZ)); + SetBoundingBoxCubeXForm(nXnYpZ, heldMesh, new Vector3(leftX, bottomY, backZ)); + SetBoundingBoxCubeXForm(nXnYnZ, heldMesh, new Vector3(leftX, bottomY, frontZ)); + + // Sets the text on the paletteRuler which appears above the palette + paletteRuler.GetComponent().text = XFrontText.text + " x " + YFrontText.text + " x " + ZTopText.text; + + // Setting the transforms for all of the other text elements + SetRulerXForm(rulerYSide, heldMesh, YSideRot, YSidePos); + SetRulerXForm(rulerZSide, heldMesh, ZSideRot, ZSidePos); + SetRulerXForm(rulerXFrontBack, heldMesh, XFrontRot, XFrontPos); + SetRulerXForm(rulerYFrontBack, heldMesh, YFrontRot, YFrontPos); + SetRulerXForm(rulerXTopBottom, heldMesh, XTopRot, XTopPos); + SetRulerXForm(rulerZTopBottom, heldMesh, ZTopRot, ZTopPos); } - } - - // Handle left/right alignment - if ((currentQuadrant.x > 0 || mostFacingFace == 0) && mostFacingFace != 1) { - // Align right - ZSideRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation - * Quaternion.AngleAxis(-90f, Vector3.up)); - YSideRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation - * Quaternion.AngleAxis(90f, Vector3.right) * Quaternion.AngleAxis(-90f, Vector3.up)); - - YSidePos.x = rightX + epsilon; - ZSidePos.x = rightX + epsilon; - XFrontPos.x = rightX - XTwidth2; - YFrontPos.x = rightX - YTheight2; - XTopPos.x = rightX - XTwidth2; - ZTopPos.x = rightX - ZTheight2; - } else { - // Align left - ZSideRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation - * Quaternion.AngleAxis(90f, Vector3.up)); - YSideRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation - * Quaternion.AngleAxis(90f, Vector3.right) * Quaternion.AngleAxis(90f, Vector3.up) ); - - XFrontPos.x = leftX + XTwidth2; - YFrontPos.x = leftX + YTheight2; - YSidePos.x = leftX - epsilon; - ZSidePos.x = leftX - epsilon; - XTopPos.x = leftX + XTwidth2; - ZTopPos.x = leftX + ZTheight2; - } - - // Handle top/bottom alignment - if ((currentQuadrant.y > 0 || mostFacingFace == 2) && mostFacingFace != 3) { - // Align top - XTopRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation - * Quaternion.AngleAxis(90f, Vector3.right)); - ZTopRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation - * Quaternion.AngleAxis(90f, Vector3.up) * Quaternion.AngleAxis(90f, Vector3.right)); - - YSidePos.y = topY - YTwidth2; - ZSidePos.y = topY - ZTheight2; - XFrontPos.y = topY - XTheight2; - YFrontPos.y = topY - YTwidth2; - XTopPos.y = topY + epsilon; - ZTopPos.y = topY + epsilon; - } else { - // Align bottom - XTopRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation - * Quaternion.AngleAxis(-90f, Vector3.right)); - ZTopRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation - * Quaternion.AngleAxis(90f, Vector3.up) * Quaternion.AngleAxis(-90f, Vector3.right)); - - YSidePos.y = bottomY + YTwidth2; - ZSidePos.y = bottomY + ZTheight2; - XFrontPos.y = bottomY + XTheight2; - YFrontPos.y = bottomY + YTwidth2; - XTopPos.y = bottomY - epsilon; - ZTopPos.y = bottomY - epsilon; - } - - // Handle front/back alignment - if ((currentQuadrant.z > 0 || mostFacingFace == 4) && mostFacingFace != 5) { - // Align back - XFrontRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation - * Quaternion.AngleAxis(180f, Vector3.up)); - YFrontRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation - * Quaternion.AngleAxis(180f, Vector3.up) * Quaternion.AngleAxis(90f, Vector3.forward)); - - YSidePos.z = backZ - YTheight2; - ZSidePos.z = backZ - ZTwidth2; - XFrontPos.z = backZ + epsilon; - YFrontPos.z = backZ + epsilon; - XTopPos.z = backZ - XTheight2; - ZTopPos.z = backZ - ZTwidth2; - } else { - // Align front - XFrontRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation); - YFrontRot = worldSpace.ModelOrientationToWorld(heldMesh.fillStartRotation * - Quaternion.AngleAxis(90f, Vector3.forward)); - - YSidePos.z = frontZ + YTheight2; - ZSidePos.z = frontZ + ZTwidth2; - XFrontPos.z = frontZ - epsilon; - YFrontPos.z = frontZ - epsilon; - XTopPos.z = frontZ + XTheight2; - ZTopPos.z = frontZ + ZTwidth2; - } - - - // Position the cubes that frame the dimensions we are measuring - this essentially frames an oriented bounding - // box for our primitives (even though their real bounding box is AABB). - SetBoundingBoxCubeXForm(pXpYpZ, heldMesh, new Vector3(rightX, topY, backZ)); - SetBoundingBoxCubeXForm(pXpYnZ, heldMesh, new Vector3(rightX, topY, frontZ)); - SetBoundingBoxCubeXForm(pXnYpZ, heldMesh, new Vector3(rightX, bottomY, backZ)); - SetBoundingBoxCubeXForm(pXnYnZ, heldMesh, new Vector3(rightX, bottomY, frontZ)); - SetBoundingBoxCubeXForm(nXpYpZ, heldMesh, new Vector3(leftX, topY, backZ)); - SetBoundingBoxCubeXForm(nXpYnZ, heldMesh, new Vector3(leftX, topY, frontZ)); - SetBoundingBoxCubeXForm(nXnYpZ, heldMesh, new Vector3(leftX, bottomY, backZ)); - SetBoundingBoxCubeXForm(nXnYnZ, heldMesh, new Vector3(leftX, bottomY, frontZ)); - - // Sets the text on the paletteRuler which appears above the palette - paletteRuler.GetComponent().text = XFrontText.text + " x " + YFrontText.text + " x " + ZTopText.text; - - // Setting the transforms for all of the other text elements - SetRulerXForm(rulerYSide, heldMesh, YSideRot, YSidePos); - SetRulerXForm(rulerZSide, heldMesh, ZSideRot, ZSidePos); - SetRulerXForm(rulerXFrontBack, heldMesh, XFrontRot, XFrontPos); - SetRulerXForm(rulerYFrontBack, heldMesh, YFrontRot, YFrontPos); - SetRulerXForm(rulerXTopBottom, heldMesh, XTopRot, XTopPos); - SetRulerXForm(rulerZTopBottom, heldMesh, ZTopRot, ZTopPos); - } - - private void SetBoundingBoxCubeXForm(GameObject bbCube, HeldMesh heldMesh, Vector3 positionMesh) { - bbCube.transform.rotation = heldMesh.fillStartRotation; - bbCube.transform.position = worldSpace.ModelToWorld(heldMesh.fillStartPosition - + heldMesh.fillStartRotation * positionMesh); - } - - private void SetRulerXForm(GameObject ruler, HeldMesh heldMesh, Quaternion orientationWorld, Vector3 positionMesh) { - ruler.transform.rotation = orientationWorld; - ruler.transform.position = - worldSpace.ModelToWorld(heldMesh.fillStartPosition + heldMesh.fillStartRotation * positionMesh); - } - /// - /// Corrects the diagonal of a rectangular prism such that it produces a primitive volume with "correct" - /// x, y, and z ratios, meaning ratios that produce a uniformly scaled primitive when being used in volume insertion; - /// for example, click and dragging a cube will only produce a perfect cube. - /// All primitives except the torus fit correctly in a 1x1x1 rectangular prism; the torus fits in - /// a 4x1x4 prism, and the diagonal must be scaled accordingly. - /// - private Vector3 CorrectRatio(Vector3 diagonal) { - float maxExtent = Mathf.Max(diagonal.x, diagonal.y, diagonal.z); - bool isTorus = (Primitives.Shape)peltzerController.shapesMenu.CurrentItemId == Primitives.Shape.TORUS; - return new Vector3(maxExtent, isTorus ? maxExtent * 0.25f : maxExtent, maxExtent); - } - - /// - /// Finds the diagonal for the rectangular prism that the volumeMesh should fill that fills in grid units. - /// - /// Whether the diagonal should be corrected before being returned so that - /// it produces a volume with uniform x, y, and z ratios. - /// - /// The diagonal for the rectangular prism that is snapped to the grid and not smaller than the simple mesh. - /// - private Vector3 GetGridFillDiagonal(bool uniformScale) { - Vector3 diagonal = peltzerController.LastPositionModel - controllerStartPosition; - - // We can't work out the 'rotation' of a group of meshes yet, so we pick one at random. - diagonal = Quaternion.Inverse(GetFirstHeldMesh().fillStartRotation) * diagonal; - - // Find the controller delta in grid units, ensuring that the rectangular prism is not smaller than - // the bounds of the original insertion. - // We remove the original size of the bounding box: a user must move as far as the bounding box on - // any given axis to begin filling. - diagonal.x = Mathf.Max(0, GridUtils.SnapToGrid(Mathf.Abs(diagonal.x) - extentsOfHeldMeshes.x)); - diagonal.y = Mathf.Max(0, GridUtils.SnapToGrid(Mathf.Abs(diagonal.y) - extentsOfHeldMeshes.y)); - diagonal.z = Mathf.Max(0, GridUtils.SnapToGrid(Mathf.Abs(diagonal.z) - extentsOfHeldMeshes.z)); - - return uniformScale ? CorrectRatio(diagonal) : diagonal; - } - - /// - /// Finds the diagonal for the rectangular prism that the volumeMesh should fill that ensures a smooth - /// fill effect. - /// - /// Whether the diagonal should be corrected before being returned so that - /// it produces a volume with uniform x, y, and z ratios. - /// - /// The diagonal for the rectangular prism that is NOT NECESSARILY snapped to the grid - /// and not smaller than the simple mesh. - /// - private Vector3 GetSmoothFillDiagonal(bool uniformScale) { - Vector3 diagonal = peltzerController.LastPositionModel - controllerStartPosition; - - // We can't work out the 'rotation' of a group of meshes yet, so we pick one at random. - diagonal = Quaternion.Inverse(GetFirstHeldMesh().fillStartRotation) * diagonal; - - // We remove the original size of the bounding box: a user must move as far as the bounding box on - // any given axis to begin filling. - diagonal.x = Mathf.Max(0, Mathf.Abs(diagonal.x) - extentsOfHeldMeshes.x); - diagonal.y = Mathf.Max(0, Mathf.Abs(diagonal.y) - extentsOfHeldMeshes.y); - diagonal.z = Mathf.Max(0, Mathf.Abs(diagonal.z) - extentsOfHeldMeshes.z); - - return uniformScale ? CorrectRatio(diagonal) : diagonal; - } + private void SetBoundingBoxCubeXForm(GameObject bbCube, HeldMesh heldMesh, Vector3 positionMesh) + { + bbCube.transform.rotation = heldMesh.fillStartRotation; + bbCube.transform.position = worldSpace.ModelToWorld(heldMesh.fillStartPosition + + heldMesh.fillStartRotation * positionMesh); + } - /// - /// Determines what quadrant a vector is pointing towards. - /// - /// The vector to check. - /// A Vector3 representing the quadrant. - private Vector3 GetQuadrant(Vector3 diagonal) { - // We can't work out the 'rotation' of a group of meshes yet, so we pick one at random. - diagonal = Quaternion.Inverse(GetFirstHeldMesh().fillStartRotation) * diagonal; - Vector3 quadrant = new Vector3(); - - quadrant.x = (diagonal.x >= 0) ? 1 : -1; - quadrant.y = (diagonal.y >= 0) ? 1 : -1; - quadrant.z = (diagonal.z >= 0) ? 1 : -1; - - return quadrant; - } + private void SetRulerXForm(GameObject ruler, HeldMesh heldMesh, Quaternion orientationWorld, Vector3 positionMesh) + { + ruler.transform.rotation = orientationWorld; + ruler.transform.position = + worldSpace.ModelToWorld(heldMesh.fillStartPosition + heldMesh.fillStartRotation * positionMesh); + } - /// - /// Moves the volumeMesh so that its position in world space, after scaling, is locked at the lockedCorner. - /// - /// The position of the volumeMesh before scaling. - /// The extents of the bounding box around the volumeMesh before scaling. - /// The extents of the bounding box around the volumeMesh after scaling. - private Vector3 ResetVolumePosition(Vector3 previousPosition, Vector3 previousExtents, Vector3 finalExtents) { - Vector3 delta = finalExtents - previousExtents; - Vector3 identityPosition = new Vector3(); + /// + /// Corrects the diagonal of a rectangular prism such that it produces a primitive volume with "correct" + /// x, y, and z ratios, meaning ratios that produce a uniformly scaled primitive when being used in volume insertion; + /// for example, click and dragging a cube will only produce a perfect cube. + /// All primitives except the torus fit correctly in a 1x1x1 rectangular prism; the torus fits in + /// a 4x1x4 prism, and the diagonal must be scaled accordingly. + /// + private Vector3 CorrectRatio(Vector3 diagonal) + { + float maxExtent = Mathf.Max(diagonal.x, diagonal.y, diagonal.z); + bool isTorus = (Primitives.Shape)peltzerController.shapesMenu.CurrentItemId == Primitives.Shape.TORUS; + return new Vector3(maxExtent, isTorus ? maxExtent * 0.25f : maxExtent, maxExtent); + } - identityPosition.x = (lockedCorner.x == 1) ? previousPosition.x - delta.x : previousPosition.x + delta.x; - identityPosition.y = (lockedCorner.y == 1) ? previousPosition.y - delta.y : previousPosition.y + delta.y; - identityPosition.z = (lockedCorner.z == 1) ? previousPosition.z - delta.z : previousPosition.z + delta.z; + /// + /// Finds the diagonal for the rectangular prism that the volumeMesh should fill that fills in grid units. + /// + /// Whether the diagonal should be corrected before being returned so that + /// it produces a volume with uniform x, y, and z ratios. + /// + /// The diagonal for the rectangular prism that is snapped to the grid and not smaller than the simple mesh. + /// + private Vector3 GetGridFillDiagonal(bool uniformScale) + { + Vector3 diagonal = peltzerController.LastPositionModel - controllerStartPosition; + + // We can't work out the 'rotation' of a group of meshes yet, so we pick one at random. + diagonal = Quaternion.Inverse(GetFirstHeldMesh().fillStartRotation) * diagonal; + + // Find the controller delta in grid units, ensuring that the rectangular prism is not smaller than + // the bounds of the original insertion. + // We remove the original size of the bounding box: a user must move as far as the bounding box on + // any given axis to begin filling. + diagonal.x = Mathf.Max(0, GridUtils.SnapToGrid(Mathf.Abs(diagonal.x) - extentsOfHeldMeshes.x)); + diagonal.y = Mathf.Max(0, GridUtils.SnapToGrid(Mathf.Abs(diagonal.y) - extentsOfHeldMeshes.y)); + diagonal.z = Mathf.Max(0, GridUtils.SnapToGrid(Mathf.Abs(diagonal.z) - extentsOfHeldMeshes.z)); + + return uniformScale ? CorrectRatio(diagonal) : diagonal; + } - Vector3 shift = identityPosition - previousPosition; - // We can't work out the 'rotation' of a group of meshes yet, so we pick one at random. - Vector3 rotatedShift = GetFirstHeldMesh().fillStartRotation * shift; + /// + /// Finds the diagonal for the rectangular prism that the volumeMesh should fill that ensures a smooth + /// fill effect. + /// + /// Whether the diagonal should be corrected before being returned so that + /// it produces a volume with uniform x, y, and z ratios. + /// + /// The diagonal for the rectangular prism that is NOT NECESSARILY snapped to the grid + /// and not smaller than the simple mesh. + /// + private Vector3 GetSmoothFillDiagonal(bool uniformScale) + { + Vector3 diagonal = peltzerController.LastPositionModel - controllerStartPosition; + + // We can't work out the 'rotation' of a group of meshes yet, so we pick one at random. + diagonal = Quaternion.Inverse(GetFirstHeldMesh().fillStartRotation) * diagonal; + + // We remove the original size of the bounding box: a user must move as far as the bounding box on + // any given axis to begin filling. + diagonal.x = Mathf.Max(0, Mathf.Abs(diagonal.x) - extentsOfHeldMeshes.x); + diagonal.y = Mathf.Max(0, Mathf.Abs(diagonal.y) - extentsOfHeldMeshes.y); + diagonal.z = Mathf.Max(0, Mathf.Abs(diagonal.z) - extentsOfHeldMeshes.z); + + return uniformScale ? CorrectRatio(diagonal) : diagonal; + } - Vector3 rotatedPosition = previousPosition + (rotatedShift.normalized - * Vector3.Distance(previousPosition, identityPosition)); + /// + /// Determines what quadrant a vector is pointing towards. + /// + /// The vector to check. + /// A Vector3 representing the quadrant. + private Vector3 GetQuadrant(Vector3 diagonal) + { + // We can't work out the 'rotation' of a group of meshes yet, so we pick one at random. + diagonal = Quaternion.Inverse(GetFirstHeldMesh().fillStartRotation) * diagonal; + Vector3 quadrant = new Vector3(); + + quadrant.x = (diagonal.x >= 0) ? 1 : -1; + quadrant.y = (diagonal.y >= 0) ? 1 : -1; + quadrant.z = (diagonal.z >= 0) ? 1 : -1; + + return quadrant; + } - return rotatedPosition; - } + /// + /// Moves the volumeMesh so that its position in world space, after scaling, is locked at the lockedCorner. + /// + /// The position of the volumeMesh before scaling. + /// The extents of the bounding box around the volumeMesh before scaling. + /// The extents of the bounding box around the volumeMesh after scaling. + private Vector3 ResetVolumePosition(Vector3 previousPosition, Vector3 previousExtents, Vector3 finalExtents) + { + Vector3 delta = finalExtents - previousExtents; + Vector3 identityPosition = new Vector3(); + + identityPosition.x = (lockedCorner.x == 1) ? previousPosition.x - delta.x : previousPosition.x + delta.x; + identityPosition.y = (lockedCorner.y == 1) ? previousPosition.y - delta.y : previousPosition.y + delta.y; + identityPosition.z = (lockedCorner.z == 1) ? previousPosition.z - delta.z : previousPosition.z + delta.z; + + Vector3 shift = identityPosition - previousPosition; + // We can't work out the 'rotation' of a group of meshes yet, so we pick one at random. + Vector3 rotatedShift = GetFirstHeldMesh().fillStartRotation * shift; + + Vector3 rotatedPosition = previousPosition + (rotatedShift.normalized + * Vector3.Distance(previousPosition, identityPosition)); + + return rotatedPosition; + } - /// - /// Hides all held meshes from view by deactivating their previews. - /// - public void Hide() { - if (!isHidden) { - isHidden = true; - foreach (HeldMesh heldMesh in heldMeshes) { - heldMesh.Preview.SetActive(false); + /// + /// Hides all held meshes from view by deactivating their previews. + /// + public void Hide() + { + if (!isHidden) + { + isHidden = true; + foreach (HeldMesh heldMesh in heldMeshes) + { + heldMesh.Preview.SetActive(false); + } + } } - } - } - /// - /// Unhides all held meshes. Note that this does not set the position or rotation of the held meshes, - /// which may not have been updated whilst the meshes were hidden. - /// - public void Unhide() { - if (isHidden) { - isHidden = false; - foreach (HeldMesh heldMesh in heldMeshes) { - heldMesh.Preview.SetActive(true); + /// + /// Unhides all held meshes. Note that this does not set the position or rotation of the held meshes, + /// which may not have been updated whilst the meshes were hidden. + /// + public void Unhide() + { + if (isHidden) + { + isHidden = false; + foreach (HeldMesh heldMesh in heldMeshes) + { + heldMesh.Preview.SetActive(true); + } + } } - } - } - /// - /// Finds the lockedCorner based on the quadrant the diagonal of the rectangular prism is pointing towards. - /// - /// The inverse of the quadrant which represents the lockedCorner. - private Vector3 FindLockedCorner() { - return currentQuadrant * -1; - } + /// + /// Finds the lockedCorner based on the quadrant the diagonal of the rectangular prism is pointing towards. + /// + /// The inverse of the quadrant which represents the lockedCorner. + private Vector3 FindLockedCorner() + { + return currentQuadrant * -1; + } - public HeldMesh GetFirstHeldMesh() { - return heldMeshes[0]; - } + public HeldMesh GetFirstHeldMesh() + { + return heldMeshes[0]; + } - public IEnumerable GetMeshes() { - return heldMeshes.Select(h => h.Mesh); - } + public IEnumerable GetMeshes() + { + return heldMeshes.Select(h => h.Mesh); + } - public IEnumerable GetMeshIds() { - return heldMeshes.Select(h => h.Mesh.id); - } + public IEnumerable GetMeshIds() + { + return heldMeshes.Select(h => h.Mesh.id); + } - public void DestroyPreviews() { - ruler.SetActive(false); - paletteRuler.SetActive(false); - foreach (HeldMesh heldMesh in heldMeshes) { - DestroyImmediate(heldMesh.Preview); - } - if (currentFaceSnapEffect != null) { - currentFaceSnapEffect.Finish(); - currentFaceSnapEffect = null; - } + public void DestroyPreviews() + { + ruler.SetActive(false); + paletteRuler.SetActive(false); + foreach (HeldMesh heldMesh in heldMeshes) + { + DestroyImmediate(heldMesh.Preview); + } + if (currentFaceSnapEffect != null) + { + currentFaceSnapEffect.Finish(); + currentFaceSnapEffect = null; + } + } } - } } diff --git a/Assets/Scripts/tools/utils/HighlightStyles/EdgeInactiveStyle.cs b/Assets/Scripts/tools/utils/HighlightStyles/EdgeInactiveStyle.cs index 75f90dd6..b221124e 100644 --- a/Assets/Scripts/tools/utils/HighlightStyles/EdgeInactiveStyle.cs +++ b/Assets/Scripts/tools/utils/HighlightStyles/EdgeInactiveStyle.cs @@ -19,71 +19,76 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// This class exists primarily to hold the static method for RenderEdges when INACTIVE is set. It may be possible to - /// consolidate this with the other Edge*Style classes in the future. - /// - public class EdgeInactiveStyle { +namespace com.google.apps.peltzer.client.tools.utils +{ + /// + /// This class exists primarily to hold the static method for RenderEdges when INACTIVE is set. It may be possible to + /// consolidate this with the other Edge*Style classes in the future. + /// + public class EdgeInactiveStyle + { - public static Material material; - public static Vector3 selectPositionModel; - private static Mesh edgeRenderMesh = new Mesh(); + public static Material material; + public static Vector3 selectPositionModel; + private static Mesh edgeRenderMesh = new Mesh(); - // Renders edge highlights. - // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing - // edge geometry frame to frame) - 37281287 - public static void RenderEdges(Model model, - HighlightUtils.TrackedHighlightSet edgeHighlights, - WorldSpace worldSpace) { - HashSet keys = edgeHighlights.getKeysForStyle((int)EdgeStyles.EDGE_INACTIVE); - if (keys.Count == 0) { return; } - edgeRenderMesh.Clear(); - int[] indices = new int[edgeHighlights.RenderableCount() * 2]; - Vector3[] vertices = new Vector3[edgeHighlights.RenderableCount() * 2]; - // Because Unity does not make a "arbitrary data" vertex channel available to us, we're going to abuse the UV - // channel to pass per-vertex animation state into the shader. - Vector2[] selectData = new Vector2[edgeHighlights.RenderableCount() * 2]; - Vector3[] normals = new Vector3[edgeHighlights.RenderableCount() * 2]; - //TODO(bug): setup connectivity info so that we can use correct normals from adjacent faces - Vector3 normal = new Vector3(0f, 1f, 0f); - int i = 0; + // Renders edge highlights. + // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing + // edge geometry frame to frame) - 37281287 + public static void RenderEdges(Model model, + HighlightUtils.TrackedHighlightSet edgeHighlights, + WorldSpace worldSpace) + { + HashSet keys = edgeHighlights.getKeysForStyle((int)EdgeStyles.EDGE_INACTIVE); + if (keys.Count == 0) { return; } + edgeRenderMesh.Clear(); + int[] indices = new int[edgeHighlights.RenderableCount() * 2]; + Vector3[] vertices = new Vector3[edgeHighlights.RenderableCount() * 2]; + // Because Unity does not make a "arbitrary data" vertex channel available to us, we're going to abuse the UV + // channel to pass per-vertex animation state into the shader. + Vector2[] selectData = new Vector2[edgeHighlights.RenderableCount() * 2]; + Vector3[] normals = new Vector3[edgeHighlights.RenderableCount() * 2]; + //TODO(bug): setup connectivity info so that we can use correct normals from adjacent faces + Vector3 normal = new Vector3(0f, 1f, 0f); + int i = 0; - float radius2 = InactiveSelectionHighlighter.INACTIVE_HIGHLIGHT_RADIUS * - InactiveSelectionHighlighter.INACTIVE_HIGHLIGHT_RADIUS; - foreach (EdgeKey key in keys) { - if (!model.HasMesh(key.meshId)) { continue; } - MMesh mesh = model.GetMesh(key.meshId); - if (!mesh.HasVertex(key.vertexId1) || !mesh.HasVertex(key.vertexId2)) continue; - vertices[i] = mesh.VertexPositionInModelCoords(key.vertexId1); - Vector3 diff = vertices[i] - selectPositionModel; - float dist2 = Vector3.Dot(diff, diff); - float alpha = Mathf.Clamp((radius2 - dist2) / radius2, 0f, 1f); + float radius2 = InactiveSelectionHighlighter.INACTIVE_HIGHLIGHT_RADIUS * + InactiveSelectionHighlighter.INACTIVE_HIGHLIGHT_RADIUS; + foreach (EdgeKey key in keys) + { + if (!model.HasMesh(key.meshId)) { continue; } + MMesh mesh = model.GetMesh(key.meshId); + if (!mesh.HasVertex(key.vertexId1) || !mesh.HasVertex(key.vertexId2)) continue; + vertices[i] = mesh.VertexPositionInModelCoords(key.vertexId1); + Vector3 diff = vertices[i] - selectPositionModel; + float dist2 = Vector3.Dot(diff, diff); + float alpha = Mathf.Clamp((radius2 - dist2) / radius2, 0f, 1f); - indices[i] = i; - float animPct = edgeHighlights.GetAnimPct(key); - selectData[i] = new Vector2(animPct, alpha); - normals[i] = normal; - i++; - vertices[i] = mesh.VertexPositionInModelCoords(key.vertexId2); - diff = vertices[i] - selectPositionModel; - dist2 = Vector3.Dot(diff, diff); - alpha = Mathf.Clamp((radius2 - dist2) / radius2, 0f, 1f); - indices[i] = i; - selectData[i] = new Vector2(animPct, alpha); - normals[i] = normal; - i++; - } - edgeRenderMesh.vertices = vertices; - // These are not actually UVs - we're using the UV channel to pass per-primitive animation data so that edges - // animate independently. - edgeRenderMesh.uv = selectData; - // Since we're using a line geometry shader we need to set the mesh up to supply data as lines. - edgeRenderMesh.SetIndices(indices, MeshTopology.Lines, 0 /* submesh id */, false /* recalculate bounds */); - if (edgeHighlights.RenderableCount() > 0) { - Graphics.DrawMesh(edgeRenderMesh, worldSpace.modelToWorld, material, 0); - } + indices[i] = i; + float animPct = edgeHighlights.GetAnimPct(key); + selectData[i] = new Vector2(animPct, alpha); + normals[i] = normal; + i++; + vertices[i] = mesh.VertexPositionInModelCoords(key.vertexId2); + diff = vertices[i] - selectPositionModel; + dist2 = Vector3.Dot(diff, diff); + alpha = Mathf.Clamp((radius2 - dist2) / radius2, 0f, 1f); + indices[i] = i; + selectData[i] = new Vector2(animPct, alpha); + normals[i] = normal; + i++; + } + edgeRenderMesh.vertices = vertices; + // These are not actually UVs - we're using the UV channel to pass per-primitive animation data so that edges + // animate independently. + edgeRenderMesh.uv = selectData; + // Since we're using a line geometry shader we need to set the mesh up to supply data as lines. + edgeRenderMesh.SetIndices(indices, MeshTopology.Lines, 0 /* submesh id */, false /* recalculate bounds */); + if (edgeHighlights.RenderableCount() > 0) + { + Graphics.DrawMesh(edgeRenderMesh, worldSpace.modelToWorld, material, 0); + } + } } - } } diff --git a/Assets/Scripts/tools/utils/HighlightStyles/EdgeSelectStyle.cs b/Assets/Scripts/tools/utils/HighlightStyles/EdgeSelectStyle.cs index 04ff19d2..9596e173 100644 --- a/Assets/Scripts/tools/utils/HighlightStyles/EdgeSelectStyle.cs +++ b/Assets/Scripts/tools/utils/HighlightStyles/EdgeSelectStyle.cs @@ -19,61 +19,66 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// This class exists primarily to hold the static method for RenderEdges when SELECT is set. It may be possible to - /// consolidate this with the other Vertes*Style classes in the future. - /// - public class EdgeSelectStyle { +namespace com.google.apps.peltzer.client.tools.utils +{ + /// + /// This class exists primarily to hold the static method for RenderEdges when SELECT is set. It may be possible to + /// consolidate this with the other Vertes*Style classes in the future. + /// + public class EdgeSelectStyle + { - public static Material material; - private static Mesh edgeRenderMesh = new Mesh(); - // Renders edge highlights. - // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing - // edge geometry frame to frame) - 37281287 - public static void RenderEdges(Model model, - HighlightUtils.TrackedHighlightSet edgeHighlights, - WorldSpace worldSpace) { - HashSet keys = edgeHighlights.getKeysForStyle((int)EdgeStyles.EDGE_SELECT); - if (keys.Count == 0) { return; } - edgeRenderMesh.Clear(); - int[] indices = new int[edgeHighlights.RenderableCount() * 2]; - Vector3[] vertices = new Vector3[edgeHighlights.RenderableCount() * 2]; - // Because Unity does not make a "arbitrary data" vertex channel available to us, we're going to abuse the UV - // channel to pass per-vertex animation state into the shader. - Vector2[] selectData = new Vector2[edgeHighlights.RenderableCount() * 2]; - Vector3[] normals = new Vector3[edgeHighlights.RenderableCount() * 2]; - float scaleFactor = InactiveRenderer.GetEdgeScaleFactor(worldSpace); - material.SetFloat("_PointSphereRadius", scaleFactor); - //TODO(bug): setup connectivity info so that we can use correct normals from adjacent faces - Vector3 normal = new Vector3(0f, 1f, 0f); - int i = 0; - foreach (EdgeKey key in keys) { - if (!model.HasMesh(key.meshId)) { continue; } - MMesh mesh = model.GetMesh(key.meshId); - if (!mesh.HasVertex(key.vertexId1) || !mesh.HasVertex(key.vertexId2)) continue; - vertices[i] = mesh.VertexPositionInModelCoords(key.vertexId1); - indices[i] = i; - float animPct = edgeHighlights.GetAnimPct(key); - selectData[i] = new Vector2(animPct, 1f); - normals[i] = normal; - i++; - vertices[i] = mesh.VertexPositionInModelCoords(key.vertexId2); - indices[i] = i; - // The second component of this vector isn't used yet. - selectData[i] = new Vector2(animPct, 1f); - normals[i] = normal; - i++; - } - edgeRenderMesh.vertices = vertices; - // These are not actually UVs - we're using the UV channel to pass per-primitive animation data so that edges - // animate independently. - edgeRenderMesh.uv = selectData; - // Since we're using a line geometry shader we need to set the mesh up to supply data as lines. - edgeRenderMesh.SetIndices(indices, MeshTopology.Lines, 0 /* submesh id */, false /* recalculate bounds */); - if (edgeHighlights.RenderableCount() > 0) { - Graphics.DrawMesh(edgeRenderMesh, worldSpace.modelToWorld, material, 0); - } + public static Material material; + private static Mesh edgeRenderMesh = new Mesh(); + // Renders edge highlights. + // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing + // edge geometry frame to frame) - 37281287 + public static void RenderEdges(Model model, + HighlightUtils.TrackedHighlightSet edgeHighlights, + WorldSpace worldSpace) + { + HashSet keys = edgeHighlights.getKeysForStyle((int)EdgeStyles.EDGE_SELECT); + if (keys.Count == 0) { return; } + edgeRenderMesh.Clear(); + int[] indices = new int[edgeHighlights.RenderableCount() * 2]; + Vector3[] vertices = new Vector3[edgeHighlights.RenderableCount() * 2]; + // Because Unity does not make a "arbitrary data" vertex channel available to us, we're going to abuse the UV + // channel to pass per-vertex animation state into the shader. + Vector2[] selectData = new Vector2[edgeHighlights.RenderableCount() * 2]; + Vector3[] normals = new Vector3[edgeHighlights.RenderableCount() * 2]; + float scaleFactor = InactiveRenderer.GetEdgeScaleFactor(worldSpace); + material.SetFloat("_PointSphereRadius", scaleFactor); + //TODO(bug): setup connectivity info so that we can use correct normals from adjacent faces + Vector3 normal = new Vector3(0f, 1f, 0f); + int i = 0; + foreach (EdgeKey key in keys) + { + if (!model.HasMesh(key.meshId)) { continue; } + MMesh mesh = model.GetMesh(key.meshId); + if (!mesh.HasVertex(key.vertexId1) || !mesh.HasVertex(key.vertexId2)) continue; + vertices[i] = mesh.VertexPositionInModelCoords(key.vertexId1); + indices[i] = i; + float animPct = edgeHighlights.GetAnimPct(key); + selectData[i] = new Vector2(animPct, 1f); + normals[i] = normal; + i++; + vertices[i] = mesh.VertexPositionInModelCoords(key.vertexId2); + indices[i] = i; + // The second component of this vector isn't used yet. + selectData[i] = new Vector2(animPct, 1f); + normals[i] = normal; + i++; + } + edgeRenderMesh.vertices = vertices; + // These are not actually UVs - we're using the UV channel to pass per-primitive animation data so that edges + // animate independently. + edgeRenderMesh.uv = selectData; + // Since we're using a line geometry shader we need to set the mesh up to supply data as lines. + edgeRenderMesh.SetIndices(indices, MeshTopology.Lines, 0 /* submesh id */, false /* recalculate bounds */); + if (edgeHighlights.RenderableCount() > 0) + { + Graphics.DrawMesh(edgeRenderMesh, worldSpace.modelToWorld, material, 0); + } + } } - } } diff --git a/Assets/Scripts/tools/utils/HighlightStyles/EdgeTemporaryStyle.cs b/Assets/Scripts/tools/utils/HighlightStyles/EdgeTemporaryStyle.cs index 76b0404d..435c1870 100644 --- a/Assets/Scripts/tools/utils/HighlightStyles/EdgeTemporaryStyle.cs +++ b/Assets/Scripts/tools/utils/HighlightStyles/EdgeTemporaryStyle.cs @@ -19,82 +19,91 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// This class exists primarily to hold the static method for RenderEdges when TEMPORARY is set. It may be possible to - /// consolidate this with the other Edge*Style classes in the future. - /// - public class EdgeTemporaryStyle { - private static int nextId = 0; - private static Mesh edgeRenderMesh = new Mesh(); - public class TemporaryEdge { - public int id; - public Vector3 vertex1PositionModelSpace; - public Vector3 vertex2PositionModelSpace; +namespace com.google.apps.peltzer.client.tools.utils +{ + /// + /// This class exists primarily to hold the static method for RenderEdges when TEMPORARY is set. It may be possible to + /// consolidate this with the other Edge*Style classes in the future. + /// + public class EdgeTemporaryStyle + { + private static int nextId = 0; + private static Mesh edgeRenderMesh = new Mesh(); + public class TemporaryEdge + { + public int id; + public Vector3 vertex1PositionModelSpace; + public Vector3 vertex2PositionModelSpace; - public TemporaryEdge() { - id = nextId++; - } + public TemporaryEdge() + { + id = nextId++; + } - public override bool Equals(object otherObject) { - if (!(otherObject is TemporaryEdge)) - return false; + public override bool Equals(object otherObject) + { + if (!(otherObject is TemporaryEdge)) + return false; - TemporaryEdge other = (TemporaryEdge)otherObject; - return other.id == id; - } + TemporaryEdge other = (TemporaryEdge)otherObject; + return other.id == id; + } - public override int GetHashCode() { - return id; - } - } + public override int GetHashCode() + { + return id; + } + } - public static Material material; + public static Material material; - // Renders edge highlights. - // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing - // edge geometry frame to frame) - 37281287 - public static void RenderEdges(Model model, - HighlightUtils.TrackedHighlightSet temporaryEdgeHighlights, - WorldSpace worldSpace) { - HashSet keys = temporaryEdgeHighlights.getKeysForStyle((int)EdgeStyles.EDGE_SELECT); - if (keys.Count == 0) { return; } - edgeRenderMesh.Clear(); - int[] indices = new int[temporaryEdgeHighlights.RenderableCount() * 2]; - Vector3[] vertices = new Vector3[temporaryEdgeHighlights.RenderableCount() * 2]; - // Because Unity does not make a "arbitrary data" vertex channel available to us, we're going to abuse the UV - // channel to pass per-vertex animation state into the shader. - Vector2[] selectData = new Vector2[temporaryEdgeHighlights.RenderableCount() * 2]; - Vector3[] normals = new Vector3[temporaryEdgeHighlights.RenderableCount() * 2]; - //TODO(bug): setup connectivity info so that we can use correct normals from adjacent faces - Vector3 normal = new Vector3(0f, 1f, 0f); - int i = 0; - float scaleFactor = InactiveRenderer.GetEdgeScaleFactor(worldSpace); - material.SetFloat("_PointSphereRadius", scaleFactor); + // Renders edge highlights. + // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing + // edge geometry frame to frame) - 37281287 + public static void RenderEdges(Model model, + HighlightUtils.TrackedHighlightSet temporaryEdgeHighlights, + WorldSpace worldSpace) + { + HashSet keys = temporaryEdgeHighlights.getKeysForStyle((int)EdgeStyles.EDGE_SELECT); + if (keys.Count == 0) { return; } + edgeRenderMesh.Clear(); + int[] indices = new int[temporaryEdgeHighlights.RenderableCount() * 2]; + Vector3[] vertices = new Vector3[temporaryEdgeHighlights.RenderableCount() * 2]; + // Because Unity does not make a "arbitrary data" vertex channel available to us, we're going to abuse the UV + // channel to pass per-vertex animation state into the shader. + Vector2[] selectData = new Vector2[temporaryEdgeHighlights.RenderableCount() * 2]; + Vector3[] normals = new Vector3[temporaryEdgeHighlights.RenderableCount() * 2]; + //TODO(bug): setup connectivity info so that we can use correct normals from adjacent faces + Vector3 normal = new Vector3(0f, 1f, 0f); + int i = 0; + float scaleFactor = InactiveRenderer.GetEdgeScaleFactor(worldSpace); + material.SetFloat("_PointSphereRadius", scaleFactor); - foreach (TemporaryEdge key in keys) { - vertices[i] = key.vertex1PositionModelSpace; - indices[i] = i; - float animPct = temporaryEdgeHighlights.GetAnimPct(key); - selectData[i] = new Vector2(animPct, 1f); - normals[i] = normal; - i++; - vertices[i] = key.vertex2PositionModelSpace; - indices[i] = i; - // The second component of this vector isn't used yet. - selectData[i] = new Vector2(animPct, 1f); - normals[i] = normal; - i++; - } - edgeRenderMesh.vertices = vertices; - // These are not actually UVs - we're using the UV channel to pass per-primitive animation data so that edges - // animate independently. - edgeRenderMesh.uv = selectData; - // Since we're using a line geometry shader we need to set the mesh up to supply data as lines. - edgeRenderMesh.SetIndices(indices, MeshTopology.Lines, 0 /* submesh id */, false /* recalculate bounds */); - if (temporaryEdgeHighlights.RenderableCount() > 0) { - Graphics.DrawMesh(edgeRenderMesh, worldSpace.modelToWorld, material, 0); - } + foreach (TemporaryEdge key in keys) + { + vertices[i] = key.vertex1PositionModelSpace; + indices[i] = i; + float animPct = temporaryEdgeHighlights.GetAnimPct(key); + selectData[i] = new Vector2(animPct, 1f); + normals[i] = normal; + i++; + vertices[i] = key.vertex2PositionModelSpace; + indices[i] = i; + // The second component of this vector isn't used yet. + selectData[i] = new Vector2(animPct, 1f); + normals[i] = normal; + i++; + } + edgeRenderMesh.vertices = vertices; + // These are not actually UVs - we're using the UV channel to pass per-primitive animation data so that edges + // animate independently. + edgeRenderMesh.uv = selectData; + // Since we're using a line geometry shader we need to set the mesh up to supply data as lines. + edgeRenderMesh.SetIndices(indices, MeshTopology.Lines, 0 /* submesh id */, false /* recalculate bounds */); + if (temporaryEdgeHighlights.RenderableCount() > 0) + { + Graphics.DrawMesh(edgeRenderMesh, worldSpace.modelToWorld, material, 0); + } + } } - } } diff --git a/Assets/Scripts/tools/utils/HighlightStyles/FaceExtrudeStyle.cs b/Assets/Scripts/tools/utils/HighlightStyles/FaceExtrudeStyle.cs index 436fb5aa..fc2824c6 100644 --- a/Assets/Scripts/tools/utils/HighlightStyles/FaceExtrudeStyle.cs +++ b/Assets/Scripts/tools/utils/HighlightStyles/FaceExtrudeStyle.cs @@ -19,83 +19,89 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// This class exists primarily to hold the static method for RenderFaces when FACE_SELECT is set. It may be possible - /// to consolidate this with the other Face*Style classes in the future. - /// - public class FaceExtrudeStyle { - public static Material material; - private static Mesh faceRenderMesh = new Mesh(); - // Renders vertex highlights. - // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing - // face geometry frame to frame) - bug - public static void RenderFaces(Model model, - HighlightUtils.TrackedHighlightSet faceHighlights, - WorldSpace worldSpace) { - HashSet keys = faceHighlights.getKeysForStyle((int)FaceStyles.EXTRUDE); - if (keys.Count == 0) { return; } - faceRenderMesh.Clear(); - List indices = new List(); - List vertices = new List(); - // Because Unity does not make a "arbitrary data" vertex channel available to us, we're going to abuse the UV - // channel to pass per-vertex animation state into the shader. - List selectData = new List(); - List normals = new List(); - List selectPositions = new List(); - List colors = new List(); +namespace com.google.apps.peltzer.client.tools.utils +{ + /// + /// This class exists primarily to hold the static method for RenderFaces when FACE_SELECT is set. It may be possible + /// to consolidate this with the other Face*Style classes in the future. + /// + public class FaceExtrudeStyle + { + public static Material material; + private static Mesh faceRenderMesh = new Mesh(); + // Renders vertex highlights. + // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing + // face geometry frame to frame) - bug + public static void RenderFaces(Model model, + HighlightUtils.TrackedHighlightSet faceHighlights, + WorldSpace worldSpace) + { + HashSet keys = faceHighlights.getKeysForStyle((int)FaceStyles.EXTRUDE); + if (keys.Count == 0) { return; } + faceRenderMesh.Clear(); + List indices = new List(); + List vertices = new List(); + // Because Unity does not make a "arbitrary data" vertex channel available to us, we're going to abuse the UV + // channel to pass per-vertex animation state into the shader. + List selectData = new List(); + List normals = new List(); + List selectPositions = new List(); + List colors = new List(); - int curIndex = 0; - foreach (FaceKey key in keys) { - if (!model.HasMesh(key.meshId)) { continue; } - MMesh mesh = model.GetMesh(key.meshId); - Face curFace; - if (mesh.TryGetFace(key.faceId, out curFace)) { - if (!mesh.HasFace(key.faceId)) continue; - // For each face, add all triangles to the mesh with all per-face data set appropriately. - Vector4 selectPosition = faceHighlights.GetCustomChannel0(key); - float animPct = faceHighlights.GetAnimPct(key); - Vector4 colorV4 = faceHighlights.GetCustomChannel1(key); - Color faceColor = new Color(colorV4.x, colorV4.y, colorV4.z, colorV4.w); - List tris = MeshHelper.TriangulateFace(mesh, curFace); - for (int i = 0; i < tris.Count; i++) { - // For each triangle in the face, add a vertex to the Mesh - vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId0])); - normals.Add(curFace.normal); - selectData.Add(new Vector2(animPct, 0f)); - indices.Add(curIndex); - colors.Add(faceColor); - selectPositions.Add(selectPosition); - curIndex++; + int curIndex = 0; + foreach (FaceKey key in keys) + { + if (!model.HasMesh(key.meshId)) { continue; } + MMesh mesh = model.GetMesh(key.meshId); + Face curFace; + if (mesh.TryGetFace(key.faceId, out curFace)) + { + if (!mesh.HasFace(key.faceId)) continue; + // For each face, add all triangles to the mesh with all per-face data set appropriately. + Vector4 selectPosition = faceHighlights.GetCustomChannel0(key); + float animPct = faceHighlights.GetAnimPct(key); + Vector4 colorV4 = faceHighlights.GetCustomChannel1(key); + Color faceColor = new Color(colorV4.x, colorV4.y, colorV4.z, colorV4.w); + List tris = MeshHelper.TriangulateFace(mesh, curFace); + for (int i = 0; i < tris.Count; i++) + { + // For each triangle in the face, add a vertex to the Mesh + vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId0])); + normals.Add(curFace.normal); + selectData.Add(new Vector2(animPct, 0f)); + indices.Add(curIndex); + colors.Add(faceColor); + selectPositions.Add(selectPosition); + curIndex++; - vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId1])); - normals.Add(curFace.normal); - selectData.Add(new Vector2(animPct, 0f)); - indices.Add(curIndex); - colors.Add(faceColor); - selectPositions.Add(selectPosition); - curIndex++; + vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId1])); + normals.Add(curFace.normal); + selectData.Add(new Vector2(animPct, 0f)); + indices.Add(curIndex); + colors.Add(faceColor); + selectPositions.Add(selectPosition); + curIndex++; - vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId2])); - normals.Add(curFace.normal); - selectData.Add(new Vector2(animPct, 0f)); - indices.Add(curIndex); - colors.Add(faceColor); - selectPositions.Add(selectPosition); - curIndex++; - } - } - } - faceRenderMesh.SetVertices(vertices); - // These are not actually UVs - we're using the UV channel to pass per-primitive animation data so that edges - // animate independently. - faceRenderMesh.SetUVs(/* channel */ 0, selectData); - faceRenderMesh.SetNormals(normals); - faceRenderMesh.SetTriangles(indices, /* subMesh */ 0); - faceRenderMesh.SetColors(colors); - faceRenderMesh.SetTangents(selectPositions); + vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId2])); + normals.Add(curFace.normal); + selectData.Add(new Vector2(animPct, 0f)); + indices.Add(curIndex); + colors.Add(faceColor); + selectPositions.Add(selectPosition); + curIndex++; + } + } + } + faceRenderMesh.SetVertices(vertices); + // These are not actually UVs - we're using the UV channel to pass per-primitive animation data so that edges + // animate independently. + faceRenderMesh.SetUVs(/* channel */ 0, selectData); + faceRenderMesh.SetNormals(normals); + faceRenderMesh.SetTriangles(indices, /* subMesh */ 0); + faceRenderMesh.SetColors(colors); + faceRenderMesh.SetTangents(selectPositions); - Graphics.DrawMesh(faceRenderMesh, worldSpace.modelToWorld, material, 0); + Graphics.DrawMesh(faceRenderMesh, worldSpace.modelToWorld, material, 0); + } } - } } diff --git a/Assets/Scripts/tools/utils/HighlightStyles/FacePaintStyle.cs b/Assets/Scripts/tools/utils/HighlightStyles/FacePaintStyle.cs index 7a719ea5..80a73b68 100644 --- a/Assets/Scripts/tools/utils/HighlightStyles/FacePaintStyle.cs +++ b/Assets/Scripts/tools/utils/HighlightStyles/FacePaintStyle.cs @@ -19,82 +19,88 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// This class exists primarily to hold the static method for RenderFaces when PAINT is set. It may be possible to - /// consolidate this with the other Face*Style classes in the future. - /// - public class FacePaintStyle { - public static Material material; - private static Mesh faceRenderMesh = new Mesh(); - // Renders vertex highlights. - // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing - // face geometry frame to frame) - 37281287 - public static void RenderFaces(Model model, - HighlightUtils.TrackedHighlightSet faceHighlights, - WorldSpace worldSpace) { - HashSet keys = faceHighlights.getKeysForStyle((int)FaceStyles.PAINT); - if (keys.Count == 0) { return; } - faceRenderMesh.Clear(); - List indices = new List(); - List vertices = new List(); - // Because Unity does not make a "arbitrary data" vertex channel available to us, we're going to abuse the UV - // channel to pass per-vertex animation state into the shader. - List selectData = new List(); - List normals = new List(); - List selectPositions = new List(); - List colors = new List(); +namespace com.google.apps.peltzer.client.tools.utils +{ + /// + /// This class exists primarily to hold the static method for RenderFaces when PAINT is set. It may be possible to + /// consolidate this with the other Face*Style classes in the future. + /// + public class FacePaintStyle + { + public static Material material; + private static Mesh faceRenderMesh = new Mesh(); + // Renders vertex highlights. + // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing + // face geometry frame to frame) - 37281287 + public static void RenderFaces(Model model, + HighlightUtils.TrackedHighlightSet faceHighlights, + WorldSpace worldSpace) + { + HashSet keys = faceHighlights.getKeysForStyle((int)FaceStyles.PAINT); + if (keys.Count == 0) { return; } + faceRenderMesh.Clear(); + List indices = new List(); + List vertices = new List(); + // Because Unity does not make a "arbitrary data" vertex channel available to us, we're going to abuse the UV + // channel to pass per-vertex animation state into the shader. + List selectData = new List(); + List normals = new List(); + List selectPositions = new List(); + List colors = new List(); - int curIndex = 0; - foreach (FaceKey key in keys) { - if (!model.HasMesh(key.meshId)) { continue; } - MMesh mesh = model.GetMesh(key.meshId); - Face curFace; - if (mesh.TryGetFace(key.faceId, out curFace)) { - // For each face, add all triangles to the mesh with all per-face data set appropriately. - Vector4 selectPosition = faceHighlights.GetCustomChannel0(key); - float animPct = faceHighlights.GetAnimPct(key); - Vector4 colorV4 = faceHighlights.GetCustomChannel1(key); - Color faceColor = new Color(colorV4.x, colorV4.y, colorV4.z, colorV4.w); - List tris = MeshHelper.TriangulateFace(mesh, curFace); - for (int i = 0; i < tris.Count; i++) { - // For each triangle in the face, add a vertex to the Mesh - vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId0])); - normals.Add(curFace.normal); - selectData.Add(new Vector2(animPct, 0f)); - indices.Add(curIndex); - colors.Add(faceColor); - selectPositions.Add(selectPosition); - curIndex++; + int curIndex = 0; + foreach (FaceKey key in keys) + { + if (!model.HasMesh(key.meshId)) { continue; } + MMesh mesh = model.GetMesh(key.meshId); + Face curFace; + if (mesh.TryGetFace(key.faceId, out curFace)) + { + // For each face, add all triangles to the mesh with all per-face data set appropriately. + Vector4 selectPosition = faceHighlights.GetCustomChannel0(key); + float animPct = faceHighlights.GetAnimPct(key); + Vector4 colorV4 = faceHighlights.GetCustomChannel1(key); + Color faceColor = new Color(colorV4.x, colorV4.y, colorV4.z, colorV4.w); + List tris = MeshHelper.TriangulateFace(mesh, curFace); + for (int i = 0; i < tris.Count; i++) + { + // For each triangle in the face, add a vertex to the Mesh + vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId0])); + normals.Add(curFace.normal); + selectData.Add(new Vector2(animPct, 0f)); + indices.Add(curIndex); + colors.Add(faceColor); + selectPositions.Add(selectPosition); + curIndex++; - vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId1])); - normals.Add(curFace.normal); - selectData.Add(new Vector2(animPct, 0f)); - indices.Add(curIndex); - colors.Add(faceColor); - selectPositions.Add(selectPosition); - curIndex++; + vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId1])); + normals.Add(curFace.normal); + selectData.Add(new Vector2(animPct, 0f)); + indices.Add(curIndex); + colors.Add(faceColor); + selectPositions.Add(selectPosition); + curIndex++; - vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId2])); - normals.Add(curFace.normal); - selectData.Add(new Vector2(animPct, 0f)); - indices.Add(curIndex); - colors.Add(faceColor); - selectPositions.Add(selectPosition); - curIndex++; - } - } - } - faceRenderMesh.SetVertices(vertices); - // These are not actually UVs - we're using the UV channel to pass per-primitive animation data so that edges - // animate independently. - faceRenderMesh.SetUVs(/* channel */ 0, selectData); - faceRenderMesh.SetNormals(normals); - faceRenderMesh.SetTriangles(indices, /* subMesh */ 0); - faceRenderMesh.SetColors(colors); - faceRenderMesh.SetTangents(selectPositions); + vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId2])); + normals.Add(curFace.normal); + selectData.Add(new Vector2(animPct, 0f)); + indices.Add(curIndex); + colors.Add(faceColor); + selectPositions.Add(selectPosition); + curIndex++; + } + } + } + faceRenderMesh.SetVertices(vertices); + // These are not actually UVs - we're using the UV channel to pass per-primitive animation data so that edges + // animate independently. + faceRenderMesh.SetUVs(/* channel */ 0, selectData); + faceRenderMesh.SetNormals(normals); + faceRenderMesh.SetTriangles(indices, /* subMesh */ 0); + faceRenderMesh.SetColors(colors); + faceRenderMesh.SetTangents(selectPositions); - Graphics.DrawMesh(faceRenderMesh, worldSpace.modelToWorld, material, 0); + Graphics.DrawMesh(faceRenderMesh, worldSpace.modelToWorld, material, 0); + } } - } } diff --git a/Assets/Scripts/tools/utils/HighlightStyles/FaceSelectStyle.cs b/Assets/Scripts/tools/utils/HighlightStyles/FaceSelectStyle.cs index 881fb450..9eba38b2 100644 --- a/Assets/Scripts/tools/utils/HighlightStyles/FaceSelectStyle.cs +++ b/Assets/Scripts/tools/utils/HighlightStyles/FaceSelectStyle.cs @@ -19,84 +19,90 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// This class exists primarily to hold the static method for RenderFaces when EXTRUDE is set. It may be possible to - /// consolidate this with the other Face*Style classes in the future. - /// - public class FaceSelectStyle { +namespace com.google.apps.peltzer.client.tools.utils +{ + /// + /// This class exists primarily to hold the static method for RenderFaces when EXTRUDE is set. It may be possible to + /// consolidate this with the other Face*Style classes in the future. + /// + public class FaceSelectStyle + { - public static Material material; - private static Mesh faceRenderMesh = new Mesh(); - // Renders vertex highlights. - // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing - // face geometry frame to frame) - 37281287 - public static void RenderFaces(Model model, - HighlightUtils.TrackedHighlightSet faceHighlights, - WorldSpace worldSpace) { - HashSet keys = faceHighlights.getKeysForStyle((int)FaceStyles.FACE_SELECT); - if (keys.Count == 0) { return; } + public static Material material; + private static Mesh faceRenderMesh = new Mesh(); + // Renders vertex highlights. + // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing + // face geometry frame to frame) - 37281287 + public static void RenderFaces(Model model, + HighlightUtils.TrackedHighlightSet faceHighlights, + WorldSpace worldSpace) + { + HashSet keys = faceHighlights.getKeysForStyle((int)FaceStyles.FACE_SELECT); + if (keys.Count == 0) { return; } - faceRenderMesh.Clear(); - List indices = new List(); - List vertices = new List(); - // Because Unity does not make a "arbitrary data" vertex channel available to us, we're going to abuse the UV - // channel to pass per-vertex animation state into the shader. - List selectData = new List(); - List normals = new List(); - List selectPositions = new List(); - List colors = new List(); + faceRenderMesh.Clear(); + List indices = new List(); + List vertices = new List(); + // Because Unity does not make a "arbitrary data" vertex channel available to us, we're going to abuse the UV + // channel to pass per-vertex animation state into the shader. + List selectData = new List(); + List normals = new List(); + List selectPositions = new List(); + List colors = new List(); - int curIndex = 0; - foreach (FaceKey key in keys) { - if (!model.HasMesh(key.meshId)) { continue; } - MMesh mesh = model.GetMesh(key.meshId); - Face curFace; - if (mesh.TryGetFace(key.faceId, out curFace)) { - // For each face, add all triangles to the mesh with all per-face data set appropriately. - Vector4 selectPosition = faceHighlights.GetCustomChannel0(key); - float animPct = faceHighlights.GetAnimPct(key); - Vector4 colorV4 = faceHighlights.GetCustomChannel1(key); - Color faceColor = new Color(colorV4.x, colorV4.y, colorV4.z, colorV4.w); - List tris = MeshHelper.TriangulateFace(mesh, curFace); - for (int i = 0; i < tris.Count; i++) { - // For each triangle in the face, add a vertex to the Mesh - vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId0])); - normals.Add(curFace.normal); - selectData.Add(new Vector2(animPct, 0f)); - indices.Add(curIndex); - colors.Add(faceColor); - selectPositions.Add(selectPosition); - curIndex++; + int curIndex = 0; + foreach (FaceKey key in keys) + { + if (!model.HasMesh(key.meshId)) { continue; } + MMesh mesh = model.GetMesh(key.meshId); + Face curFace; + if (mesh.TryGetFace(key.faceId, out curFace)) + { + // For each face, add all triangles to the mesh with all per-face data set appropriately. + Vector4 selectPosition = faceHighlights.GetCustomChannel0(key); + float animPct = faceHighlights.GetAnimPct(key); + Vector4 colorV4 = faceHighlights.GetCustomChannel1(key); + Color faceColor = new Color(colorV4.x, colorV4.y, colorV4.z, colorV4.w); + List tris = MeshHelper.TriangulateFace(mesh, curFace); + for (int i = 0; i < tris.Count; i++) + { + // For each triangle in the face, add a vertex to the Mesh + vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId0])); + normals.Add(curFace.normal); + selectData.Add(new Vector2(animPct, 0f)); + indices.Add(curIndex); + colors.Add(faceColor); + selectPositions.Add(selectPosition); + curIndex++; - vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId1])); - normals.Add(curFace.normal); - selectData.Add(new Vector2(animPct, 0f)); - indices.Add(curIndex); - colors.Add(faceColor); - selectPositions.Add(selectPosition); - curIndex++; + vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId1])); + normals.Add(curFace.normal); + selectData.Add(new Vector2(animPct, 0f)); + indices.Add(curIndex); + colors.Add(faceColor); + selectPositions.Add(selectPosition); + curIndex++; - vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId2])); - normals.Add(curFace.normal); - selectData.Add(new Vector2(animPct, 0f)); - indices.Add(curIndex); - colors.Add(faceColor); - selectPositions.Add(selectPosition); - curIndex++; - } - } - } - faceRenderMesh.SetVertices(vertices); - // These are not actually UVs - we're using the UV channel to pass per-primitive animation data so that edges - // animate independently. - faceRenderMesh.SetUVs(/* channel */ 0, selectData); - faceRenderMesh.SetNormals(normals); - faceRenderMesh.SetTriangles(indices, /* subMesh */ 0); - faceRenderMesh.SetColors(colors); - faceRenderMesh.SetTangents(selectPositions); + vertices.Add(mesh.VertexPositionInModelCoords(curFace.vertexIds[tris[i].vertId2])); + normals.Add(curFace.normal); + selectData.Add(new Vector2(animPct, 0f)); + indices.Add(curIndex); + colors.Add(faceColor); + selectPositions.Add(selectPosition); + curIndex++; + } + } + } + faceRenderMesh.SetVertices(vertices); + // These are not actually UVs - we're using the UV channel to pass per-primitive animation data so that edges + // animate independently. + faceRenderMesh.SetUVs(/* channel */ 0, selectData); + faceRenderMesh.SetNormals(normals); + faceRenderMesh.SetTriangles(indices, /* subMesh */ 0); + faceRenderMesh.SetColors(colors); + faceRenderMesh.SetTangents(selectPositions); - Graphics.DrawMesh(faceRenderMesh, worldSpace.modelToWorld, material, 0); + Graphics.DrawMesh(faceRenderMesh, worldSpace.modelToWorld, material, 0); + } } - } } diff --git a/Assets/Scripts/tools/utils/HighlightStyles/InactiveRenderer.cs b/Assets/Scripts/tools/utils/HighlightStyles/InactiveRenderer.cs index 17fe870f..18fb86b0 100644 --- a/Assets/Scripts/tools/utils/HighlightStyles/InactiveRenderer.cs +++ b/Assets/Scripts/tools/utils/HighlightStyles/InactiveRenderer.cs @@ -19,322 +19,359 @@ using com.google.apps.peltzer.client.model.main; using UnityEngine; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// This class handles rendering of inactive meshes, caching as much as it can frame to frame. - /// - public class InactiveRenderer { - private static float SCALE_THRESH = 1f; - private const int MAX_INDEX_COUNT = 64000; - - private class StaticCachedRenderMesh { - public Vector3[] vertices; - public Vector3[] normals; - public List indices; - public int numElements; - public bool dirty; - public Mesh mesh; - - public StaticCachedRenderMesh() { - mesh = new Mesh(); - vertices = new Vector3[MAX_INDEX_COUNT]; - normals = new Vector3[MAX_INDEX_COUNT]; - indices = new List(MAX_INDEX_COUNT); - numElements = 0; - dirty = false; - vertices[0] = new Vector3(999999, 999999, 999999); - vertices[1] = new Vector3(-999999, -999999, -999999); - mesh.vertices = vertices; - // Make sure this never gets culled - mesh.RecalculateBounds(); - } - - public void Clear() { - Array.Clear(vertices, 0, vertices.Length); - Array.Clear(normals, 0, normals.Length); - indices.Clear(); - numElements = 0; - dirty = false; - } - } - - - private Model model; - private WorldSpace worldSpace; - public Material inactiveEdgeMaterial; - public Material inactivePointMaterial; - private Vector3 selectPositionWorld; - - /// - /// Sets whether the inactive renderer should render inactive vertices. - /// - public bool showPoints { get; set; } - - /// - /// Determines whether the inactive renderer should render inactive edges. - /// - public bool showEdges { get; set; } - - private static float baseVertexScale; - private static float baseEdgeScale; - - public InactiveRenderer(Model model, WorldSpace worldSpace, MaterialLibrary materialLibrary) { - this.model = model; - this.worldSpace = worldSpace; - availableEdgeMeshes = new List(); - edgeMeshes = new List(); - meshesInEdgeMeshes = new HashSet(); - availablePointMeshes = new List(); - pointMeshes = new List(); - meshesInPointMeshes = new HashSet(); - inactiveEdgeMaterial = new Material(materialLibrary.edgeInactiveMaterial); - inactivePointMaterial = new Material(materialLibrary.pointInactiveMaterial); - baseVertexScale = inactivePointMaterial.GetFloat("_PointSphereRadius"); - baseEdgeScale = inactiveEdgeMaterial.GetFloat("_PointSphereRadius"); - } - - private List availableEdgeMeshes; - private List edgeMeshes; - private HashSet meshesInEdgeMeshes; - - private List availablePointMeshes; - private List pointMeshes; - private HashSet meshesInPointMeshes; - +namespace com.google.apps.peltzer.client.tools.utils +{ /// - /// Returns the scale factor used for rendering inactive vertices - used by selector to make sure selection radii - /// match with what the user sees. + /// This class handles rendering of inactive meshes, caching as much as it can frame to frame. /// - public static float GetVertScaleFactor(WorldSpace worldSpace) { - return (Mathf.Min(worldSpace.scale, SCALE_THRESH) / SCALE_THRESH) * baseVertexScale; - } - - /// - /// Returns the scale factor used for rendering inactive edges - used by selector to make sure selection radii - /// match with what the user sees. - /// - public static float GetEdgeScaleFactor(WorldSpace worldSpace) { - return (Mathf.Min(worldSpace.scale, SCALE_THRESH) / SCALE_THRESH) * baseEdgeScale; - } - - private List edges = new List(); - private HashSet edgeSet = new HashSet(); - - /// - /// Turns on edge wireframes for supplied meshes. (Will use cached data if a mesh has been passed to this method - /// since the most recent clear.) - /// - public void TurnOnEdgeWireframe(IEnumerable meshIds) { - edges.Clear(); - edgeSet.Clear(); - foreach (int meshId in meshIds) { - if (meshesInEdgeMeshes.Contains(meshId)) continue; - - if (!model.HasMesh(meshId)) continue; - - MMesh polyMesh = model.GetMesh(meshId); - - foreach (Face curFace in polyMesh.GetFaces()) { - for (int i = 0; i < curFace.vertexIds.Count; i++) { - edgeSet.Add(new EdgeKey(meshId, curFace.vertexIds[i], curFace.vertexIds[(i + 1) % curFace.vertexIds.Count])); - } - } - meshesInEdgeMeshes.Add(meshId); - } - edges = edgeSet.ToList(); - int curEdge = 0; - while (curEdge < edges.Count) { - StaticCachedRenderMesh curMesh = GetCurEdgeMesh(); - int curMeshStartingIndex = curMesh.numElements; - int edgesSpaceStillNeeded = (edges.Count - curEdge) * 2; - int edgeSpaceLeftInCurMesh = MAX_INDEX_COUNT - curMesh.numElements; - int edgesToPutInCurMesh = Mathf.Min(edgesSpaceStillNeeded, edgeSpaceLeftInCurMesh); - int curMeshIndex = curMeshStartingIndex; - curMesh.dirty = curMesh.dirty || edgesToPutInCurMesh > 0; - for(int i = curEdge; i < edgesToPutInCurMesh/2 + curEdge; i ++) { - int index = curMeshIndex + 2 * (i - curEdge); - if (!model.HasMesh(edges[i].meshId)) continue; - - MMesh polyMesh = model.GetMesh(edges[i].meshId); - - curMesh.vertices[index] = - polyMesh.VertexPositionInModelCoords(edges[i].vertexId1); - curMesh.vertices[index + 1] = - polyMesh.VertexPositionInModelCoords(edges[i].vertexId2); - curMesh.normals[index] = new Vector3(0f, 1f, 0f); - curMesh.normals[index + 1] = new Vector3(0f, 1f, 0f); - - curMesh.indices.Add(index); - curMesh.indices.Add(index + 1); + public class InactiveRenderer + { + private static float SCALE_THRESH = 1f; + private const int MAX_INDEX_COUNT = 64000; + + private class StaticCachedRenderMesh + { + public Vector3[] vertices; + public Vector3[] normals; + public List indices; + public int numElements; + public bool dirty; + public Mesh mesh; + + public StaticCachedRenderMesh() + { + mesh = new Mesh(); + vertices = new Vector3[MAX_INDEX_COUNT]; + normals = new Vector3[MAX_INDEX_COUNT]; + indices = new List(MAX_INDEX_COUNT); + numElements = 0; + dirty = false; + vertices[0] = new Vector3(999999, 999999, 999999); + vertices[1] = new Vector3(-999999, -999999, -999999); + mesh.vertices = vertices; + // Make sure this never gets culled + mesh.RecalculateBounds(); + } + + public void Clear() + { + Array.Clear(vertices, 0, vertices.Length); + Array.Clear(normals, 0, normals.Length); + indices.Clear(); + numElements = 0; + dirty = false; + } } - curEdge += edgesToPutInCurMesh / 2; - curMesh.numElements += edgesToPutInCurMesh; - curMesh.mesh.vertices = curMesh.vertices; - curMesh.mesh.normals = curMesh.normals; - curMesh.mesh.SetIndices(curMesh.indices.ToArray(), MeshTopology.Lines, 0, false /* recalculateBounds */); - } - } - - /// - /// Turns on vertex wireframes for supplied meshes. (Will use cached data if a mesh has been passed to this method - /// since the most recent clear.) - /// - public void TurnOnPointWireframe(IEnumerable meshIds) { - List vertexKeys = new List(); - foreach (int meshId in meshIds) { - if (meshesInPointMeshes.Contains(meshId)) continue; - - if (!model.HasMesh(meshId)) continue; - MMesh polyMesh = model.GetMesh(meshId); - foreach (int vertId in polyMesh.GetVertexIds()) { - vertexKeys.Add(new VertexKey(meshId, vertId)); + private Model model; + private WorldSpace worldSpace; + public Material inactiveEdgeMaterial; + public Material inactivePointMaterial; + private Vector3 selectPositionWorld; + + /// + /// Sets whether the inactive renderer should render inactive vertices. + /// + public bool showPoints { get; set; } + + /// + /// Determines whether the inactive renderer should render inactive edges. + /// + public bool showEdges { get; set; } + + private static float baseVertexScale; + private static float baseEdgeScale; + + public InactiveRenderer(Model model, WorldSpace worldSpace, MaterialLibrary materialLibrary) + { + this.model = model; + this.worldSpace = worldSpace; + availableEdgeMeshes = new List(); + edgeMeshes = new List(); + meshesInEdgeMeshes = new HashSet(); + availablePointMeshes = new List(); + pointMeshes = new List(); + meshesInPointMeshes = new HashSet(); + inactiveEdgeMaterial = new Material(materialLibrary.edgeInactiveMaterial); + inactivePointMaterial = new Material(materialLibrary.pointInactiveMaterial); + baseVertexScale = inactivePointMaterial.GetFloat("_PointSphereRadius"); + baseEdgeScale = inactiveEdgeMaterial.GetFloat("_PointSphereRadius"); } - meshesInPointMeshes.Add(meshId); - } - int curVert = 0; - while (curVert < vertexKeys.Count) { - StaticCachedRenderMesh curMesh = GetCurPointMesh(); - int curMeshStartingIndex = curMesh.numElements; - int vertexSpaceStillNeeded = (vertexKeys.Count - curVert); - int vertSpaceLeftInCurMesh = MAX_INDEX_COUNT - curMesh.numElements; - int numVertsToPutInCurMesh = Mathf.Min(vertexSpaceStillNeeded, vertSpaceLeftInCurMesh); - int curMeshIndex = curMeshStartingIndex; - curMesh.dirty = curMesh.dirty || numVertsToPutInCurMesh > 0; - for(int i = curVert; i < numVertsToPutInCurMesh + curVert; i ++) { - int index = curMeshIndex + i - curVert; - - if (!model.HasMesh(vertexKeys[i].meshId)) continue; + private List availableEdgeMeshes; + private List edgeMeshes; + private HashSet meshesInEdgeMeshes; + + private List availablePointMeshes; + private List pointMeshes; + private HashSet meshesInPointMeshes; + + /// + /// Returns the scale factor used for rendering inactive vertices - used by selector to make sure selection radii + /// match with what the user sees. + /// + public static float GetVertScaleFactor(WorldSpace worldSpace) + { + return (Mathf.Min(worldSpace.scale, SCALE_THRESH) / SCALE_THRESH) * baseVertexScale; + } - MMesh polyMesh = model.GetMesh(vertexKeys[i].meshId); + /// + /// Returns the scale factor used for rendering inactive edges - used by selector to make sure selection radii + /// match with what the user sees. + /// + public static float GetEdgeScaleFactor(WorldSpace worldSpace) + { + return (Mathf.Min(worldSpace.scale, SCALE_THRESH) / SCALE_THRESH) * baseEdgeScale; + } - curMesh.vertices[index] = - polyMesh.VertexPositionInModelCoords(vertexKeys[i].vertexId); - curMesh.indices.Add(index); + private List edges = new List(); + private HashSet edgeSet = new HashSet(); + + /// + /// Turns on edge wireframes for supplied meshes. (Will use cached data if a mesh has been passed to this method + /// since the most recent clear.) + /// + public void TurnOnEdgeWireframe(IEnumerable meshIds) + { + edges.Clear(); + edgeSet.Clear(); + foreach (int meshId in meshIds) + { + if (meshesInEdgeMeshes.Contains(meshId)) continue; + + if (!model.HasMesh(meshId)) continue; + + MMesh polyMesh = model.GetMesh(meshId); + + foreach (Face curFace in polyMesh.GetFaces()) + { + for (int i = 0; i < curFace.vertexIds.Count; i++) + { + edgeSet.Add(new EdgeKey(meshId, curFace.vertexIds[i], curFace.vertexIds[(i + 1) % curFace.vertexIds.Count])); + } + } + meshesInEdgeMeshes.Add(meshId); + } + edges = edgeSet.ToList(); + int curEdge = 0; + while (curEdge < edges.Count) + { + StaticCachedRenderMesh curMesh = GetCurEdgeMesh(); + int curMeshStartingIndex = curMesh.numElements; + int edgesSpaceStillNeeded = (edges.Count - curEdge) * 2; + int edgeSpaceLeftInCurMesh = MAX_INDEX_COUNT - curMesh.numElements; + int edgesToPutInCurMesh = Mathf.Min(edgesSpaceStillNeeded, edgeSpaceLeftInCurMesh); + int curMeshIndex = curMeshStartingIndex; + curMesh.dirty = curMesh.dirty || edgesToPutInCurMesh > 0; + for (int i = curEdge; i < edgesToPutInCurMesh / 2 + curEdge; i++) + { + int index = curMeshIndex + 2 * (i - curEdge); + if (!model.HasMesh(edges[i].meshId)) continue; + + MMesh polyMesh = model.GetMesh(edges[i].meshId); + + curMesh.vertices[index] = + polyMesh.VertexPositionInModelCoords(edges[i].vertexId1); + curMesh.vertices[index + 1] = + polyMesh.VertexPositionInModelCoords(edges[i].vertexId2); + curMesh.normals[index] = new Vector3(0f, 1f, 0f); + curMesh.normals[index + 1] = new Vector3(0f, 1f, 0f); + + curMesh.indices.Add(index); + curMesh.indices.Add(index + 1); + } + curEdge += edgesToPutInCurMesh / 2; + curMesh.numElements += edgesToPutInCurMesh; + curMesh.mesh.vertices = curMesh.vertices; + curMesh.mesh.normals = curMesh.normals; + curMesh.mesh.SetIndices(curMesh.indices.ToArray(), MeshTopology.Lines, 0, false /* recalculateBounds */); + } + } + /// + /// Turns on vertex wireframes for supplied meshes. (Will use cached data if a mesh has been passed to this method + /// since the most recent clear.) + /// + public void TurnOnPointWireframe(IEnumerable meshIds) + { + List vertexKeys = new List(); + foreach (int meshId in meshIds) + { + if (meshesInPointMeshes.Contains(meshId)) continue; + + if (!model.HasMesh(meshId)) continue; + + MMesh polyMesh = model.GetMesh(meshId); + + foreach (int vertId in polyMesh.GetVertexIds()) + { + vertexKeys.Add(new VertexKey(meshId, vertId)); + } + meshesInPointMeshes.Add(meshId); + } + + int curVert = 0; + while (curVert < vertexKeys.Count) + { + StaticCachedRenderMesh curMesh = GetCurPointMesh(); + int curMeshStartingIndex = curMesh.numElements; + int vertexSpaceStillNeeded = (vertexKeys.Count - curVert); + int vertSpaceLeftInCurMesh = MAX_INDEX_COUNT - curMesh.numElements; + int numVertsToPutInCurMesh = Mathf.Min(vertexSpaceStillNeeded, vertSpaceLeftInCurMesh); + int curMeshIndex = curMeshStartingIndex; + curMesh.dirty = curMesh.dirty || numVertsToPutInCurMesh > 0; + for (int i = curVert; i < numVertsToPutInCurMesh + curVert; i++) + { + int index = curMeshIndex + i - curVert; + + if (!model.HasMesh(vertexKeys[i].meshId)) continue; + + MMesh polyMesh = model.GetMesh(vertexKeys[i].meshId); + + curMesh.vertices[index] = + polyMesh.VertexPositionInModelCoords(vertexKeys[i].vertexId); + curMesh.indices.Add(index); + + } + curVert += numVertsToPutInCurMesh; + curMesh.numElements += numVertsToPutInCurMesh; + curMesh.mesh.vertices = curMesh.vertices; + curMesh.mesh.normals = curMesh.normals; + curMesh.mesh.SetIndices(curMesh.indices.ToArray(), MeshTopology.Points, 0, false /* recalculateBounds */); + } } - curVert += numVertsToPutInCurMesh; - curMesh.numElements += numVertsToPutInCurMesh; - curMesh.mesh.vertices = curMesh.vertices; - curMesh.mesh.normals = curMesh.normals; - curMesh.mesh.SetIndices(curMesh.indices.ToArray(), MeshTopology.Points, 0, false /* recalculateBounds */); - } - } - /// - /// Sets the select position to use for rendering inactive elements - this is used to fade out the selection. - /// - /// - public void SetSelectPosition(Vector3 selectPositionModel) { - selectPositionWorld = worldSpace.ModelToWorld(selectPositionModel); - } + /// + /// Sets the select position to use for rendering inactive elements - this is used to fade out the selection. + /// + /// + public void SetSelectPosition(Vector3 selectPositionModel) + { + selectPositionWorld = worldSpace.ModelToWorld(selectPositionModel); + } - private StaticCachedRenderMesh GetCurEdgeMesh() { - if (edgeMeshes.Count == 0) { - if (availableEdgeMeshes.Count > 0) { - int lastAvailableMeshIndex = availableEdgeMeshes.Count - 1; - StaticCachedRenderMesh mesh = availableEdgeMeshes[lastAvailableMeshIndex]; - edgeMeshes.Add(mesh); - availableEdgeMeshes.RemoveAt(lastAvailableMeshIndex); - return mesh; + private StaticCachedRenderMesh GetCurEdgeMesh() + { + if (edgeMeshes.Count == 0) + { + if (availableEdgeMeshes.Count > 0) + { + int lastAvailableMeshIndex = availableEdgeMeshes.Count - 1; + StaticCachedRenderMesh mesh = availableEdgeMeshes[lastAvailableMeshIndex]; + edgeMeshes.Add(mesh); + availableEdgeMeshes.RemoveAt(lastAvailableMeshIndex); + return mesh; + } + StaticCachedRenderMesh newMesh = new StaticCachedRenderMesh(); + edgeMeshes.Add(newMesh); + return newMesh; + } + + StaticCachedRenderMesh lastMesh = edgeMeshes[edgeMeshes.Count - 1]; + if (lastMesh.numElements >= MAX_INDEX_COUNT) + { + StaticCachedRenderMesh newMesh = new StaticCachedRenderMesh(); + edgeMeshes.Add(newMesh); + return newMesh; + } + return lastMesh; } - StaticCachedRenderMesh newMesh = new StaticCachedRenderMesh(); - edgeMeshes.Add(newMesh); - return newMesh; - } - - StaticCachedRenderMesh lastMesh = edgeMeshes[edgeMeshes.Count - 1]; - if (lastMesh.numElements >= MAX_INDEX_COUNT) { - StaticCachedRenderMesh newMesh = new StaticCachedRenderMesh(); - edgeMeshes.Add(newMesh); - return newMesh; - } - return lastMesh; - } - private StaticCachedRenderMesh GetCurPointMesh() { - if (pointMeshes.Count == 0) { - if (availablePointMeshes.Count > 0) { - int lastAvailableMeshIndex = availablePointMeshes.Count - 1; - StaticCachedRenderMesh mesh = availablePointMeshes[lastAvailableMeshIndex]; - pointMeshes.Add(mesh); - availablePointMeshes.RemoveAt(lastAvailableMeshIndex); - return mesh; + private StaticCachedRenderMesh GetCurPointMesh() + { + if (pointMeshes.Count == 0) + { + if (availablePointMeshes.Count > 0) + { + int lastAvailableMeshIndex = availablePointMeshes.Count - 1; + StaticCachedRenderMesh mesh = availablePointMeshes[lastAvailableMeshIndex]; + pointMeshes.Add(mesh); + availablePointMeshes.RemoveAt(lastAvailableMeshIndex); + return mesh; + } + StaticCachedRenderMesh newMesh = new StaticCachedRenderMesh(); + pointMeshes.Add(newMesh); + return newMesh; + } + + StaticCachedRenderMesh lastMesh = pointMeshes[pointMeshes.Count - 1]; + if (lastMesh.numElements >= MAX_INDEX_COUNT) + { + StaticCachedRenderMesh newMesh = new StaticCachedRenderMesh(); + pointMeshes.Add(newMesh); + return newMesh; + } + return lastMesh; } - StaticCachedRenderMesh newMesh = new StaticCachedRenderMesh(); - pointMeshes.Add(newMesh); - return newMesh; - } - - StaticCachedRenderMesh lastMesh = pointMeshes[pointMeshes.Count - 1]; - if (lastMesh.numElements >= MAX_INDEX_COUNT) { - StaticCachedRenderMesh newMesh = new StaticCachedRenderMesh(); - pointMeshes.Add(newMesh); - return newMesh; - } - return lastMesh; - } - /// - /// Clears all vertices and edges out of the inactive renderer. - /// - public void Clear() { - availableEdgeMeshes.AddRange(edgeMeshes); - edgeMeshes.Clear(); - foreach (StaticCachedRenderMesh mesh in availableEdgeMeshes) { - mesh.Clear(); - } - meshesInEdgeMeshes.Clear(); - - availablePointMeshes.AddRange(pointMeshes); - pointMeshes.Clear(); - foreach (StaticCachedRenderMesh mesh in availablePointMeshes) { - mesh.Clear(); - } - meshesInPointMeshes.Clear(); - // If the user has changed the flag, we handle it here so that next time they use the tool it's updated. - // It's a bit janky, but since this is handling a console command rather than real UX it's okay - if we go - // with the new radius it will be set from the start and this just goes away. - InactiveSelectionHighlighter.INACTIVE_HIGHLIGHT_RADIUS = Features.expandedWireframeRadius - ? InactiveSelectionHighlighter.NEW_INACTIVE_HIGHLIGHT_RADIUS - : InactiveSelectionHighlighter.OLD_INACTIVE_HIGHLIGHT_RADIUS; - } + /// + /// Clears all vertices and edges out of the inactive renderer. + /// + public void Clear() + { + availableEdgeMeshes.AddRange(edgeMeshes); + edgeMeshes.Clear(); + foreach (StaticCachedRenderMesh mesh in availableEdgeMeshes) + { + mesh.Clear(); + } + meshesInEdgeMeshes.Clear(); + + availablePointMeshes.AddRange(pointMeshes); + pointMeshes.Clear(); + foreach (StaticCachedRenderMesh mesh in availablePointMeshes) + { + mesh.Clear(); + } + meshesInPointMeshes.Clear(); + // If the user has changed the flag, we handle it here so that next time they use the tool it's updated. + // It's a bit janky, but since this is handling a console command rather than real UX it's okay - if we go + // with the new radius it will be set from the start and this just goes away. + InactiveSelectionHighlighter.INACTIVE_HIGHLIGHT_RADIUS = Features.expandedWireframeRadius + ? InactiveSelectionHighlighter.NEW_INACTIVE_HIGHLIGHT_RADIUS + : InactiveSelectionHighlighter.OLD_INACTIVE_HIGHLIGHT_RADIUS; + } - /// - /// Renders the inactive edges. - /// - public void RenderEdges() { - if (showEdges && edgeMeshes.Count > 0) { - float scaleFactor = GetEdgeScaleFactor(worldSpace); - inactiveEdgeMaterial.SetFloat("_PointSphereRadius", scaleFactor); - inactiveEdgeMaterial.SetFloat("_VertexSphereRadius", scaleFactor); - inactiveEdgeMaterial.SetVector("_SelectPositionWorld", selectPositionWorld); - inactiveEdgeMaterial.SetFloat("_SelectRadius", InactiveSelectionHighlighter.INACTIVE_HIGHLIGHT_RADIUS); - for (int i = 0; i < edgeMeshes.Count; i++) { - Graphics.DrawMesh(edgeMeshes[i].mesh, worldSpace.modelToWorld, inactiveEdgeMaterial, - MeshWithMaterialRenderer.DEFAULT_LAYER); + /// + /// Renders the inactive edges. + /// + public void RenderEdges() + { + if (showEdges && edgeMeshes.Count > 0) + { + float scaleFactor = GetEdgeScaleFactor(worldSpace); + inactiveEdgeMaterial.SetFloat("_PointSphereRadius", scaleFactor); + inactiveEdgeMaterial.SetFloat("_VertexSphereRadius", scaleFactor); + inactiveEdgeMaterial.SetVector("_SelectPositionWorld", selectPositionWorld); + inactiveEdgeMaterial.SetFloat("_SelectRadius", InactiveSelectionHighlighter.INACTIVE_HIGHLIGHT_RADIUS); + for (int i = 0; i < edgeMeshes.Count; i++) + { + Graphics.DrawMesh(edgeMeshes[i].mesh, worldSpace.modelToWorld, inactiveEdgeMaterial, + MeshWithMaterialRenderer.DEFAULT_LAYER); + } + } } - } - } - /// - /// Renders the inactive vertices. - /// - public void RenderPoints() { - if (showPoints && pointMeshes.Count > 0) { - float scaleFactor = GetVertScaleFactor(worldSpace); - inactivePointMaterial.SetFloat("_PointSphereRadius", scaleFactor); - inactivePointMaterial.SetVector("_SelectPositionWorld", selectPositionWorld); - inactivePointMaterial.SetFloat("_SelectRadius", InactiveSelectionHighlighter.INACTIVE_HIGHLIGHT_RADIUS); - for (int i = 0; i < pointMeshes.Count; i++) { - Graphics.DrawMesh(pointMeshes[i].mesh, worldSpace.modelToWorld, inactivePointMaterial, - MeshWithMaterialRenderer.DEFAULT_LAYER); + /// + /// Renders the inactive vertices. + /// + public void RenderPoints() + { + if (showPoints && pointMeshes.Count > 0) + { + float scaleFactor = GetVertScaleFactor(worldSpace); + inactivePointMaterial.SetFloat("_PointSphereRadius", scaleFactor); + inactivePointMaterial.SetVector("_SelectPositionWorld", selectPositionWorld); + inactivePointMaterial.SetFloat("_SelectRadius", InactiveSelectionHighlighter.INACTIVE_HIGHLIGHT_RADIUS); + for (int i = 0; i < pointMeshes.Count; i++) + { + Graphics.DrawMesh(pointMeshes[i].mesh, worldSpace.modelToWorld, inactivePointMaterial, + MeshWithMaterialRenderer.DEFAULT_LAYER); + } + } } - } - } - } + } } diff --git a/Assets/Scripts/tools/utils/HighlightStyles/MaterialCycler.cs b/Assets/Scripts/tools/utils/HighlightStyles/MaterialCycler.cs index f4ce0242..8f934ae7 100644 --- a/Assets/Scripts/tools/utils/HighlightStyles/MaterialCycler.cs +++ b/Assets/Scripts/tools/utils/HighlightStyles/MaterialCycler.cs @@ -15,35 +15,41 @@ using System.Collections.Generic; using UnityEngine; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// This class supplies instances of a material so that we don't need to reinstantiate, which can leak Materials. - /// The queue of instances is for situations where a draw call needs a unique instance (one of the properties is - /// animated in a way that may be different per-draw call, for example). If more unique instances are requested than - /// exist, it will start reusing - this will cause a visual artifact, but as most of these animations are quite fast - /// it should be hard to trigger. - /// - public class MaterialCycler { - private Queue matQueue; - private Material fixedInstance; +namespace com.google.apps.peltzer.client.tools.utils +{ + /// + /// This class supplies instances of a material so that we don't need to reinstantiate, which can leak Materials. + /// The queue of instances is for situations where a draw call needs a unique instance (one of the properties is + /// animated in a way that may be different per-draw call, for example). If more unique instances are requested than + /// exist, it will start reusing - this will cause a visual artifact, but as most of these animations are quite fast + /// it should be hard to trigger. + /// + public class MaterialCycler + { + private Queue matQueue; + private Material fixedInstance; - public MaterialCycler(Material baseMaterial, int instanceSize) { - fixedInstance = new Material(baseMaterial); - matQueue = new Queue(); - for (int i = 0; i < instanceSize; i++) { - matQueue.Enqueue(new Material(baseMaterial)); - } - } + public MaterialCycler(Material baseMaterial, int instanceSize) + { + fixedInstance = new Material(baseMaterial); + matQueue = new Queue(); + for (int i = 0; i < instanceSize; i++) + { + matQueue.Enqueue(new Material(baseMaterial)); + } + } - public Material GetFixedMaterial() { - return fixedInstance; - } + public Material GetFixedMaterial() + { + return fixedInstance; + } - public Material GetInstanceOfMaterial() { - // Pull the material off the front of the queue, add it back to the end, then return it. - Material front = matQueue.Dequeue(); - matQueue.Enqueue(front); - return front; + public Material GetInstanceOfMaterial() + { + // Pull the material off the front of the queue, add it back to the end, then return it. + Material front = matQueue.Dequeue(); + matQueue.Enqueue(front); + return front; + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/tools/utils/HighlightStyles/MeshCycler.cs b/Assets/Scripts/tools/utils/HighlightStyles/MeshCycler.cs index fd01fe00..fe79016b 100644 --- a/Assets/Scripts/tools/utils/HighlightStyles/MeshCycler.cs +++ b/Assets/Scripts/tools/utils/HighlightStyles/MeshCycler.cs @@ -15,65 +15,76 @@ using System.Collections.Generic; using UnityEngine; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// Class to efficiently recycle temporary mesh object so that we don't risk memory leaks of Mesh objects. - /// - public class MeshCycler { - // Don't need a queue per se, but it has fast insert and remove ops. - private static Queue meshPool = new Queue(); - // A dictionary from meshId/materialId to Unity Mesh, structured as: - // Key: MeshID - // Value: A dictionary of: - // Key: MaterialID - // Value: Unity Mesh - private static Dictionary> meshDict = new Dictionary>(); - +namespace com.google.apps.peltzer.client.tools.utils +{ /// - /// Returns a Unity Mesh corresponding to the given meshId/materialId pair. - /// Note that THE CALLER SHOULD NOT HOLD A REFERENCE TO THIS MESH as it will be recycled. + /// Class to efficiently recycle temporary mesh object so that we don't risk memory leaks of Mesh objects. /// - /// A Mesh ID - /// A Material ID - /// - /// Whether the mesh needed to be created (in which case its vertices need to be set. - /// - public static Mesh GetTempMeshForMeshMatId(int meshId, int materialId, out bool createdMesh) { - // Get, or create, the dictionary of materials-to-Unity Meshes for this mesh ID. - Dictionary matDict; - if (!meshDict.TryGetValue(meshId, out matDict)) { - matDict = new Dictionary(); - meshDict[meshId] = matDict; - } + public class MeshCycler + { + // Don't need a queue per se, but it has fast insert and remove ops. + private static Queue meshPool = new Queue(); + // A dictionary from meshId/materialId to Unity Mesh, structured as: + // Key: MeshID + // Value: A dictionary of: + // Key: MaterialID + // Value: Unity Mesh + private static Dictionary> meshDict = new Dictionary>(); - // Get the Unity Mesh for this material ID if it already exists. - Mesh mesh; - if (matDict.TryGetValue(materialId, out mesh)) { - createdMesh = false; - return mesh; - } + /// + /// Returns a Unity Mesh corresponding to the given meshId/materialId pair. + /// Note that THE CALLER SHOULD NOT HOLD A REFERENCE TO THIS MESH as it will be recycled. + /// + /// A Mesh ID + /// A Material ID + /// + /// Whether the mesh needed to be created (in which case its vertices need to be set. + /// + public static Mesh GetTempMeshForMeshMatId(int meshId, int materialId, out bool createdMesh) + { + // Get, or create, the dictionary of materials-to-Unity Meshes for this mesh ID. + Dictionary matDict; + if (!meshDict.TryGetValue(meshId, out matDict)) + { + matDict = new Dictionary(); + meshDict[meshId] = matDict; + } - // Create the Unity Mesh for this material ID. Try and fetch a spare mesh from the pool if possible, - // else create a new Mesh. - createdMesh = true; - if (meshPool.Count > 0) { - mesh = meshPool.Dequeue(); - } else { - mesh = new Mesh(); - } + // Get the Unity Mesh for this material ID if it already exists. + Mesh mesh; + if (matDict.TryGetValue(materialId, out mesh)) + { + createdMesh = false; + return mesh; + } - matDict[meshId] = mesh; - return mesh; - } + // Create the Unity Mesh for this material ID. Try and fetch a spare mesh from the pool if possible, + // else create a new Mesh. + createdMesh = true; + if (meshPool.Count > 0) + { + mesh = meshPool.Dequeue(); + } + else + { + mesh = new Mesh(); + } + + matDict[meshId] = mesh; + return mesh; + } - public static void ResetCycler() { - foreach (Dictionary matDict in meshDict.Values) { - foreach (Mesh mesh in matDict.Values) { - mesh.Clear(); - meshPool.Enqueue(mesh); + public static void ResetCycler() + { + foreach (Dictionary matDict in meshDict.Values) + { + foreach (Mesh mesh in matDict.Values) + { + mesh.Clear(); + meshPool.Enqueue(mesh); + } + } + meshDict.Clear(); } - } - meshDict.Clear(); } - } } diff --git a/Assets/Scripts/tools/utils/HighlightStyles/MeshDeleteStyle.cs b/Assets/Scripts/tools/utils/HighlightStyles/MeshDeleteStyle.cs index c65ce475..8456e5d4 100644 --- a/Assets/Scripts/tools/utils/HighlightStyles/MeshDeleteStyle.cs +++ b/Assets/Scripts/tools/utils/HighlightStyles/MeshDeleteStyle.cs @@ -19,66 +19,73 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// This class exists primarily to hold the static method for RenderMeshes when DELETE is set. It may be possible to - /// consolidate this with the other Mesh*Style classes in the future. - /// - public class MeshDeleteStyle { - private static readonly float HIGHLIGHT_EMISSIVE_AMOUNT = 0.5f; - private static readonly Color ERASE_COLOR = new Color(249f / 255f, 105f/255f, 191f/255f, 1f); - private static Dictionary matDict; +namespace com.google.apps.peltzer.client.tools.utils +{ + /// + /// This class exists primarily to hold the static method for RenderMeshes when DELETE is set. It may be possible to + /// consolidate this with the other Mesh*Style classes in the future. + /// + public class MeshDeleteStyle + { + private static readonly float HIGHLIGHT_EMISSIVE_AMOUNT = 0.5f; + private static readonly Color ERASE_COLOR = new Color(249f / 255f, 105f / 255f, 191f / 255f, 1f); + private static Dictionary matDict; - public static void Setup() { - matDict = new Dictionary(); - MaterialCycler gemCycler = new MaterialCycler( - MaterialRegistry.GetMaterialAndColorById(MaterialRegistry.GEM_ID).material, 5); - matDict[MaterialRegistry.MaterialType.GEM] = gemCycler; - MaterialCycler glassCycler = new MaterialCycler( - MaterialRegistry.GetMaterialAndColorById(MaterialRegistry.GLASS_ID).material, 5); - matDict[MaterialRegistry.MaterialType.GLASS] = glassCycler; - MaterialCycler paperCycler = new MaterialCycler( - MaterialRegistry.GetMaterialAndColorById(1).material, 5); - matDict[MaterialRegistry.MaterialType.PAPER] = paperCycler; - } + public static void Setup() + { + matDict = new Dictionary(); + MaterialCycler gemCycler = new MaterialCycler( + MaterialRegistry.GetMaterialAndColorById(MaterialRegistry.GEM_ID).material, 5); + matDict[MaterialRegistry.MaterialType.GEM] = gemCycler; + MaterialCycler glassCycler = new MaterialCycler( + MaterialRegistry.GetMaterialAndColorById(MaterialRegistry.GLASS_ID).material, 5); + matDict[MaterialRegistry.MaterialType.GLASS] = glassCycler; + MaterialCycler paperCycler = new MaterialCycler( + MaterialRegistry.GetMaterialAndColorById(1).material, 5); + matDict[MaterialRegistry.MaterialType.PAPER] = paperCycler; + } - // Renders mesh highlights. - // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing - // mesh geometry frame to frame) - 37281287 - public static void RenderMeshes(Model model, - HighlightUtils.TrackedHighlightSet meshHighlights, WorldSpace worldSpace) { - HashSet meshes = meshHighlights.getKeysForStyle((int)MeshStyles.MESH_DELETE); - foreach (int key in meshes) { - if (model.MeshIsMarkedForDeletion(key) || !model.HasMesh(key)) continue; + // Renders mesh highlights. + // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing + // mesh geometry frame to frame) - 37281287 + public static void RenderMeshes(Model model, + HighlightUtils.TrackedHighlightSet meshHighlights, WorldSpace worldSpace) + { + HashSet meshes = meshHighlights.getKeysForStyle((int)MeshStyles.MESH_DELETE); + foreach (int key in meshes) + { + if (model.MeshIsMarkedForDeletion(key) || !model.HasMesh(key)) continue; - Dictionary contexts = - model.meshRepresentationCache.FetchComponentsForMesh( - key, /* abortOnTooManyCacheMisses */ true); - if (contexts == null) { - continue; - } + Dictionary contexts = + model.meshRepresentationCache.FetchComponentsForMesh( + key, /* abortOnTooManyCacheMisses */ true); + if (contexts == null) + { + continue; + } + + float animPct = meshHighlights.GetAnimPct(key); + float emissiveAmount = HIGHLIGHT_EMISSIVE_AMOUNT * animPct; - float animPct = meshHighlights.GetAnimPct(key); - float emissiveAmount = HIGHLIGHT_EMISSIVE_AMOUNT * animPct; + foreach (KeyValuePair pair in contexts) + { + int matId = pair.Key; + MeshGenContext meshGenContext = pair.Value; - foreach (KeyValuePair pair in contexts) { - int matId = pair.Key; - MeshGenContext meshGenContext = pair.Value; - - Mesh curMesh = new Mesh(); - Material curMaterial = animPct >= .99f ? matDict[MaterialRegistry.GetMaterialType(matId)].GetFixedMaterial() : - matDict[MaterialRegistry.GetMaterialType(matId)].GetInstanceOfMaterial(); - curMaterial.SetFloat("_OverrideAmount", emissiveAmount); - curMaterial.SetColor("_OverrideColor", ERASE_COLOR); - curMaterial.renderQueue = 3000; - curMesh.SetVertices(meshGenContext.verts); - curMesh.SetColors(meshGenContext.colors); - curMesh.SetTriangles(meshGenContext.triangles, /* subMesh */ 0); - curMesh.RecalculateNormals(); + Mesh curMesh = new Mesh(); + Material curMaterial = animPct >= .99f ? matDict[MaterialRegistry.GetMaterialType(matId)].GetFixedMaterial() : + matDict[MaterialRegistry.GetMaterialType(matId)].GetInstanceOfMaterial(); + curMaterial.SetFloat("_OverrideAmount", emissiveAmount); + curMaterial.SetColor("_OverrideColor", ERASE_COLOR); + curMaterial.renderQueue = 3000; + curMesh.SetVertices(meshGenContext.verts); + curMesh.SetColors(meshGenContext.colors); + curMesh.SetTriangles(meshGenContext.triangles, /* subMesh */ 0); + curMesh.RecalculateNormals(); - Graphics.DrawMesh(curMesh, worldSpace.modelToWorld, curMaterial, 0); + Graphics.DrawMesh(curMesh, worldSpace.modelToWorld, curMaterial, 0); + } + } } - } } - } } diff --git a/Assets/Scripts/tools/utils/HighlightStyles/MeshPaintStyle.cs b/Assets/Scripts/tools/utils/HighlightStyles/MeshPaintStyle.cs index 094ff6de..ac3b01c6 100644 --- a/Assets/Scripts/tools/utils/HighlightStyles/MeshPaintStyle.cs +++ b/Assets/Scripts/tools/utils/HighlightStyles/MeshPaintStyle.cs @@ -19,77 +19,86 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// This class exists primarily to hold the static method for RenderMeshes when PAINT is set. It may be possible to - /// consolidate this with the other Mesh*Style classes in the future. - /// - public class MeshPaintStyle { - private static readonly float HIGHLIGHT_EMISSIVE_AMOUNT = 0.7f; - private static readonly float HIGHLIGHT_ALPHA = 0.75f; - public static Material material; - private static Dictionary matDict; +namespace com.google.apps.peltzer.client.tools.utils +{ + /// + /// This class exists primarily to hold the static method for RenderMeshes when PAINT is set. It may be possible to + /// consolidate this with the other Mesh*Style classes in the future. + /// + public class MeshPaintStyle + { + private static readonly float HIGHLIGHT_EMISSIVE_AMOUNT = 0.7f; + private static readonly float HIGHLIGHT_ALPHA = 0.75f; + public static Material material; + private static Dictionary matDict; - public static void Setup() { - matDict = new Dictionary(); - MaterialCycler gemCycler = new MaterialCycler( - MaterialRegistry.GetMaterialAndColorById(MaterialRegistry.GEM_ID).material, 5); - matDict[MaterialRegistry.MaterialType.GEM] = gemCycler; - MaterialCycler glassCycler = new MaterialCycler( - MaterialRegistry.GetMaterialAndColorById(MaterialRegistry.GLASS_ID).material, 5); - matDict[MaterialRegistry.MaterialType.GLASS] = glassCycler; - MaterialCycler paperCycler = new MaterialCycler( - MaterialRegistry.GetMaterialAndColorById(1).material, 5); - matDict[MaterialRegistry.MaterialType.PAPER] = paperCycler; - } + public static void Setup() + { + matDict = new Dictionary(); + MaterialCycler gemCycler = new MaterialCycler( + MaterialRegistry.GetMaterialAndColorById(MaterialRegistry.GEM_ID).material, 5); + matDict[MaterialRegistry.MaterialType.GEM] = gemCycler; + MaterialCycler glassCycler = new MaterialCycler( + MaterialRegistry.GetMaterialAndColorById(MaterialRegistry.GLASS_ID).material, 5); + matDict[MaterialRegistry.MaterialType.GLASS] = glassCycler; + MaterialCycler paperCycler = new MaterialCycler( + MaterialRegistry.GetMaterialAndColorById(1).material, 5); + matDict[MaterialRegistry.MaterialType.PAPER] = paperCycler; + } - // Renders mesh highlights. - public static void RenderMeshes(Model model, - HighlightUtils.TrackedHighlightSet meshHighlights, WorldSpace worldSpace) { - foreach (int key in meshHighlights.getKeysForStyle((int)MeshStyles.MESH_PAINT)) { - if (!model.HasMesh(key)) continue; + // Renders mesh highlights. + public static void RenderMeshes(Model model, + HighlightUtils.TrackedHighlightSet meshHighlights, WorldSpace worldSpace) + { + foreach (int key in meshHighlights.getKeysForStyle((int)MeshStyles.MESH_PAINT)) + { + if (!model.HasMesh(key)) continue; - Dictionary contexts = - model.meshRepresentationCache.FetchComponentsForMesh( - key, /* abortOnTooManyCacheMisses */ true); - if (contexts == null) { - continue; - } + Dictionary contexts = + model.meshRepresentationCache.FetchComponentsForMesh( + key, /* abortOnTooManyCacheMisses */ true); + if (contexts == null) + { + continue; + } - float animPct = meshHighlights.GetAnimPct(key); - MaterialAndColor currentMaterialAndColor = MaterialRegistry.GetMaterialAndColorById( - PeltzerMain.Instance.peltzerController.currentMaterial); - Material currentMaterial = animPct >= .99f ? matDict[MaterialRegistry.GetMaterialType(currentMaterialAndColor.matId)].GetFixedMaterial() : - matDict[MaterialRegistry.GetMaterialType(currentMaterialAndColor.matId)].GetInstanceOfMaterial(); - Color currentColor = currentMaterialAndColor.color; + float animPct = meshHighlights.GetAnimPct(key); + MaterialAndColor currentMaterialAndColor = MaterialRegistry.GetMaterialAndColorById( + PeltzerMain.Instance.peltzerController.currentMaterial); + Material currentMaterial = animPct >= .99f ? matDict[MaterialRegistry.GetMaterialType(currentMaterialAndColor.matId)].GetFixedMaterial() : + matDict[MaterialRegistry.GetMaterialType(currentMaterialAndColor.matId)].GetInstanceOfMaterial(); + Color currentColor = currentMaterialAndColor.color; - float emissiveAmount = HIGHLIGHT_EMISSIVE_AMOUNT * animPct; - float transparentMult = currentMaterial.GetFloat("_MultiplicitiveAlpha") * (1 - animPct) + HIGHLIGHT_ALPHA * animPct; - currentMaterial.SetFloat("_EmissiveAmount", emissiveAmount); - currentMaterial.SetFloat("_MultiplicitiveAlpha", transparentMult); - currentMaterial.renderQueue = 3000; + float emissiveAmount = HIGHLIGHT_EMISSIVE_AMOUNT * animPct; + float transparentMult = currentMaterial.GetFloat("_MultiplicitiveAlpha") * (1 - animPct) + HIGHLIGHT_ALPHA * animPct; + currentMaterial.SetFloat("_EmissiveAmount", emissiveAmount); + currentMaterial.SetFloat("_MultiplicitiveAlpha", transparentMult); + currentMaterial.renderQueue = 3000; - foreach (KeyValuePair pair in contexts) { - int matId = pair.Key; - MeshGenContext meshGenContext = pair.Value; + foreach (KeyValuePair pair in contexts) + { + int matId = pair.Key; + MeshGenContext meshGenContext = pair.Value; - bool needToPopulateMesh; - Mesh curMesh = MeshCycler.GetTempMeshForMeshMatId(key, matId, out needToPopulateMesh); + bool needToPopulateMesh; + Mesh curMesh = MeshCycler.GetTempMeshForMeshMatId(key, matId, out needToPopulateMesh); - if (needToPopulateMesh) { - curMesh.SetVertices(meshGenContext.verts); - int count = meshGenContext.verts.Count; - List colors = new List(count); - for (int i = 0; i < count; i++) { - colors.Add(currentColor); + if (needToPopulateMesh) + { + curMesh.SetVertices(meshGenContext.verts); + int count = meshGenContext.verts.Count; + List colors = new List(count); + for (int i = 0; i < count; i++) + { + colors.Add(currentColor); + } + curMesh.SetColors(colors); + curMesh.SetTriangles(meshGenContext.triangles, /* subMesh */ 0); + curMesh.RecalculateNormals(); + } + Graphics.DrawMesh(curMesh, worldSpace.modelToWorld, currentMaterial, 0); + } } - curMesh.SetColors(colors); - curMesh.SetTriangles(meshGenContext.triangles, /* subMesh */ 0); - curMesh.RecalculateNormals(); - } - Graphics.DrawMesh(curMesh, worldSpace.modelToWorld, currentMaterial, 0); } - } } - } } \ No newline at end of file diff --git a/Assets/Scripts/tools/utils/HighlightStyles/MeshSelectStyle.cs b/Assets/Scripts/tools/utils/HighlightStyles/MeshSelectStyle.cs index 065fba90..a768f075 100644 --- a/Assets/Scripts/tools/utils/HighlightStyles/MeshSelectStyle.cs +++ b/Assets/Scripts/tools/utils/HighlightStyles/MeshSelectStyle.cs @@ -19,89 +19,98 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// This class exists primarily to hold the static method for RenderMeshes when SELECT is set. It may be possible to - /// consolidate this with the other Mesh*Style classes in the future. - /// - public class MeshSelectStyle { - private static readonly float HIGHLIGHT_EMISSIVE_AMOUNT = 0.7f; - private static readonly float HIGHLIGHT_ALPHA = 0.75f; - public static Material material; - public static Material silhouetteMaterial; - private static Dictionary matDict; - private static MaterialCycler silhouetteInstancer; - private static Material gemInstance; +namespace com.google.apps.peltzer.client.tools.utils +{ + /// + /// This class exists primarily to hold the static method for RenderMeshes when SELECT is set. It may be possible to + /// consolidate this with the other Mesh*Style classes in the future. + /// + public class MeshSelectStyle + { + private static readonly float HIGHLIGHT_EMISSIVE_AMOUNT = 0.7f; + private static readonly float HIGHLIGHT_ALPHA = 0.75f; + public static Material material; + public static Material silhouetteMaterial; + private static Dictionary matDict; + private static MaterialCycler silhouetteInstancer; + private static Material gemInstance; - public static void Setup() { - matDict = new Dictionary(); - MaterialCycler gemCycler = new MaterialCycler( - MaterialRegistry.GetMaterialAndColorById(MaterialRegistry.GEM_ID).material, 5); - matDict[MaterialRegistry.MaterialType.GEM] = gemCycler; - MaterialCycler glassCycler = new MaterialCycler( - MaterialRegistry.GetMaterialAndColorById(MaterialRegistry.GLASS_ID).material, 5); - matDict[MaterialRegistry.MaterialType.GLASS] = glassCycler; - MaterialCycler paperCycler = new MaterialCycler( - MaterialRegistry.GetMaterialAndColorById(1).material, 5); - matDict[MaterialRegistry.MaterialType.PAPER] = paperCycler; - silhouetteInstancer = new MaterialCycler(silhouetteMaterial, 5); - } + public static void Setup() + { + matDict = new Dictionary(); + MaterialCycler gemCycler = new MaterialCycler( + MaterialRegistry.GetMaterialAndColorById(MaterialRegistry.GEM_ID).material, 5); + matDict[MaterialRegistry.MaterialType.GEM] = gemCycler; + MaterialCycler glassCycler = new MaterialCycler( + MaterialRegistry.GetMaterialAndColorById(MaterialRegistry.GLASS_ID).material, 5); + matDict[MaterialRegistry.MaterialType.GLASS] = glassCycler; + MaterialCycler paperCycler = new MaterialCycler( + MaterialRegistry.GetMaterialAndColorById(1).material, 5); + matDict[MaterialRegistry.MaterialType.PAPER] = paperCycler; + silhouetteInstancer = new MaterialCycler(silhouetteMaterial, 5); + } - // Renders mesh highlights. - // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing - // mesh geometry frame to frame) - 37281287 - public static void RenderMeshes(Model model, - HighlightUtils.TrackedHighlightSet meshHighlights, - WorldSpace worldSpace) { - // Get world position of selector position. - Vector4 selectorWorldPosition = PeltzerMain.Instance.worldSpace - .ModelToWorld(PeltzerMain.Instance.GetSelector().selectorPosition); - foreach (int key in meshHighlights.getKeysForStyle((int) MeshStyles.MESH_SELECT)) { - if (!model.HasMesh(key)) continue; + // Renders mesh highlights. + // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing + // mesh geometry frame to frame) - 37281287 + public static void RenderMeshes(Model model, + HighlightUtils.TrackedHighlightSet meshHighlights, + WorldSpace worldSpace) + { + // Get world position of selector position. + Vector4 selectorWorldPosition = PeltzerMain.Instance.worldSpace + .ModelToWorld(PeltzerMain.Instance.GetSelector().selectorPosition); + foreach (int key in meshHighlights.getKeysForStyle((int)MeshStyles.MESH_SELECT)) + { + if (!model.HasMesh(key)) continue; - Dictionary contexts = - model.meshRepresentationCache.FetchComponentsForMesh( - key, /* abortOnTooManyCacheMisses */ true); - if (contexts == null) { - continue; - } + Dictionary contexts = + model.meshRepresentationCache.FetchComponentsForMesh( + key, /* abortOnTooManyCacheMisses */ true); + if (contexts == null) + { + continue; + } - float animPct = meshHighlights.GetAnimPct(key); - float emissiveAmount = HIGHLIGHT_EMISSIVE_AMOUNT * animPct; + float animPct = meshHighlights.GetAnimPct(key); + float emissiveAmount = HIGHLIGHT_EMISSIVE_AMOUNT * animPct; - foreach (KeyValuePair pair in contexts) { - int matId = pair.Key; - MeshGenContext meshGenContext = pair.Value; + foreach (KeyValuePair pair in contexts) + { + int matId = pair.Key; + MeshGenContext meshGenContext = pair.Value; - bool needToPopulateMesh; - Mesh curMesh = MeshCycler.GetTempMeshForMeshMatId(key, matId, out needToPopulateMesh); - Material curMaterial = animPct >= .99f ? matDict[MaterialRegistry.GetMaterialType(matId)].GetFixedMaterial() : - matDict[MaterialRegistry.GetMaterialType(matId)].GetInstanceOfMaterial(); - curMaterial.SetFloat("_EmissiveAmount", emissiveAmount); - float transparentMult = curMaterial.GetFloat("_MultiplicitiveAlpha") * (1 - animPct) + HIGHLIGHT_ALPHA * animPct; - curMaterial.SetFloat("_MultiplicitiveAlpha", transparentMult); - Material curSilMat = animPct >= .99f ? silhouetteInstancer.GetFixedMaterial() - : silhouetteInstancer.GetInstanceOfMaterial(); - curSilMat.SetFloat("_EmissiveAmount", emissiveAmount); - curSilMat.SetFloat("_MultiplicitiveAlpha", transparentMult); - // Set w component to indicate active vs inactive. - selectorWorldPosition.w = 0f; - if (PeltzerMain.Instance.GetSelector().isMultiSelecting) { - selectorWorldPosition.w = 1.0f; - } - curMaterial.SetVector("_SelectorPosition", selectorWorldPosition); - curMaterial.renderQueue = 3000; - if (needToPopulateMesh) { - curMesh.SetVertices(meshGenContext.verts); - curMesh.SetColors(meshGenContext.colors); - curMesh.SetTriangles(meshGenContext.triangles, /* subMesh */ 0); - curMesh.RecalculateNormals(); - } + bool needToPopulateMesh; + Mesh curMesh = MeshCycler.GetTempMeshForMeshMatId(key, matId, out needToPopulateMesh); + Material curMaterial = animPct >= .99f ? matDict[MaterialRegistry.GetMaterialType(matId)].GetFixedMaterial() : + matDict[MaterialRegistry.GetMaterialType(matId)].GetInstanceOfMaterial(); + curMaterial.SetFloat("_EmissiveAmount", emissiveAmount); + float transparentMult = curMaterial.GetFloat("_MultiplicitiveAlpha") * (1 - animPct) + HIGHLIGHT_ALPHA * animPct; + curMaterial.SetFloat("_MultiplicitiveAlpha", transparentMult); + Material curSilMat = animPct >= .99f ? silhouetteInstancer.GetFixedMaterial() + : silhouetteInstancer.GetInstanceOfMaterial(); + curSilMat.SetFloat("_EmissiveAmount", emissiveAmount); + curSilMat.SetFloat("_MultiplicitiveAlpha", transparentMult); + // Set w component to indicate active vs inactive. + selectorWorldPosition.w = 0f; + if (PeltzerMain.Instance.GetSelector().isMultiSelecting) + { + selectorWorldPosition.w = 1.0f; + } + curMaterial.SetVector("_SelectorPosition", selectorWorldPosition); + curMaterial.renderQueue = 3000; + if (needToPopulateMesh) + { + curMesh.SetVertices(meshGenContext.verts); + curMesh.SetColors(meshGenContext.colors); + curMesh.SetTriangles(meshGenContext.triangles, /* subMesh */ 0); + curMesh.RecalculateNormals(); + } - Graphics.DrawMesh(curMesh, worldSpace.modelToWorld, curMaterial, 0); - Graphics.DrawMesh(curMesh, worldSpace.modelToWorld, curSilMat, 0); + Graphics.DrawMesh(curMesh, worldSpace.modelToWorld, curMaterial, 0); + Graphics.DrawMesh(curMesh, worldSpace.modelToWorld, curSilMat, 0); + } + } } - } } - } } \ No newline at end of file diff --git a/Assets/Scripts/tools/utils/HighlightStyles/TutorialHighlightStyle.cs b/Assets/Scripts/tools/utils/HighlightStyles/TutorialHighlightStyle.cs index 352bd53b..fc80a9d2 100644 --- a/Assets/Scripts/tools/utils/HighlightStyles/TutorialHighlightStyle.cs +++ b/Assets/Scripts/tools/utils/HighlightStyles/TutorialHighlightStyle.cs @@ -19,87 +19,95 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// This class defines the static method for RenderMeshes when a mesh needs to be - /// highlighted during a tutorial. It may be possible to consolidate this with the - /// other Mesh*Style classes in the future. - /// - public class TutorialHighlightStyle { - - /// - /// The period of the "glow" animation (the time taken by each repetition of it). - /// Shorter values means a faster animation. Given in seconds. - /// - private const float GLOW_PERIOD = 1.0f; - - /// - /// The maximum emission factor when glowing. More is brighter. - /// - private const float GLOW_MAX_EMISSION = 0.45f; - - private static readonly float HIGHLIGHT_ALPHA = 0.75f; - +namespace com.google.apps.peltzer.client.tools.utils +{ /// - /// String names of the shader properties to be changed. + /// This class defines the static method for RenderMeshes when a mesh needs to be + /// highlighted during a tutorial. It may be possible to consolidate this with the + /// other Mesh*Style classes in the future. /// - private const string EMISSIVE_AMT_NAME = "_EmissiveAmount"; - private const string EMISSIVE_COLOR_NAME = "_EmissiveColor"; - - /// - /// Base color of the glowing animations. (Greenish) - /// - private static readonly Color GLOW_COLOR = new Color(0.2f, 1.0f, 0.2f); - - public static Material material; - private static Dictionary matDict; - - public static void Setup() { - matDict = new Dictionary(); - MaterialCycler paperCycler = new MaterialCycler( - MaterialRegistry.GetMaterialAndColorById(1).material, 5); - matDict[MaterialRegistry.MaterialType.PAPER] = paperCycler; - } - - // Renders mesh highlights. - public static void RenderMeshes(Model model, - HighlightUtils.TrackedHighlightSet meshHighlights, WorldSpace worldSpace) { - foreach (int key in meshHighlights.getKeysForStyle((int)MeshStyles.TUTORIAL_HIGHLIGHT)) { - if (!model.HasMesh(key)) continue; - - Dictionary contexts = - model.meshRepresentationCache.FetchComponentsForMesh( - key, /* abortOnTooManyCacheMisses */ true); - if (contexts == null) { - continue; + public class TutorialHighlightStyle + { + + /// + /// The period of the "glow" animation (the time taken by each repetition of it). + /// Shorter values means a faster animation. Given in seconds. + /// + private const float GLOW_PERIOD = 1.0f; + + /// + /// The maximum emission factor when glowing. More is brighter. + /// + private const float GLOW_MAX_EMISSION = 0.45f; + + private static readonly float HIGHLIGHT_ALPHA = 0.75f; + + /// + /// String names of the shader properties to be changed. + /// + private const string EMISSIVE_AMT_NAME = "_EmissiveAmount"; + private const string EMISSIVE_COLOR_NAME = "_EmissiveColor"; + + /// + /// Base color of the glowing animations. (Greenish) + /// + private static readonly Color GLOW_COLOR = new Color(0.2f, 1.0f, 0.2f); + + public static Material material; + private static Dictionary matDict; + + public static void Setup() + { + matDict = new Dictionary(); + MaterialCycler paperCycler = new MaterialCycler( + MaterialRegistry.GetMaterialAndColorById(1).material, 5); + matDict[MaterialRegistry.MaterialType.PAPER] = paperCycler; } - float factor = Mathf.PingPong(Time.time / GLOW_PERIOD, GLOW_MAX_EMISSION); - - foreach (KeyValuePair pair in contexts) { - int matId = pair.Key; - MeshGenContext meshGenContext = pair.Value; - - bool needToPopulateMesh; - Mesh curMesh = MeshCycler.GetTempMeshForMeshMatId(key, matId, out needToPopulateMesh); - Material curMaterial = matDict[MaterialRegistry.GetMaterialType(matId)].GetInstanceOfMaterial(); - float emissiveAmount = factor; - curMaterial.SetFloat("_EmissiveAmount", factor); - float transparentMult = - curMaterial.GetFloat("_MultiplicitiveAlpha") * (1 - factor) + HIGHLIGHT_ALPHA * factor; - curMaterial.SetFloat("_MultiplicitiveAlpha", transparentMult); - - curMaterial.renderQueue = 3000; - if (needToPopulateMesh) { - curMesh.SetVertices(meshGenContext.verts); - curMesh.SetColors(meshGenContext.colors); - curMesh.SetTriangles(meshGenContext.triangles, /* subMesh */ 0); - curMesh.RecalculateNormals(); - } - - Graphics.DrawMesh(curMesh, worldSpace.modelToWorld, curMaterial, 0); + // Renders mesh highlights. + public static void RenderMeshes(Model model, + HighlightUtils.TrackedHighlightSet meshHighlights, WorldSpace worldSpace) + { + foreach (int key in meshHighlights.getKeysForStyle((int)MeshStyles.TUTORIAL_HIGHLIGHT)) + { + if (!model.HasMesh(key)) continue; + + Dictionary contexts = + model.meshRepresentationCache.FetchComponentsForMesh( + key, /* abortOnTooManyCacheMisses */ true); + if (contexts == null) + { + continue; + } + + float factor = Mathf.PingPong(Time.time / GLOW_PERIOD, GLOW_MAX_EMISSION); + + foreach (KeyValuePair pair in contexts) + { + int matId = pair.Key; + MeshGenContext meshGenContext = pair.Value; + + bool needToPopulateMesh; + Mesh curMesh = MeshCycler.GetTempMeshForMeshMatId(key, matId, out needToPopulateMesh); + Material curMaterial = matDict[MaterialRegistry.GetMaterialType(matId)].GetInstanceOfMaterial(); + float emissiveAmount = factor; + curMaterial.SetFloat("_EmissiveAmount", factor); + float transparentMult = + curMaterial.GetFloat("_MultiplicitiveAlpha") * (1 - factor) + HIGHLIGHT_ALPHA * factor; + curMaterial.SetFloat("_MultiplicitiveAlpha", transparentMult); + + curMaterial.renderQueue = 3000; + if (needToPopulateMesh) + { + curMesh.SetVertices(meshGenContext.verts); + curMesh.SetColors(meshGenContext.colors); + curMesh.SetTriangles(meshGenContext.triangles, /* subMesh */ 0); + curMesh.RecalculateNormals(); + } + + Graphics.DrawMesh(curMesh, worldSpace.modelToWorld, curMaterial, 0); + } + } } - } } - } } \ No newline at end of file diff --git a/Assets/Scripts/tools/utils/HighlightStyles/VertexInactiveStyle.cs b/Assets/Scripts/tools/utils/HighlightStyles/VertexInactiveStyle.cs index a1337016..386981a4 100644 --- a/Assets/Scripts/tools/utils/HighlightStyles/VertexInactiveStyle.cs +++ b/Assets/Scripts/tools/utils/HighlightStyles/VertexInactiveStyle.cs @@ -19,58 +19,63 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// This class exists primarily to hold the static method for RenderVertices when INACTIVE is set. It may be possible to - /// consolidate this with the other Vertex*Style classes in the future. - /// - public class VertexInactiveStyle { +namespace com.google.apps.peltzer.client.tools.utils +{ + /// + /// This class exists primarily to hold the static method for RenderVertices when INACTIVE is set. It may be possible to + /// consolidate this with the other Vertex*Style classes in the future. + /// + public class VertexInactiveStyle + { - public static Material material; - public static Vector3 selectPositionModel; - private static Mesh vertexRenderMesh = new Mesh(); - // Renders vertex highlights. - // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing - // vertex geometry frame to frame) - 37281287 - public static void RenderVertices(Model model, - HighlightUtils.TrackedHighlightSet vertexHighlights, - WorldSpace worldSpace) { - // Renders vertex highlights. - HashSet keys = vertexHighlights.getKeysForStyle((int)VertexStyles.VERTEX_INACTIVE); - if (keys.Count == 0) { return; } - vertexRenderMesh.Clear(); - int[] indices = new int[vertexHighlights.RenderableCount()]; - Vector3[] vertices = new Vector3[vertexHighlights.RenderableCount()]; - // Because Unity does not make a "arbitrary data" vertex channel available to us, we're going to abuse the UV - // channel to pass per-vertex animation state into the shader. - Vector2[] selectData = new Vector2[vertexHighlights.RenderableCount()]; - float radius2 = InactiveSelectionHighlighter.INACTIVE_HIGHLIGHT_RADIUS * - InactiveSelectionHighlighter.INACTIVE_HIGHLIGHT_RADIUS; - int i = 0; - foreach (VertexKey key in keys) { - if (!model.HasMesh(key.meshId)) { continue; } - MMesh mesh = model.GetMesh(key.meshId); - if (!mesh.HasVertex(key.vertexId)) { - continue; - } - vertices[i] = mesh.VertexPositionInModelCoords(key.vertexId); - Vector3 diff = vertices[i] - selectPositionModel; - float dist2 = Vector3.Dot(diff, diff); - float alpha = Mathf.Clamp((radius2 - dist2) / radius2, 0f, 1f); - indices[i] = i; - float animPct = vertexHighlights.GetAnimPct(key); + public static Material material; + public static Vector3 selectPositionModel; + private static Mesh vertexRenderMesh = new Mesh(); + // Renders vertex highlights. + // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing + // vertex geometry frame to frame) - 37281287 + public static void RenderVertices(Model model, + HighlightUtils.TrackedHighlightSet vertexHighlights, + WorldSpace worldSpace) + { + // Renders vertex highlights. + HashSet keys = vertexHighlights.getKeysForStyle((int)VertexStyles.VERTEX_INACTIVE); + if (keys.Count == 0) { return; } + vertexRenderMesh.Clear(); + int[] indices = new int[vertexHighlights.RenderableCount()]; + Vector3[] vertices = new Vector3[vertexHighlights.RenderableCount()]; + // Because Unity does not make a "arbitrary data" vertex channel available to us, we're going to abuse the UV + // channel to pass per-vertex animation state into the shader. + Vector2[] selectData = new Vector2[vertexHighlights.RenderableCount()]; + float radius2 = InactiveSelectionHighlighter.INACTIVE_HIGHLIGHT_RADIUS * + InactiveSelectionHighlighter.INACTIVE_HIGHLIGHT_RADIUS; + int i = 0; + foreach (VertexKey key in keys) + { + if (!model.HasMesh(key.meshId)) { continue; } + MMesh mesh = model.GetMesh(key.meshId); + if (!mesh.HasVertex(key.vertexId)) + { + continue; + } + vertices[i] = mesh.VertexPositionInModelCoords(key.vertexId); + Vector3 diff = vertices[i] - selectPositionModel; + float dist2 = Vector3.Dot(diff, diff); + float alpha = Mathf.Clamp((radius2 - dist2) / radius2, 0f, 1f); + indices[i] = i; + float animPct = vertexHighlights.GetAnimPct(key); - selectData[i] = new Vector2(animPct, alpha); - i++; - } - vertexRenderMesh.vertices = vertices; - // These are not actually UVs - we're using the UV channel to pass per-primitive animation data so that edges - // animate independently. - vertexRenderMesh.uv = selectData; - // Since we're using a point geometry shader we need to set the mesh up to supply data as points. - vertexRenderMesh.SetIndices(indices, MeshTopology.Points, 0 /* submesh id */, false /* recalculate bounds */); + selectData[i] = new Vector2(animPct, alpha); + i++; + } + vertexRenderMesh.vertices = vertices; + // These are not actually UVs - we're using the UV channel to pass per-primitive animation data so that edges + // animate independently. + vertexRenderMesh.uv = selectData; + // Since we're using a point geometry shader we need to set the mesh up to supply data as points. + vertexRenderMesh.SetIndices(indices, MeshTopology.Points, 0 /* submesh id */, false /* recalculate bounds */); - Graphics.DrawMesh(vertexRenderMesh, worldSpace.modelToWorld, material, 0); + Graphics.DrawMesh(vertexRenderMesh, worldSpace.modelToWorld, material, 0); + } } - } } diff --git a/Assets/Scripts/tools/utils/HighlightStyles/VertexSelectStyle.cs b/Assets/Scripts/tools/utils/HighlightStyles/VertexSelectStyle.cs index 8254f124..74208408 100644 --- a/Assets/Scripts/tools/utils/HighlightStyles/VertexSelectStyle.cs +++ b/Assets/Scripts/tools/utils/HighlightStyles/VertexSelectStyle.cs @@ -19,54 +19,59 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// This class exists primarily to hold the static method for RenderVertices when SELECT is set. It may be possible to - /// consolidate this with the other Vertes*Style classes in the future. - /// - public class VertexSelectStyle { +namespace com.google.apps.peltzer.client.tools.utils +{ + /// + /// This class exists primarily to hold the static method for RenderVertices when SELECT is set. It may be possible to + /// consolidate this with the other Vertes*Style classes in the future. + /// + public class VertexSelectStyle + { - public static Material material; - private static Mesh vertexRenderMesh = new Mesh(); - // Renders vertex highlights. - // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing - // vertex geometry frame to frame) - 37281287 - public static void RenderVertices(Model model, - HighlightUtils.TrackedHighlightSet vertexHighlights, - WorldSpace worldSpace) { - // Renders vertex highlights. - HashSet keys = vertexHighlights.getKeysForStyle((int)VertexStyles.VERTEX_SELECT); - if (keys.Count == 0) { return; } - vertexRenderMesh.Clear(); - int[] indices = new int[vertexHighlights.RenderableCount()]; - Vector3[] vertices = new Vector3[vertexHighlights.RenderableCount()]; - // Because Unity does not make a "arbitrary data" vertex channel available to us, we're going to abuse the UV - // channel to pass per-vertex animation state into the shader. - Vector2[] selectData = new Vector2[vertexHighlights.RenderableCount()]; - float scaleFactor = InactiveRenderer.GetVertScaleFactor(worldSpace); - material.SetFloat("_PointSphereRadius", scaleFactor); - int i = 0; - foreach (VertexKey key in keys) { - if (!model.HasMesh(key.meshId)) { continue; } - MMesh mesh = model.GetMesh(key.meshId); - if (!mesh.HasVertex(key.vertexId)) { - continue; - } - vertices[i] = mesh.VertexPositionInModelCoords(key.vertexId); - indices[i] = i; - float animPct = vertexHighlights.GetAnimPct(key); + public static Material material; + private static Mesh vertexRenderMesh = new Mesh(); + // Renders vertex highlights. + // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing + // vertex geometry frame to frame) - 37281287 + public static void RenderVertices(Model model, + HighlightUtils.TrackedHighlightSet vertexHighlights, + WorldSpace worldSpace) + { + // Renders vertex highlights. + HashSet keys = vertexHighlights.getKeysForStyle((int)VertexStyles.VERTEX_SELECT); + if (keys.Count == 0) { return; } + vertexRenderMesh.Clear(); + int[] indices = new int[vertexHighlights.RenderableCount()]; + Vector3[] vertices = new Vector3[vertexHighlights.RenderableCount()]; + // Because Unity does not make a "arbitrary data" vertex channel available to us, we're going to abuse the UV + // channel to pass per-vertex animation state into the shader. + Vector2[] selectData = new Vector2[vertexHighlights.RenderableCount()]; + float scaleFactor = InactiveRenderer.GetVertScaleFactor(worldSpace); + material.SetFloat("_PointSphereRadius", scaleFactor); + int i = 0; + foreach (VertexKey key in keys) + { + if (!model.HasMesh(key.meshId)) { continue; } + MMesh mesh = model.GetMesh(key.meshId); + if (!mesh.HasVertex(key.vertexId)) + { + continue; + } + vertices[i] = mesh.VertexPositionInModelCoords(key.vertexId); + indices[i] = i; + float animPct = vertexHighlights.GetAnimPct(key); - selectData[i] = new Vector2(animPct, 1.0f); - i++; - } - vertexRenderMesh.vertices = vertices; - // These are not actually UVs - we're using the UV channel to pass per-primitive animation data so that edges - // animate independently. - vertexRenderMesh.uv = selectData; - // Since we're using a point geometry shader we need to set the mesh up to supply data as points. - vertexRenderMesh.SetIndices(indices, MeshTopology.Points, 0 /* submesh id */, false /* recalculate bounds */); + selectData[i] = new Vector2(animPct, 1.0f); + i++; + } + vertexRenderMesh.vertices = vertices; + // These are not actually UVs - we're using the UV channel to pass per-primitive animation data so that edges + // animate independently. + vertexRenderMesh.uv = selectData; + // Since we're using a point geometry shader we need to set the mesh up to supply data as points. + vertexRenderMesh.SetIndices(indices, MeshTopology.Points, 0 /* submesh id */, false /* recalculate bounds */); - Graphics.DrawMesh(vertexRenderMesh, worldSpace.modelToWorld, material, 0); + Graphics.DrawMesh(vertexRenderMesh, worldSpace.modelToWorld, material, 0); + } } - } } diff --git a/Assets/Scripts/tools/utils/HighlightUtils.cs b/Assets/Scripts/tools/utils/HighlightUtils.cs index c95f0d75..283c4c5c 100644 --- a/Assets/Scripts/tools/utils/HighlightUtils.cs +++ b/Assets/Scripts/tools/utils/HighlightUtils.cs @@ -20,548 +20,629 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// These enums help to specify a style in which an effect for a vertex, edge, or face is rendered. This is - /// orthogonal to whether the element is turned on or off (though once an element finishes animating off its style - /// will be cleared). - /// - public enum VertexStyles : int { - VERTEX_SELECT=0, - VERTEX_INACTIVE - }; - - public enum EdgeStyles : int { - EDGE_SELECT=0, - EDGE_INACTIVE, - }; - - public enum FaceStyles : int { - FACE_SELECT=0, - PAINT, // Uses vertex colors for highlight color - EXTRUDE - }; - - public enum MeshStyles : int { - MESH_SELECT=0, - MESH_DELETE, - MESH_PAINT, - TUTORIAL_HIGHLIGHT - }; - - /// - /// Manages the presentation (rendering and animation) of highlighted geometry elements based on highlight state - /// supplied by tools that need highlight UI. - /// - public class HighlightUtils : MonoBehaviour { - - public static readonly float DEFAULT_EDGE_DIMENSION = 0.001f; +namespace com.google.apps.peltzer.client.tools.utils +{ + /// + /// These enums help to specify a style in which an effect for a vertex, edge, or face is rendered. This is + /// orthogonal to whether the element is turned on or off (though once an element finishes animating off its style + /// will be cleared). + /// + public enum VertexStyles : int + { + VERTEX_SELECT = 0, + VERTEX_INACTIVE + }; + + public enum EdgeStyles : int + { + EDGE_SELECT = 0, + EDGE_INACTIVE, + }; + + public enum FaceStyles : int + { + FACE_SELECT = 0, + PAINT, // Uses vertex colors for highlight color + EXTRUDE + }; + + public enum MeshStyles : int + { + MESH_SELECT = 0, + MESH_DELETE, + MESH_PAINT, + TUTORIAL_HIGHLIGHT + }; /// - /// Internal class for tracking highlight information common to highlighting edges, faces, and vertices. - /// Mostly, this manages in and out animation timing. + /// Manages the presentation (rendering and animation) of highlighted geometry elements based on highlight state + /// supplied by tools that need highlight UI. /// - /// - public class TrackedHighlightSet { - // Pairs of element keys and times of their animation events. For fade-in, the time is the time in seconds at - // which the element should start fading in. For fade-out, the time is the negative of the time at which the - // element should start fading out. - private Dictionary selectTimes; - private Dictionary styles; - private Dictionary animationInDurations; - // While we can derive this from the above, we'd need to do so every frame so it's cheaper to maintain as we go. - private Dictionary> elementsByStyle; - private float animationDurationIn; - private float animationDurationOut; - - //Exposed for renderers to optimize against. - public HashSet newlyAdded; - public HashSet newlyRemoved; - // Custom channels which may need to be used for various effects, containing data such as effect origin point and - // color. - // A given effect should use these in order - ie, customChannel1 should not be used unless customChannel0 is - // already being used. - // A given style id should be tied to a specific usage of the data in these channels, and there's probably an - // elegant way to enforce that. But for now we use the honor system. - private Dictionary customChannel0; - private Dictionary customChannel1; - - private void ClearCustomChannels(T key) { - if (customChannel0.ContainsKey(key)) { - customChannel0.Remove(key); - // Should only need to check channel 1 if there was data in channel 0. - if (customChannel1.ContainsKey(key)) { - customChannel1.Remove(key); - } - } - } - - /// The duration of a fade-in or a fade-out - public TrackedHighlightSet(float animationDurationIn, - float animationDurationOut, - IEnumerable permittedStyles) { - selectTimes = new Dictionary(); - styles = new Dictionary(); - animationInDurations = new Dictionary(); - elementsByStyle = new Dictionary>(); - this.animationDurationIn = animationDurationIn; - this.animationDurationOut = animationDurationOut; - foreach (int styleEnumId in permittedStyles) { - elementsByStyle[styleEnumId] = new HashSet(); - } - this.customChannel0 = new Dictionary(); - this.customChannel1 = new Dictionary(); - this.newlyAdded = new HashSet(); - this.newlyRemoved = new HashSet(); - } - - /// - /// Returns the number of renderable elements (distinct from the number of selected elements. - /// - /// - public int RenderableCount() { - return selectTimes.Count; - } - - // Turns highlighting for a given element on (it will animate in over animationDuration seconds) - // Optionally accepts an animation duration to override the default. - public void TurnOn(T key, float? durationIn = null) { - float thisAnimationDuration; - if (!animationInDurations.TryGetValue(key, out thisAnimationDuration)) { - thisAnimationDuration = durationIn == null ? animationDurationIn : durationIn.Value; - animationInDurations[key] = thisAnimationDuration; - } - - float selectTime; - if (!selectTimes.TryGetValue(key, out selectTime)) { - selectTimes[key] = Time.time; - newlyAdded.Add(key); - } else { - if (selectTime < 0) { - float curPct = Mathf.Min(1.0f, (Time.time + selectTime) / animationDurationOut); - selectTimes[key] = - Mathf.Min(Time.time, Time.time + thisAnimationDuration - curPct * thisAnimationDuration); - } - } - if (!styles.ContainsKey(key)) { - styles.Add(key, 0); - elementsByStyle[0].Add(key); - customChannel0.Add(key, Vector4.zero); - customChannel1.Add(key, Vector4.zero); - } - } - - public void SetStyle(T key, int styleId) { - SetStyle(key, styleId, Vector4.zero, Vector4.zero); - } - - public void SetStyle(T key, int styleId, Vector4 channelData0) { - SetStyle(key, styleId, channelData0, Vector4.zero); - } - - public void SetStyle(T key, int styleId, Vector4 channelData0, Vector4 channelData1) { - int oldStyle; - if (styles.TryGetValue(key, out oldStyle)) { - elementsByStyle[oldStyle].Remove(key); - } - styles[key] = styleId; - elementsByStyle[styleId].Add(key); - customChannel0[key] = channelData0; - customChannel1[key] = channelData1; - } - - // Turns highlighting for a given element on immediately - public void TurnOnImmediate(T key) { - selectTimes[key] = Time.time - animationInDurations[key]; - if (!styles.ContainsKey(key)) { - styles.Add(key, 0); - } - } - - // Turns highlighting for a given element off (it will still display until it finishes animating out) - public void TurnOff(T key) { - if (selectTimes.ContainsKey(key)) { - if (selectTimes[key] > 0) { - float curPct = Mathf.Min(1.0f, (Time.time - selectTimes[key]) / animationInDurations[key]); - selectTimes[key] = Mathf.Max(-Time.time, -(Time.time + - (animationDurationOut - curPct * animationDurationOut))); - } - } - } - - // Turns highlighting for a given element off (it will still display until it finishes animating out) - public void TurnOffImmediate(T key) { - if (selectTimes.ContainsKey(key)) { - if (selectTimes[key] > 0) { - selectTimes[key] = -Time.time + animationDurationOut; - } - } - } - - public IEnumerable Keys() { - return selectTimes.Keys; - } - - /// - /// Returns the percentage of the way through the animation the given element is, with 100% being fully visible - /// and 0% being hidden. - /// - /// - /// - public float GetAnimPct(T key) { - float curPct = 0f; - if (!selectTimes.ContainsKey(key)) return curPct; - if (selectTimes[key] > 0) return Mathf.Min(1.0f, (Time.time - selectTimes[key]) / animationInDurations[key]); - return 1.0f - Mathf.Min(1.0f, (Time.time + selectTimes[key]) / animationDurationOut); - } - - public HashSet getKeysForStyle(int styleId) { - return elementsByStyle[styleId]; - } - - public void ClearAll() { - newlyRemoved.UnionWith(selectTimes.Keys); - selectTimes.Clear(); - styles.Clear(); - foreach (int styleId in elementsByStyle.Keys) { - elementsByStyle[styleId].Clear(); - } - customChannel0.Clear(); - customChannel1.Clear(); - newlyAdded.Clear(); - } - - public Vector4 GetCustomChannel0(T key) { - return customChannel0[key]; - } - - public Vector4 GetCustomChannel1(T key) { - return customChannel1[key]; - } - - // Removes all animations that have completed fading out - public void ClearExpired() { - newlyAdded.Clear(); - newlyRemoved.Clear(); - List keysToRemove = new List(); - foreach (T key in selectTimes.Keys) { - if (selectTimes[key] < 0) { - if (Time.time + selectTimes[key] > animationDurationOut) { - keysToRemove.Add(key); + public class HighlightUtils : MonoBehaviour + { + + public static readonly float DEFAULT_EDGE_DIMENSION = 0.001f; + + /// + /// Internal class for tracking highlight information common to highlighting edges, faces, and vertices. + /// Mostly, this manages in and out animation timing. + /// + /// + public class TrackedHighlightSet + { + // Pairs of element keys and times of their animation events. For fade-in, the time is the time in seconds at + // which the element should start fading in. For fade-out, the time is the negative of the time at which the + // element should start fading out. + private Dictionary selectTimes; + private Dictionary styles; + private Dictionary animationInDurations; + // While we can derive this from the above, we'd need to do so every frame so it's cheaper to maintain as we go. + private Dictionary> elementsByStyle; + private float animationDurationIn; + private float animationDurationOut; + + //Exposed for renderers to optimize against. + public HashSet newlyAdded; + public HashSet newlyRemoved; + // Custom channels which may need to be used for various effects, containing data such as effect origin point and + // color. + // A given effect should use these in order - ie, customChannel1 should not be used unless customChannel0 is + // already being used. + // A given style id should be tied to a specific usage of the data in these channels, and there's probably an + // elegant way to enforce that. But for now we use the honor system. + private Dictionary customChannel0; + private Dictionary customChannel1; + + private void ClearCustomChannels(T key) + { + if (customChannel0.ContainsKey(key)) + { + customChannel0.Remove(key); + // Should only need to check channel 1 if there was data in channel 0. + if (customChannel1.ContainsKey(key)) + { + customChannel1.Remove(key); + } + } + } + + /// The duration of a fade-in or a fade-out + public TrackedHighlightSet(float animationDurationIn, + float animationDurationOut, + IEnumerable permittedStyles) + { + selectTimes = new Dictionary(); + styles = new Dictionary(); + animationInDurations = new Dictionary(); + elementsByStyle = new Dictionary>(); + this.animationDurationIn = animationDurationIn; + this.animationDurationOut = animationDurationOut; + foreach (int styleEnumId in permittedStyles) + { + elementsByStyle[styleEnumId] = new HashSet(); + } + this.customChannel0 = new Dictionary(); + this.customChannel1 = new Dictionary(); + this.newlyAdded = new HashSet(); + this.newlyRemoved = new HashSet(); + } + + /// + /// Returns the number of renderable elements (distinct from the number of selected elements. + /// + /// + public int RenderableCount() + { + return selectTimes.Count; + } + + // Turns highlighting for a given element on (it will animate in over animationDuration seconds) + // Optionally accepts an animation duration to override the default. + public void TurnOn(T key, float? durationIn = null) + { + float thisAnimationDuration; + if (!animationInDurations.TryGetValue(key, out thisAnimationDuration)) + { + thisAnimationDuration = durationIn == null ? animationDurationIn : durationIn.Value; + animationInDurations[key] = thisAnimationDuration; + } + + float selectTime; + if (!selectTimes.TryGetValue(key, out selectTime)) + { + selectTimes[key] = Time.time; + newlyAdded.Add(key); + } + else + { + if (selectTime < 0) + { + float curPct = Mathf.Min(1.0f, (Time.time + selectTime) / animationDurationOut); + selectTimes[key] = + Mathf.Min(Time.time, Time.time + thisAnimationDuration - curPct * thisAnimationDuration); + } + } + if (!styles.ContainsKey(key)) + { + styles.Add(key, 0); + elementsByStyle[0].Add(key); + customChannel0.Add(key, Vector4.zero); + customChannel1.Add(key, Vector4.zero); + } + } + + public void SetStyle(T key, int styleId) + { + SetStyle(key, styleId, Vector4.zero, Vector4.zero); + } + + public void SetStyle(T key, int styleId, Vector4 channelData0) + { + SetStyle(key, styleId, channelData0, Vector4.zero); + } + + public void SetStyle(T key, int styleId, Vector4 channelData0, Vector4 channelData1) + { + int oldStyle; + if (styles.TryGetValue(key, out oldStyle)) + { + elementsByStyle[oldStyle].Remove(key); + } + styles[key] = styleId; + elementsByStyle[styleId].Add(key); + customChannel0[key] = channelData0; + customChannel1[key] = channelData1; + } + + // Turns highlighting for a given element on immediately + public void TurnOnImmediate(T key) + { + selectTimes[key] = Time.time - animationInDurations[key]; + if (!styles.ContainsKey(key)) + { + styles.Add(key, 0); + } + } + + // Turns highlighting for a given element off (it will still display until it finishes animating out) + public void TurnOff(T key) + { + if (selectTimes.ContainsKey(key)) + { + if (selectTimes[key] > 0) + { + float curPct = Mathf.Min(1.0f, (Time.time - selectTimes[key]) / animationInDurations[key]); + selectTimes[key] = Mathf.Max(-Time.time, -(Time.time + + (animationDurationOut - curPct * animationDurationOut))); + } + } + } + + // Turns highlighting for a given element off (it will still display until it finishes animating out) + public void TurnOffImmediate(T key) + { + if (selectTimes.ContainsKey(key)) + { + if (selectTimes[key] > 0) + { + selectTimes[key] = -Time.time + animationDurationOut; + } + } + } + + public IEnumerable Keys() + { + return selectTimes.Keys; + } + + /// + /// Returns the percentage of the way through the animation the given element is, with 100% being fully visible + /// and 0% being hidden. + /// + /// + /// + public float GetAnimPct(T key) + { + float curPct = 0f; + if (!selectTimes.ContainsKey(key)) return curPct; + if (selectTimes[key] > 0) return Mathf.Min(1.0f, (Time.time - selectTimes[key]) / animationInDurations[key]); + return 1.0f - Mathf.Min(1.0f, (Time.time + selectTimes[key]) / animationDurationOut); + } + + public HashSet getKeysForStyle(int styleId) + { + return elementsByStyle[styleId]; + } + + public void ClearAll() + { + newlyRemoved.UnionWith(selectTimes.Keys); + selectTimes.Clear(); + styles.Clear(); + foreach (int styleId in elementsByStyle.Keys) + { + elementsByStyle[styleId].Clear(); + } + customChannel0.Clear(); + customChannel1.Clear(); + newlyAdded.Clear(); + } + + public Vector4 GetCustomChannel0(T key) + { + return customChannel0[key]; + } + + public Vector4 GetCustomChannel1(T key) + { + return customChannel1[key]; + } + + // Removes all animations that have completed fading out + public void ClearExpired() + { + newlyAdded.Clear(); + newlyRemoved.Clear(); + List keysToRemove = new List(); + foreach (T key in selectTimes.Keys) + { + if (selectTimes[key] < 0) + { + if (Time.time + selectTimes[key] > animationDurationOut) + { + keysToRemove.Add(key); + } + } + } + foreach (T key in keysToRemove) + { + selectTimes.Remove(key); + elementsByStyle[styles[key]].Remove(key); + styles.Remove(key); + ClearCustomChannels(key); + newlyRemoved.Add(key); + } } - } - } - foreach (T key in keysToRemove) { - selectTimes.Remove(key); - elementsByStyle[styles[key]].Remove(key); - styles.Remove(key); - ClearCustomChannels(key); - newlyRemoved.Add(key); } - } - } - private WorldSpace worldSpace; - private Model model; - - private TrackedHighlightSet vertexHighlights; - private TrackedHighlightSet edgeHighlights; - private TrackedHighlightSet temporaryEdgeHighlights; - private TrackedHighlightSet faceHighlights; - private TrackedHighlightSet meshHighlights; - public InactiveRenderer inactiveRenderer; - private bool isSetup = false; - - public const float VERT_EDGE_ANIMATION_DURATION_IN = 0.08f; - public const float VERT_EDGE_ANIMATION_DURATION_OUT = 0.08f; - public const float FACE_ANIMATION_DURATION_IN = 0.4f; - public const float FACE_ANIMATION_DURATION_OUT = 0.15f; - public const float MESH_ANIMATION_DURATION_IN = 0.4f; - public const float MESH_ANIMATION_DURATION_OUT = 0.15f; - // Face highlight animation durations are (inversely) correlated with face size. - // This is the base for that calculation. - private const float BASE_FACE_HIGHLIGHT_DURATION = 0.1125f; - // Mesh highlight animation durations are (inversely) correlated with face size. - // This is the base for that calculation. - private const float MESH_FACE_HIGHLIGHT_DURATION = 0.225f; - /// - /// Sets up materials and data structures for managing highlights. - /// - public void Setup(WorldSpace worldSpace, Model model, MaterialLibrary materialLibrary) { - this.worldSpace = worldSpace; - this.model = model; - EdgeSelectStyle.material = new Material(materialLibrary.edgeHighlightMaterial); - EdgeInactiveStyle.material = new Material(materialLibrary.edgeInactiveMaterial); - EdgeTemporaryStyle.material = new Material(materialLibrary.edgeHighlightMaterial); - FaceSelectStyle.material = new Material(materialLibrary.faceHighlightMaterial); - FacePaintStyle.material = new Material(materialLibrary.facePaintMaterial); - FaceExtrudeStyle.material = new Material(materialLibrary.faceExtrudeMaterial); - MeshSelectStyle.material = new Material(materialLibrary.meshSelectMaterial); - MeshSelectStyle.silhouetteMaterial = new Material(materialLibrary.highlightSilhouetteMaterial); - MeshPaintStyle.material = new Material(materialLibrary.meshSelectMaterial); - VertexSelectStyle.material = new Material(materialLibrary.pointHighlightMaterial); - VertexInactiveStyle.material = new Material(materialLibrary.pointInactiveMaterial); - TutorialHighlightStyle.material = materialLibrary.meshSelectMaterial; - - - MeshSelectStyle.Setup(); - MeshPaintStyle.Setup(); - MeshDeleteStyle.Setup(); - TutorialHighlightStyle.Setup(); - - inactiveRenderer = new InactiveRenderer(model, worldSpace, materialLibrary); - vertexHighlights = new TrackedHighlightSet(VERT_EDGE_ANIMATION_DURATION_IN, - VERT_EDGE_ANIMATION_DURATION_OUT, - new [] {(int)VertexStyles.VERTEX_SELECT, (int)VertexStyles.VERTEX_INACTIVE}); - - edgeHighlights = new TrackedHighlightSet(VERT_EDGE_ANIMATION_DURATION_IN, - VERT_EDGE_ANIMATION_DURATION_OUT, - new [] {(int)EdgeStyles.EDGE_SELECT, (int)EdgeStyles.EDGE_INACTIVE}); - - temporaryEdgeHighlights = new TrackedHighlightSet(VERT_EDGE_ANIMATION_DURATION_IN, - VERT_EDGE_ANIMATION_DURATION_OUT, - new[] { (int)EdgeStyles.EDGE_SELECT}); - - faceHighlights = new TrackedHighlightSet(FACE_ANIMATION_DURATION_IN, - FACE_ANIMATION_DURATION_OUT, - new [] { + private WorldSpace worldSpace; + private Model model; + + private TrackedHighlightSet vertexHighlights; + private TrackedHighlightSet edgeHighlights; + private TrackedHighlightSet temporaryEdgeHighlights; + private TrackedHighlightSet faceHighlights; + private TrackedHighlightSet meshHighlights; + public InactiveRenderer inactiveRenderer; + private bool isSetup = false; + + public const float VERT_EDGE_ANIMATION_DURATION_IN = 0.08f; + public const float VERT_EDGE_ANIMATION_DURATION_OUT = 0.08f; + public const float FACE_ANIMATION_DURATION_IN = 0.4f; + public const float FACE_ANIMATION_DURATION_OUT = 0.15f; + public const float MESH_ANIMATION_DURATION_IN = 0.4f; + public const float MESH_ANIMATION_DURATION_OUT = 0.15f; + // Face highlight animation durations are (inversely) correlated with face size. + // This is the base for that calculation. + private const float BASE_FACE_HIGHLIGHT_DURATION = 0.1125f; + // Mesh highlight animation durations are (inversely) correlated with face size. + // This is the base for that calculation. + private const float MESH_FACE_HIGHLIGHT_DURATION = 0.225f; + /// + /// Sets up materials and data structures for managing highlights. + /// + public void Setup(WorldSpace worldSpace, Model model, MaterialLibrary materialLibrary) + { + this.worldSpace = worldSpace; + this.model = model; + EdgeSelectStyle.material = new Material(materialLibrary.edgeHighlightMaterial); + EdgeInactiveStyle.material = new Material(materialLibrary.edgeInactiveMaterial); + EdgeTemporaryStyle.material = new Material(materialLibrary.edgeHighlightMaterial); + FaceSelectStyle.material = new Material(materialLibrary.faceHighlightMaterial); + FacePaintStyle.material = new Material(materialLibrary.facePaintMaterial); + FaceExtrudeStyle.material = new Material(materialLibrary.faceExtrudeMaterial); + MeshSelectStyle.material = new Material(materialLibrary.meshSelectMaterial); + MeshSelectStyle.silhouetteMaterial = new Material(materialLibrary.highlightSilhouetteMaterial); + MeshPaintStyle.material = new Material(materialLibrary.meshSelectMaterial); + VertexSelectStyle.material = new Material(materialLibrary.pointHighlightMaterial); + VertexInactiveStyle.material = new Material(materialLibrary.pointInactiveMaterial); + TutorialHighlightStyle.material = materialLibrary.meshSelectMaterial; + + + MeshSelectStyle.Setup(); + MeshPaintStyle.Setup(); + MeshDeleteStyle.Setup(); + TutorialHighlightStyle.Setup(); + + inactiveRenderer = new InactiveRenderer(model, worldSpace, materialLibrary); + vertexHighlights = new TrackedHighlightSet(VERT_EDGE_ANIMATION_DURATION_IN, + VERT_EDGE_ANIMATION_DURATION_OUT, + new[] { (int)VertexStyles.VERTEX_SELECT, (int)VertexStyles.VERTEX_INACTIVE }); + + edgeHighlights = new TrackedHighlightSet(VERT_EDGE_ANIMATION_DURATION_IN, + VERT_EDGE_ANIMATION_DURATION_OUT, + new[] { (int)EdgeStyles.EDGE_SELECT, (int)EdgeStyles.EDGE_INACTIVE }); + + temporaryEdgeHighlights = new TrackedHighlightSet(VERT_EDGE_ANIMATION_DURATION_IN, + VERT_EDGE_ANIMATION_DURATION_OUT, + new[] { (int)EdgeStyles.EDGE_SELECT }); + + faceHighlights = new TrackedHighlightSet(FACE_ANIMATION_DURATION_IN, + FACE_ANIMATION_DURATION_OUT, + new[] { (int)FaceStyles.FACE_SELECT, (int)FaceStyles.PAINT, (int)FaceStyles.EXTRUDE - }); + }); - meshHighlights = new TrackedHighlightSet(MESH_ANIMATION_DURATION_IN, - MESH_ANIMATION_DURATION_OUT, - new [] {(int)MeshStyles.MESH_SELECT, (int)MeshStyles.MESH_DELETE, (int)MeshStyles.MESH_PAINT, + meshHighlights = new TrackedHighlightSet(MESH_ANIMATION_DURATION_IN, + MESH_ANIMATION_DURATION_OUT, + new[] {(int)MeshStyles.MESH_SELECT, (int)MeshStyles.MESH_DELETE, (int)MeshStyles.MESH_PAINT, (int)MeshStyles.TUTORIAL_HIGHLIGHT}); - } + } - // Turns the highlight on for the edge with the supplied key, optionally over the given duration. - public void TurnOn(EdgeKey key, float? durationIn = null) { - edgeHighlights.TurnOn(key, durationIn); - } + // Turns the highlight on for the edge with the supplied key, optionally over the given duration. + public void TurnOn(EdgeKey key, float? durationIn = null) + { + edgeHighlights.TurnOn(key, durationIn); + } - // Turns the highlight off for the edge with the supplied key. - public void TurnOff(EdgeKey key) { - edgeHighlights.TurnOff(key); - } + // Turns the highlight off for the edge with the supplied key. + public void TurnOff(EdgeKey key) + { + edgeHighlights.TurnOff(key); + } - // Turns highlights off immediately for all edges (no fade-out). - public void ClearEdges() { - edgeHighlights.ClearAll(); - } + // Turns highlights off immediately for all edges (no fade-out). + public void ClearEdges() + { + edgeHighlights.ClearAll(); + } - // Turns the highlight on for the temporary edge with the supplied key, optionally over the given duration. - public void TurnOn(EdgeTemporaryStyle.TemporaryEdge key, float? durationIn = null) { - temporaryEdgeHighlights.TurnOn(key, durationIn); - } + // Turns the highlight on for the temporary edge with the supplied key, optionally over the given duration. + public void TurnOn(EdgeTemporaryStyle.TemporaryEdge key, float? durationIn = null) + { + temporaryEdgeHighlights.TurnOn(key, durationIn); + } - // Turns the highlight off for the temporary edge with the supplied key. - public void TurnOff(EdgeTemporaryStyle.TemporaryEdge key) { - temporaryEdgeHighlights.TurnOff(key); - } + // Turns the highlight off for the temporary edge with the supplied key. + public void TurnOff(EdgeTemporaryStyle.TemporaryEdge key) + { + temporaryEdgeHighlights.TurnOff(key); + } - // Turns highlights off immediately for all temporary edges (no fade-out). - public void ClearTemporaryEdges() { - temporaryEdgeHighlights.ClearAll(); - } + // Turns highlights off immediately for all temporary edges (no fade-out). + public void ClearTemporaryEdges() + { + temporaryEdgeHighlights.ClearAll(); + } - // Turns the highlight on for the vertex with the supplied key. - public void TurnOn(VertexKey key) { - vertexHighlights.TurnOn(key); - } + // Turns the highlight on for the vertex with the supplied key. + public void TurnOn(VertexKey key) + { + vertexHighlights.TurnOn(key); + } - // Turns the highlight off for the vertex with the supplied key. - public void TurnOff(VertexKey key) { - vertexHighlights.TurnOff(key); - } + // Turns the highlight off for the vertex with the supplied key. + public void TurnOff(VertexKey key) + { + vertexHighlights.TurnOff(key); + } - // Turns highlighting off immediately for all vertices (no fade-out). - public void ClearVertices() { - vertexHighlights.ClearAll(); - } + // Turns highlighting off immediately for all vertices (no fade-out). + public void ClearVertices() + { + vertexHighlights.ClearAll(); + } - // Sets the style for the specified vertex highlight to Select. - public void SetVertexStyleToSelect(VertexKey key) { - vertexHighlights.SetStyle(key, (int) VertexStyles.VERTEX_SELECT); - } + // Sets the style for the specified vertex highlight to Select. + public void SetVertexStyleToSelect(VertexKey key) + { + vertexHighlights.SetStyle(key, (int)VertexStyles.VERTEX_SELECT); + } - // Sets the style for the specified vertex highlight to inactive. - public void SetVertexStyleToInactive(VertexKey key) { - vertexHighlights.SetStyle(key, (int) VertexStyles.VERTEX_INACTIVE); - } - - // Sets the style for the specified edge highlight to Select. - public void SetEdgeStyleToSelect(EdgeKey key) { - edgeHighlights.SetStyle(key, (int) EdgeStyles.EDGE_SELECT); - } + // Sets the style for the specified vertex highlight to inactive. + public void SetVertexStyleToInactive(VertexKey key) + { + vertexHighlights.SetStyle(key, (int)VertexStyles.VERTEX_INACTIVE); + } - // Sets the style for the specified edge highlight to inactive. - public void SetEdgeStyleToInactive(EdgeKey key) { - edgeHighlights.SetStyle(key, (int) EdgeStyles.EDGE_INACTIVE); - } + // Sets the style for the specified edge highlight to Select. + public void SetEdgeStyleToSelect(EdgeKey key) + { + edgeHighlights.SetStyle(key, (int)EdgeStyles.EDGE_SELECT); + } - // Sets the style for the specified edge highlight to Select. - public void SetTemporaryEdgeStyleToSelect(EdgeTemporaryStyle.TemporaryEdge key) { - temporaryEdgeHighlights.SetStyle(key, (int)EdgeStyles.EDGE_SELECT); - } + // Sets the style for the specified edge highlight to inactive. + public void SetEdgeStyleToInactive(EdgeKey key) + { + edgeHighlights.SetStyle(key, (int)EdgeStyles.EDGE_INACTIVE); + } - // Sets the style for the specified face highlight to Paint, using the specified position as the origin of the - // effect, and the color as the paint color. - public void SetFaceStyleToPaint(FaceKey key, Vector3 position, Color color) { - Vector3 worldSelectPosition = worldSpace.ModelToWorld(position); - faceHighlights.SetStyle(key, - (int)FaceStyles.PAINT, - new Vector4(worldSelectPosition.x, worldSelectPosition.y, worldSelectPosition.z, 1f), - new Vector4(color.r, color.g, color.b, color.a)); - } + // Sets the style for the specified edge highlight to Select. + public void SetTemporaryEdgeStyleToSelect(EdgeTemporaryStyle.TemporaryEdge key) + { + temporaryEdgeHighlights.SetStyle(key, (int)EdgeStyles.EDGE_SELECT); + } - // Sets the style for the specified face highlight to Extrude, using the specified position as the origin of the - // effect, and the color as the paint color. (Extrude effect is WIP, so these args may change) - public void SetFaceStyleToExtrude(FaceKey key, Vector3 positionModel, Color color) { - faceHighlights.SetStyle(key, - (int)FaceStyles.EXTRUDE, - new Vector4(positionModel.x, positionModel.y, positionModel.z, 1f), - new Vector4(color.r, color.g, color.b, color.a)); - } + // Sets the style for the specified face highlight to Paint, using the specified position as the origin of the + // effect, and the color as the paint color. + public void SetFaceStyleToPaint(FaceKey key, Vector3 position, Color color) + { + Vector3 worldSelectPosition = worldSpace.ModelToWorld(position); + faceHighlights.SetStyle(key, + (int)FaceStyles.PAINT, + new Vector4(worldSelectPosition.x, worldSelectPosition.y, worldSelectPosition.z, 1f), + new Vector4(color.r, color.g, color.b, color.a)); + } - // Sets the style for the specified face highlight to Select, using the specified position as the origin of the - // selection animation. - public void SetFaceStyleToSelect(FaceKey key, Vector3 position) { - Vector3 worldSelectPosition = worldSpace.ModelToWorld(position); - faceHighlights.SetStyle(key, - (int) FaceStyles.FACE_SELECT, - new Vector4(worldSelectPosition.x, worldSelectPosition.y, worldSelectPosition.z, 1f)); - } + // Sets the style for the specified face highlight to Extrude, using the specified position as the origin of the + // effect, and the color as the paint color. (Extrude effect is WIP, so these args may change) + public void SetFaceStyleToExtrude(FaceKey key, Vector3 positionModel, Color color) + { + faceHighlights.SetStyle(key, + (int)FaceStyles.EXTRUDE, + new Vector4(positionModel.x, positionModel.y, positionModel.z, 1f), + new Vector4(color.r, color.g, color.b, color.a)); + } - // Turns the highlight on for the vertex with the supplied key. - public void TurnOn(FaceKey key, Vector3 selectionPos) { - MMesh mesh = model.GetMesh(key.meshId); - Face face = mesh.GetFace(key.faceId); + // Sets the style for the specified face highlight to Select, using the specified position as the origin of the + // selection animation. + public void SetFaceStyleToSelect(FaceKey key, Vector3 position) + { + Vector3 worldSelectPosition = worldSpace.ModelToWorld(position); + faceHighlights.SetStyle(key, + (int)FaceStyles.FACE_SELECT, + new Vector4(worldSelectPosition.x, worldSelectPosition.y, worldSelectPosition.z, 1f)); + } - // Assuming most faces have only a handful of verts, this is pretty cheap. - Bounds bounds = new Bounds(mesh.VertexPositionInModelCoords(face.vertexIds[0]), Vector3.zero); - for (int i = 1; i < face.vertexIds.Count; i++) { - bounds.Encapsulate(mesh.VertexPositionInModelCoords(face.vertexIds[i])); - } + // Turns the highlight on for the vertex with the supplied key. + public void TurnOn(FaceKey key, Vector3 selectionPos) + { + MMesh mesh = model.GetMesh(key.meshId); + Face face = mesh.GetFace(key.faceId); + + // Assuming most faces have only a handful of verts, this is pretty cheap. + Bounds bounds = new Bounds(mesh.VertexPositionInModelCoords(face.vertexIds[0]), Vector3.zero); + for (int i = 1; i < face.vertexIds.Count; i++) + { + bounds.Encapsulate(mesh.VertexPositionInModelCoords(face.vertexIds[i])); + } - // On our standard cube in grid mode with default zoom, each scale-up operation - // increases this magnitude by ~0.0145. The magnitude of the default cube is ~0.0565 - float magnitude = Mathf.Max(0.05f, bounds.size.magnitude * worldSpace.scale); + // On our standard cube in grid mode with default zoom, each scale-up operation + // increases this magnitude by ~0.0145. The magnitude of the default cube is ~0.0565 + float magnitude = Mathf.Max(0.05f, bounds.size.magnitude * worldSpace.scale); - faceHighlights.TurnOn(key, BASE_FACE_HIGHLIGHT_DURATION * magnitude); - } + faceHighlights.TurnOn(key, BASE_FACE_HIGHLIGHT_DURATION * magnitude); + } - // Turns the highlight on for the vertex with the supplied key. - public void TurnOn(FaceKey key) { - TurnOn(key, Vector4.zero); - } + // Turns the highlight on for the vertex with the supplied key. + public void TurnOn(FaceKey key) + { + TurnOn(key, Vector4.zero); + } - // Turns the highlight off for the vertex with the supplied key. - public void TurnOff(FaceKey key) { - faceHighlights.TurnOff(key); - } + // Turns the highlight off for the vertex with the supplied key. + public void TurnOff(FaceKey key) + { + faceHighlights.TurnOff(key); + } - // Turns highlighting off immediately for all vertices (no fade-out). - public void ClearFaces() { - faceHighlights.ClearAll(); - } + // Turns highlighting off immediately for all vertices (no fade-out). + public void ClearFaces() + { + faceHighlights.ClearAll(); + } - // Turns the highlight on for the mesh with the supplied key. - public void TurnOnMesh(int meshId, Vector3 selectionPos) { - MMesh mesh = model.GetMesh(meshId); + // Turns the highlight on for the mesh with the supplied key. + public void TurnOnMesh(int meshId, Vector3 selectionPos) + { + MMesh mesh = model.GetMesh(meshId); - // On our standard cube in grid mode with default zoom, each scale-up operation - // increases this magnitude by ~0.0145. The magnitude of the default cube is ~0.0565 - float magnitude = Mathf.Max(0.5f, mesh.bounds.size.magnitude * worldSpace.scale); + // On our standard cube in grid mode with default zoom, each scale-up operation + // increases this magnitude by ~0.0145. The magnitude of the default cube is ~0.0565 + float magnitude = Mathf.Max(0.5f, mesh.bounds.size.magnitude * worldSpace.scale); - meshHighlights.TurnOn(meshId, MESH_FACE_HIGHLIGHT_DURATION * magnitude); - } + meshHighlights.TurnOn(meshId, MESH_FACE_HIGHLIGHT_DURATION * magnitude); + } - // Turns the highlight on for the mesh with the supplied key. - public void TurnOnMesh(int meshId) { - TurnOnMesh(meshId, Vector4.zero); - } + // Turns the highlight on for the mesh with the supplied key. + public void TurnOnMesh(int meshId) + { + TurnOnMesh(meshId, Vector4.zero); + } - // Turns the highlight off for the mesh with the supplied key. - public void TurnOffMesh(int meshId) { - meshHighlights.TurnOffImmediate(meshId); - } + // Turns the highlight off for the mesh with the supplied key. + public void TurnOffMesh(int meshId) + { + meshHighlights.TurnOffImmediate(meshId); + } - // Turns highlighting off immediately for all meshes (no fade-out). - public void ClearMeshes() { - meshHighlights.ClearAll(); - } + // Turns highlighting off immediately for all meshes (no fade-out). + public void ClearMeshes() + { + meshHighlights.ClearAll(); + } - // Sets the style for the specified mesh highlight to Delete. - public void SetMeshStyleToDelete(int meshId) { - meshHighlights.SetStyle(meshId, (int) MeshStyles.MESH_DELETE); - } + // Sets the style for the specified mesh highlight to Delete. + public void SetMeshStyleToDelete(int meshId) + { + meshHighlights.SetStyle(meshId, (int)MeshStyles.MESH_DELETE); + } - // Sets the style for the specified mesh highlight to Delete. - public void SetMeshStyleToPaint(int meshId) { - meshHighlights.SetStyle(meshId, (int)MeshStyles.MESH_PAINT); - } + // Sets the style for the specified mesh highlight to Delete. + public void SetMeshStyleToPaint(int meshId) + { + meshHighlights.SetStyle(meshId, (int)MeshStyles.MESH_PAINT); + } - // Sets the style for the specified mesh highlight to Tutorial. - public void SetMeshStyleToTutorial(int meshId) { - meshHighlights.SetStyle(meshId, (int)MeshStyles.TUTORIAL_HIGHLIGHT); - } + // Sets the style for the specified mesh highlight to Tutorial. + public void SetMeshStyleToTutorial(int meshId) + { + meshHighlights.SetStyle(meshId, (int)MeshStyles.TUTORIAL_HIGHLIGHT); + } - // Clears all highlight state managed here. - public void ClearAll() { - vertexHighlights.ClearAll(); - edgeHighlights.ClearAll(); - ClearFaces(); - ClearMeshes(); - MeshCycler.ResetCycler(); - } + // Clears all highlight state managed here. + public void ClearAll() + { + vertexHighlights.ClearAll(); + edgeHighlights.ClearAll(); + ClearFaces(); + ClearMeshes(); + MeshCycler.ResetCycler(); + } - // Renders highlight meshes for all selected geometric elements. - public void LateUpdate() { - RenderVertices(); - RenderEdges(); - RenderFaces(); - RenderMeshes(); - inactiveRenderer.RenderEdges(); - inactiveRenderer.RenderPoints(); - } + // Renders highlight meshes for all selected geometric elements. + public void LateUpdate() + { + RenderVertices(); + RenderEdges(); + RenderFaces(); + RenderMeshes(); + inactiveRenderer.RenderEdges(); + inactiveRenderer.RenderPoints(); + } - // Renders vertex highlights. - private void RenderVertices() { - VertexSelectStyle.RenderVertices(model, vertexHighlights, worldSpace); - VertexInactiveStyle.RenderVertices(model, vertexHighlights, worldSpace); - vertexHighlights.ClearExpired(); - } + // Renders vertex highlights. + private void RenderVertices() + { + VertexSelectStyle.RenderVertices(model, vertexHighlights, worldSpace); + VertexInactiveStyle.RenderVertices(model, vertexHighlights, worldSpace); + vertexHighlights.ClearExpired(); + } - // Renders edge highlights. - private void RenderEdges() { - EdgeSelectStyle.RenderEdges(model, edgeHighlights, worldSpace); - EdgeInactiveStyle.RenderEdges(model, edgeHighlights, worldSpace); - EdgeTemporaryStyle.RenderEdges(model, temporaryEdgeHighlights, worldSpace); - edgeHighlights.ClearExpired(); - } + // Renders edge highlights. + private void RenderEdges() + { + EdgeSelectStyle.RenderEdges(model, edgeHighlights, worldSpace); + EdgeInactiveStyle.RenderEdges(model, edgeHighlights, worldSpace); + EdgeTemporaryStyle.RenderEdges(model, temporaryEdgeHighlights, worldSpace); + edgeHighlights.ClearExpired(); + } - // Renders face highlights. - // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing - // face geometry frame to frame) - private void RenderFaces() { - FaceSelectStyle.RenderFaces(model, faceHighlights, worldSpace); - FacePaintStyle.RenderFaces(model, faceHighlights, worldSpace); - FaceExtrudeStyle.RenderFaces(model, faceHighlights, worldSpace); - faceHighlights.ClearExpired(); - } + // Renders face highlights. + // There are some obvious optimization opportunities here if profiling shows them to be necessary (mostly reusing + // face geometry frame to frame) + private void RenderFaces() + { + FaceSelectStyle.RenderFaces(model, faceHighlights, worldSpace); + FacePaintStyle.RenderFaces(model, faceHighlights, worldSpace); + FaceExtrudeStyle.RenderFaces(model, faceHighlights, worldSpace); + faceHighlights.ClearExpired(); + } - // Renders mesh highlights. - private void RenderMeshes() { - MeshSelectStyle.RenderMeshes(model, meshHighlights, worldSpace); - MeshDeleteStyle.RenderMeshes(model, meshHighlights, worldSpace); - MeshPaintStyle.RenderMeshes(model, meshHighlights, worldSpace); - if (PeltzerMain.Instance.tutorialManager.TutorialOccurring()) { - TutorialHighlightStyle.RenderMeshes(model, meshHighlights, worldSpace); - } - meshHighlights.ClearExpired(); + // Renders mesh highlights. + private void RenderMeshes() + { + MeshSelectStyle.RenderMeshes(model, meshHighlights, worldSpace); + MeshDeleteStyle.RenderMeshes(model, meshHighlights, worldSpace); + MeshPaintStyle.RenderMeshes(model, meshHighlights, worldSpace); + if (PeltzerMain.Instance.tutorialManager.TutorialOccurring()) + { + TutorialHighlightStyle.RenderMeshes(model, meshHighlights, worldSpace); + } + meshHighlights.ClearExpired(); + } } - } } diff --git a/Assets/Scripts/tools/utils/InactiveSelectionHighlighter.cs b/Assets/Scripts/tools/utils/InactiveSelectionHighlighter.cs index 5fb4c72e..154e91e2 100644 --- a/Assets/Scripts/tools/utils/InactiveSelectionHighlighter.cs +++ b/Assets/Scripts/tools/utils/InactiveSelectionHighlighter.cs @@ -17,176 +17,194 @@ using com.google.apps.peltzer.client.model.main; using UnityEngine; -namespace com.google.apps.peltzer.client.tools.utils { - public class InactiveSelectionHighlighter { - public static float INACTIVE_HIGHLIGHT_RADIUS = .5f; - public static float OLD_INACTIVE_HIGHLIGHT_RADIUS = .5f; - public static float NEW_INACTIVE_HIGHLIGHT_RADIUS = 10f; - public static readonly int MAX_VERTS_IN_WIREFRAME = 500; - private SpatialIndex spatialIndex; - private HighlightUtils highlightUtils; - private WorldSpace worldSpace; - private Model model; - - private HashSet currentSelectableVerts; - private HashSet currentSelectableEdges; - private HashSet meshesInRange; - - private HashSet knownSelectedOrHighlightedVerts; - private HashSet knownSelectedOrHighlightedEdges; - - private MMesh closestMesh; - private float closestDistance; - - public InactiveSelectionHighlighter(SpatialIndex spatialIndex, HighlightUtils highlightUtils, - WorldSpace worldSpace, Model model) { - this.spatialIndex = spatialIndex; - this.highlightUtils = highlightUtils; - this.worldSpace = worldSpace; - this.model = model; - - currentSelectableVerts = new HashSet(); - currentSelectableEdges = new HashSet(); - knownSelectedOrHighlightedEdges = new HashSet(); - knownSelectedOrHighlightedVerts = new HashSet(); - meshesInRange = new HashSet(); +namespace com.google.apps.peltzer.client.tools.utils +{ + public class InactiveSelectionHighlighter + { + public static float INACTIVE_HIGHLIGHT_RADIUS = .5f; + public static float OLD_INACTIVE_HIGHLIGHT_RADIUS = .5f; + public static float NEW_INACTIVE_HIGHLIGHT_RADIUS = 10f; + public static readonly int MAX_VERTS_IN_WIREFRAME = 500; + private SpatialIndex spatialIndex; + private HighlightUtils highlightUtils; + private WorldSpace worldSpace; + private Model model; + + private HashSet currentSelectableVerts; + private HashSet currentSelectableEdges; + private HashSet meshesInRange; + + private HashSet knownSelectedOrHighlightedVerts; + private HashSet knownSelectedOrHighlightedEdges; + + private MMesh closestMesh; + private float closestDistance; + + public InactiveSelectionHighlighter(SpatialIndex spatialIndex, HighlightUtils highlightUtils, + WorldSpace worldSpace, Model model) + { + this.spatialIndex = spatialIndex; + this.highlightUtils = highlightUtils; + this.worldSpace = worldSpace; + this.model = model; + + currentSelectableVerts = new HashSet(); + currentSelectableEdges = new HashSet(); + knownSelectedOrHighlightedEdges = new HashSet(); + knownSelectedOrHighlightedVerts = new HashSet(); + meshesInRange = new HashSet(); + } + + /// + /// Turns on inactive vertex and edge rendering near the current position. + /// + /// The current active selection position. + /// Currently selected vertices from Selector + /// Currently hovered vertex from Selector + /// Currently selected edges from Selector + /// Currently hovered edge from Selector + public void ShowSelectableVertsEdgesNear(Vector3 selectPositionModel, + HashSet currentlySelectedVerts, + VertexKey currentlyHoveredVert, + HashSet currentlySelectedEdges, + EdgeKey currentlyHoveredEdge) + { + GetMeshesNear(selectPositionModel, currentlySelectedEdges.Count == 0, currentlySelectedVerts.Count == 0, + currentlySelectedVerts, currentlySelectedEdges, currentlyHoveredVert, currentlyHoveredEdge); + } + + /// + /// Turns off all inactive mesh element rendering. + /// + public void TurnOffVertsEdges() + { + TurnOffInactiveEdges(); + TurnOffInactiveVerts(); + highlightUtils.inactiveRenderer.Clear(); + meshesInRange.Clear(); + } + + /// + /// Turns off inactive vertex rendering. + /// + private void TurnOffInactiveVerts() + { + foreach (VertexKey key in currentSelectableVerts) + { + highlightUtils.TurnOff(key); + } + } + + /// + /// Turns off inactive edge rendering. + /// + private void TurnOffInactiveEdges() + { + foreach (EdgeKey key in currentSelectableEdges) + { + highlightUtils.TurnOff(key); + } + } + + /// + /// Turns on inactive verts. + /// + public void ShowSelectableVertsNear(Vector3 selectPositionModel, + HashSet currentlySelectedVerts, + VertexKey currentlyHoveredVert) + { + TurnOffInactiveEdges(); + GetMeshesNear(selectPositionModel, true, false, currentlySelectedVerts, null, currentlyHoveredVert, null); + + } + + /// This should be treated as a local variable for GetMeshesNear - it's being declared outside of the method in + /// order to preallocate the hash set. + private HashSet nearbyMeshes = new HashSet(); + private void GetMeshesNear(Vector3 selectPositionModel, bool showVerts, bool showEdges, + HashSet selectedVerts, HashSet selectedEdges, VertexKey hoveredVert, EdgeKey hoveredEdge) + { + nearbyMeshes.Clear(); + spatialIndex.FindMeshesClosestToDirect(selectPositionModel, INACTIVE_HIGHLIGHT_RADIUS, ref nearbyMeshes); + SelectFromMeshes(nearbyMeshes, showVerts, showEdges, selectedVerts, selectedEdges, hoveredVert, hoveredEdge); + highlightUtils.inactiveRenderer.SetSelectPosition(selectPositionModel); + } + + /// This should be treated as a local variable for SelectFromMeshes - it's being declared outside of the method in + /// order to preallocate the hash set. + private HashSet newlyInactiveMeshes = new HashSet(); + /// + /// Given a set of meshids which should render wireframes, manage the rendering of the wireframes. + /// + private void SelectFromMeshes(HashSet selectableMeshIds, bool showVerts, bool showEdges, + HashSet selectedVerts, HashSet selectedEdges, VertexKey hoveredVert, EdgeKey hoveredEdge) + { + newlyInactiveMeshes.Clear(); + newlyInactiveMeshes.UnionWith(meshesInRange); + newlyInactiveMeshes.ExceptWith(selectableMeshIds); + highlightUtils.inactiveRenderer.TurnOnEdgeWireframe(newlyInactiveMeshes); + + // And turn them off + // Remove the inactive ones from the set we'll render + meshesInRange.ExceptWith(newlyInactiveMeshes); + // Remove ones that are actually selected + selectableMeshIds.ExceptWith(meshesInRange); + highlightUtils.inactiveRenderer.showEdges = showEdges; + highlightUtils.inactiveRenderer.showPoints = showVerts; + if (showEdges) + { + highlightUtils.inactiveRenderer.TurnOnEdgeWireframe(selectableMeshIds); + } + if (showVerts) + { + highlightUtils.inactiveRenderer.TurnOnPointWireframe(selectableMeshIds); + } + // And add any new ones we found + meshesInRange.UnionWith(selectableMeshIds); + } + + /// This should be treated as a local variable for ShowSelectableVertsNearInternal - it's being declared outside of + /// the method in order to preallocate the hash set. + private static HashSet newlyInactiveVerts = new HashSet(); + /// + /// Renders inactive vertices in a radius around the selection point. + /// + private void ShowSelectableVertsNearInternal(HashSet currentlySelectedVerts, + HashSet selectableVerts, + VertexKey currentlyHoveredVert) + { + // Subtract that set from our old set to find which have transitioned into inactive + newlyInactiveVerts.Clear(); + newlyInactiveVerts.UnionWith(currentSelectableVerts); + newlyInactiveVerts.ExceptWith(selectableVerts); + // Remove the inactive ones from the set we'll render + currentSelectableVerts.ExceptWith(newlyInactiveVerts); + // And add any new ones we found + currentSelectableVerts.UnionWith(selectableVerts); + // Remove ones that are actually selected + currentSelectableVerts.ExceptWith(currentlySelectedVerts); + // And if one is hovered, remove that too. + if (currentlyHoveredVert != null) + { + currentSelectableVerts.Remove(currentlyHoveredVert); + } + // Now turn 'em all on. + foreach (VertexKey key in currentSelectableVerts) + { + highlightUtils.TurnOn(key); + highlightUtils.SetVertexStyleToInactive(key); + } + } + + /// + /// Turns on inactive edges. + /// + public void ShowSelectableEdgesNear(Vector3 selectPositionModel, + HashSet currentlySelectedEdges, + EdgeKey currentlyHoveredEdge) + { + TurnOffInactiveVerts(); + GetMeshesNear(selectPositionModel, false, true, null, currentlySelectedEdges, null, currentlyHoveredEdge); + } } - - /// - /// Turns on inactive vertex and edge rendering near the current position. - /// - /// The current active selection position. - /// Currently selected vertices from Selector - /// Currently hovered vertex from Selector - /// Currently selected edges from Selector - /// Currently hovered edge from Selector - public void ShowSelectableVertsEdgesNear(Vector3 selectPositionModel, - HashSet currentlySelectedVerts, - VertexKey currentlyHoveredVert, - HashSet currentlySelectedEdges, - EdgeKey currentlyHoveredEdge) { - GetMeshesNear(selectPositionModel, currentlySelectedEdges.Count == 0, currentlySelectedVerts.Count == 0, - currentlySelectedVerts, currentlySelectedEdges, currentlyHoveredVert, currentlyHoveredEdge); - } - - /// - /// Turns off all inactive mesh element rendering. - /// - public void TurnOffVertsEdges() { - TurnOffInactiveEdges(); - TurnOffInactiveVerts(); - highlightUtils.inactiveRenderer.Clear(); - meshesInRange.Clear(); - } - - /// - /// Turns off inactive vertex rendering. - /// - private void TurnOffInactiveVerts() { - foreach (VertexKey key in currentSelectableVerts) { - highlightUtils.TurnOff(key); - } - } - - /// - /// Turns off inactive edge rendering. - /// - private void TurnOffInactiveEdges() { - foreach (EdgeKey key in currentSelectableEdges) { - highlightUtils.TurnOff(key); - } - } - - /// - /// Turns on inactive verts. - /// - public void ShowSelectableVertsNear(Vector3 selectPositionModel, - HashSet currentlySelectedVerts, - VertexKey currentlyHoveredVert) { - TurnOffInactiveEdges(); - GetMeshesNear(selectPositionModel, true, false, currentlySelectedVerts, null, currentlyHoveredVert, null); - - } - - /// This should be treated as a local variable for GetMeshesNear - it's being declared outside of the method in - /// order to preallocate the hash set. - private HashSet nearbyMeshes = new HashSet(); - private void GetMeshesNear(Vector3 selectPositionModel, bool showVerts, bool showEdges, - HashSet selectedVerts, HashSet selectedEdges, VertexKey hoveredVert, EdgeKey hoveredEdge) { - nearbyMeshes.Clear(); - spatialIndex.FindMeshesClosestToDirect(selectPositionModel, INACTIVE_HIGHLIGHT_RADIUS, ref nearbyMeshes); - SelectFromMeshes(nearbyMeshes, showVerts, showEdges, selectedVerts, selectedEdges, hoveredVert, hoveredEdge); - highlightUtils.inactiveRenderer.SetSelectPosition(selectPositionModel); - } - - /// This should be treated as a local variable for SelectFromMeshes - it's being declared outside of the method in - /// order to preallocate the hash set. - private HashSet newlyInactiveMeshes = new HashSet(); - /// - /// Given a set of meshids which should render wireframes, manage the rendering of the wireframes. - /// - private void SelectFromMeshes(HashSet selectableMeshIds, bool showVerts, bool showEdges, - HashSet selectedVerts, HashSet selectedEdges, VertexKey hoveredVert, EdgeKey hoveredEdge) { - newlyInactiveMeshes.Clear(); - newlyInactiveMeshes.UnionWith(meshesInRange); - newlyInactiveMeshes.ExceptWith(selectableMeshIds); - highlightUtils.inactiveRenderer.TurnOnEdgeWireframe(newlyInactiveMeshes); - - // And turn them off - // Remove the inactive ones from the set we'll render - meshesInRange.ExceptWith(newlyInactiveMeshes); - // Remove ones that are actually selected - selectableMeshIds.ExceptWith(meshesInRange); - highlightUtils.inactiveRenderer.showEdges = showEdges; - highlightUtils.inactiveRenderer.showPoints = showVerts; - if (showEdges) { - highlightUtils.inactiveRenderer.TurnOnEdgeWireframe(selectableMeshIds); - } - if (showVerts) { - highlightUtils.inactiveRenderer.TurnOnPointWireframe(selectableMeshIds); - } - // And add any new ones we found - meshesInRange.UnionWith(selectableMeshIds); - } - - /// This should be treated as a local variable for ShowSelectableVertsNearInternal - it's being declared outside of - /// the method in order to preallocate the hash set. - private static HashSet newlyInactiveVerts = new HashSet(); - /// - /// Renders inactive vertices in a radius around the selection point. - /// - private void ShowSelectableVertsNearInternal(HashSet currentlySelectedVerts, - HashSet selectableVerts, - VertexKey currentlyHoveredVert) { - // Subtract that set from our old set to find which have transitioned into inactive - newlyInactiveVerts.Clear(); - newlyInactiveVerts.UnionWith(currentSelectableVerts); - newlyInactiveVerts.ExceptWith(selectableVerts); - // Remove the inactive ones from the set we'll render - currentSelectableVerts.ExceptWith(newlyInactiveVerts); - // And add any new ones we found - currentSelectableVerts.UnionWith(selectableVerts); - // Remove ones that are actually selected - currentSelectableVerts.ExceptWith(currentlySelectedVerts); - // And if one is hovered, remove that too. - if (currentlyHoveredVert != null) { - currentSelectableVerts.Remove(currentlyHoveredVert); - } - // Now turn 'em all on. - foreach (VertexKey key in currentSelectableVerts) { - highlightUtils.TurnOn(key); - highlightUtils.SetVertexStyleToInactive(key); - } - } - - /// - /// Turns on inactive edges. - /// - public void ShowSelectableEdgesNear(Vector3 selectPositionModel, - HashSet currentlySelectedEdges, - EdgeKey currentlyHoveredEdge) { - TurnOffInactiveVerts(); - GetMeshesNear(selectPositionModel, false, true, null, currentlySelectedEdges, null, currentlyHoveredEdge); - } - } } \ No newline at end of file diff --git a/Assets/Scripts/tools/utils/ScaleType.cs b/Assets/Scripts/tools/utils/ScaleType.cs index 931c167c..92da75bd 100644 --- a/Assets/Scripts/tools/utils/ScaleType.cs +++ b/Assets/Scripts/tools/utils/ScaleType.cs @@ -12,22 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace com.google.apps.peltzer.client.tools.utils { - /// - /// The scaling types used by various tools. - /// - public enum ScaleType { +namespace com.google.apps.peltzer.client.tools.utils +{ /// - /// Scaling type when no scaling is happening currently. + /// The scaling types used by various tools. /// - NONE, - /// - /// Scaling type when the user is scaling up. - /// - SCALE_UP, - /// - /// Scaling type when the user is scaling down. - /// - SCALE_DOWN, - } + public enum ScaleType + { + /// + /// Scaling type when no scaling is happening currently. + /// + NONE, + /// + /// Scaling type when the user is scaling up. + /// + SCALE_UP, + /// + /// Scaling type when the user is scaling down. + /// + SCALE_DOWN, + } } diff --git a/Assets/Scripts/tutorial/AttentionCaller.cs b/Assets/Scripts/tutorial/AttentionCaller.cs index 813fb32b..bbe039c0 100644 --- a/Assets/Scripts/tutorial/AttentionCaller.cs +++ b/Assets/Scripts/tutorial/AttentionCaller.cs @@ -21,97 +21,102 @@ using com.google.apps.peltzer.client.model.core; using TMPro; -namespace com.google.apps.peltzer.client.tutorial { - public class Glow { - /// - /// The period of the "glow" animation (the time taken by each repetition of it). - /// Shorter values means a faster animation. Given in seconds. - /// - private const float GLOW_PERIOD = 0.5f; - - /// - /// The maximum emission factor when glowing. More is brighter. - /// - private const float GLOW_MAX_EMISSION = 0.9f; - - /// - /// The period of the "glow" animation (the time taken by each repetition of it). - /// Shorter values means a faster animation. Given in seconds. - /// - public float period { get; private set; } - - /// - /// The maximum emission factor when glowing. More is brighter. - /// - public float maxEmission { get; private set; } - - /// - /// The time to start this glow. - /// - public float startTime { get; private set; } - - /// - /// The time to end this glow. - /// - public float endTime { get; private set; } - - /// - /// A glow. This holds all the information to glow a gameObject a certain amount at a certain rate during a certain - /// time period. - /// - /// The time for each glow repetition. - /// The brightest the glow can be. - /// How long we should wait from when the glow is created until we start it. - /// How long the glow should last. - public Glow(float period = GLOW_PERIOD, float maxEmission = GLOW_MAX_EMISSION, float delay = 0f, float duration = Mathf.Infinity) { - this.period = period; - this.maxEmission = maxEmission; - startTime = Time.time + delay; - endTime = startTime + duration; +namespace com.google.apps.peltzer.client.tutorial +{ + public class Glow + { + /// + /// The period of the "glow" animation (the time taken by each repetition of it). + /// Shorter values means a faster animation. Given in seconds. + /// + private const float GLOW_PERIOD = 0.5f; + + /// + /// The maximum emission factor when glowing. More is brighter. + /// + private const float GLOW_MAX_EMISSION = 0.9f; + + /// + /// The period of the "glow" animation (the time taken by each repetition of it). + /// Shorter values means a faster animation. Given in seconds. + /// + public float period { get; private set; } + + /// + /// The maximum emission factor when glowing. More is brighter. + /// + public float maxEmission { get; private set; } + + /// + /// The time to start this glow. + /// + public float startTime { get; private set; } + + /// + /// The time to end this glow. + /// + public float endTime { get; private set; } + + /// + /// A glow. This holds all the information to glow a gameObject a certain amount at a certain rate during a certain + /// time period. + /// + /// The time for each glow repetition. + /// The brightest the glow can be. + /// How long we should wait from when the glow is created until we start it. + /// How long the glow should last. + public Glow(float period = GLOW_PERIOD, float maxEmission = GLOW_MAX_EMISSION, float delay = 0f, float duration = Mathf.Infinity) + { + this.period = period; + this.maxEmission = maxEmission; + startTime = Time.time + delay; + endTime = startTime + duration; + } } - } - /// - /// Responsible for calling attention to several parts of the UI during tutorial. - /// - public class AttentionCaller : MonoBehaviour, IMeshRenderOwner { /// - /// Possible elements that we can call attention to. + /// Responsible for calling attention to several parts of the UI during tutorial. /// - public enum Element { - PELTZER_TOUCHPAD_LEFT, - PELTZER_TOUCHPAD_RIGHT, - PELTZER_TOUCHPAD_UP, - PELTZER_TOUCHPAD_DOWN, - PALETTE_TOUCHPAD_LEFT, - PALETTE_TOUCHPAD_RIGHT, - PALETTE_TOUCHPAD_UP, - PALETTE_TOUCHPAD_DOWN, - PELTZER_TRIGGER, - PALETTE_TRIGGER, - PELTZER_GRIP_LEFT, - PELTZER_GRIP_RIGHT, - PALETTE_GRIP_LEFT, - PALETTE_GRIP_RIGHT, - PELTZER_MENU_BUTTON, - PALETTE_MENU_BUTTON, - PELTZER_SYSTEM_BUTTON, - PALETTE_SYSTEM_BUTTON, - RED_PAINT_SWATCH, - NEW_BUTTON, - SAVE_BUTTON_ICON, - GRID_BUTTON, - TUTORIAL_BUTTON, - TAKE_A_TUTORIAL_BUTTON, - SIREN, - PELTZER_SECONDARY_BUTTON, - PALETTE_SECONDARY_BUTTON, - PELTZER_THUMBSTICK, - PALETTE_THUMBSTICK, - SAVE_SELECTED_BUTTON, - } + public class AttentionCaller : MonoBehaviour, IMeshRenderOwner + { + /// + /// Possible elements that we can call attention to. + /// + public enum Element + { + PELTZER_TOUCHPAD_LEFT, + PELTZER_TOUCHPAD_RIGHT, + PELTZER_TOUCHPAD_UP, + PELTZER_TOUCHPAD_DOWN, + PALETTE_TOUCHPAD_LEFT, + PALETTE_TOUCHPAD_RIGHT, + PALETTE_TOUCHPAD_UP, + PALETTE_TOUCHPAD_DOWN, + PELTZER_TRIGGER, + PALETTE_TRIGGER, + PELTZER_GRIP_LEFT, + PELTZER_GRIP_RIGHT, + PALETTE_GRIP_LEFT, + PALETTE_GRIP_RIGHT, + PELTZER_MENU_BUTTON, + PALETTE_MENU_BUTTON, + PELTZER_SYSTEM_BUTTON, + PALETTE_SYSTEM_BUTTON, + RED_PAINT_SWATCH, + NEW_BUTTON, + SAVE_BUTTON_ICON, + GRID_BUTTON, + TUTORIAL_BUTTON, + TAKE_A_TUTORIAL_BUTTON, + SIREN, + PELTZER_SECONDARY_BUTTON, + PALETTE_SECONDARY_BUTTON, + PELTZER_THUMBSTICK, + PALETTE_THUMBSTICK, + SAVE_SELECTED_BUTTON, + } - private List supportedModes = new List() { + private List supportedModes = new List() { ControllerMode.insertVolume, ControllerMode.insertStroke, ControllerMode.paintMesh, @@ -120,189 +125,192 @@ public enum Element { ControllerMode.delete }; - /// - /// The period of the "glow" animation (the time taken by each repetition of it). - /// Shorter values means a faster animation. Given in seconds. - /// - private const float GLOW_PERIOD = 1.0f; - - /// - /// The maximum emission factor when glowing. More is brighter. - /// - private const float GLOW_MAX_EMISSION = 0.8f; - - /// - /// The maximum percent we can grey something. - /// - private const float GREY_MAX = 0.9f; - - private const float DEFAULT_BULB_PERIOD = 0.25f; - private const float DEFAULT_BULB_MAX_EMISSION = 0.9f; - private const float DEFAULT_BULB_DURATION = 1.5f; - - private const float DEFAULT_SIREN_PERIOD = 0.4f; - private const float DEFAULT_SIREN_MAX_EMISSION = 1f; - private const float DEFAULT_SIREN_DELAY = 0f; - private const float DEFAULT_SIREN_DURATION = 2f; - - /// - /// Base color of the glow animation. - /// - private readonly Color GLOW_BASE_COLOR = Color.black; - - /// - /// Override color. - /// - private readonly Color OVERRIDE_COLOR = new Color(128f/255f, 128f/255f, 128f/255f); - - /// - /// Base color of the glowing sphere animations. - /// - private readonly Color SPHERE_BASE_COLOR = new Color(0.5f, 1.0f, 0.5f); - - /// - /// Color for an icon if it is inactive or "greyed out". - /// - private readonly Color INACTIVE_ICON_COLOR = new Color(1f, 1f, 1f, 0.117f); - - /// - /// Color for an icon if it is active or "coloured". - /// - private readonly Color ACTIVE_ICON_COLOR = new Color(1f, 1f, 1f, 1f); - - /// - /// Name of the emission color variable in the shader. - /// - private const string EMISSIVE_COLOR_VAR_NAME = "_EmissiveColor"; - - /// - /// Elements to highlight with a large glowing green sphere. - /// - private readonly List SPHERE_ELEMENTS = new List{}; - - private const string NEW_BUTTON_PATH = "ID_PanelTools/ToolSide/Actions/New"; - private const string SAVE_BUTTON_ICON_PATH = "ID_PanelTools/ToolSide/Actions/Save"; - private const string SAVE_SELECTED_BUTTON_PATH = "ID_PanelTools/ToolSide/Menu-Save/Save-Selected"; - private const string GRID_BUTTON_PATH = "ID_PanelTools/ToolSide/Actions/Blockmode"; - private const string TUTORIAL_BUTTON_PATH = "ID_PanelTools/ToolSide/Actions/Tutorial"; - private const string TAKE_A_TUTORIAL_BUTTON_PATH = "ID_PanelTools/ToolSide/TutorialPrompt/Btns/YesTutorial"; - - private const int RED_MATERIAL_ID = 8; - - private PeltzerController peltzerController; - private PaletteController paletteController; - - /// - /// Maps an Element to the GameObject it represents. - /// - private Dictionary elements = new Dictionary(); - - ChangeMaterialMenuItem[] allColourSwatches; - - List lightBulbs; - - /// - /// Maps Elements to spheres that are currently in the scene. - /// - private Dictionary currentSpheres = new Dictionary(); - - /// - /// Offsets that adjust attention-calling-spheres to align with their element, because - /// most controller elements are offcenter. - /// - private readonly Vector3 TRIGGER_OFFSET = new Vector3(0.0f, -0.04f, -0.03f); - private readonly Vector3 LEFT_GRIP_OFFSET = new Vector3(.02f, -.08f, 0.01f); - private readonly Vector3 RIGHT_GRIP_OFFSET = new Vector3(-.02f, -.08f, 0.01f); - - /// - /// Sizes of the different attention-calling-spheres. - /// - const float TRIGGER_SPHERE_SIZE_MAX = 0.1f; - const float TRIGGER_SPHERE_SIZE_MIN = 0.03f; - const float GRIP_SPHERE_SIZE_MAX = 0.03f; - const float GRIP_SPHERE_SIZE_MIN = 0.01f; - - private Dictionary currentlyGlowing = new Dictionary(); - - /// - /// Prefab that holds the glowing sphere. - /// - private GameObject spherePrefab; - - /// - /// List of Mesh IDs of meshes that are currently being highlighted with a glow effect. - /// - private List claimedMeshes = new List(); - - /// - /// Starting value for the interpolation param between sphere sizes. - /// - static float sizePct = 0.0f; - - public Glow defaultSirenGlow; + /// + /// The period of the "glow" animation (the time taken by each repetition of it). + /// Shorter values means a faster animation. Given in seconds. + /// + private const float GLOW_PERIOD = 1.0f; + + /// + /// The maximum emission factor when glowing. More is brighter. + /// + private const float GLOW_MAX_EMISSION = 0.8f; + + /// + /// The maximum percent we can grey something. + /// + private const float GREY_MAX = 0.9f; + + private const float DEFAULT_BULB_PERIOD = 0.25f; + private const float DEFAULT_BULB_MAX_EMISSION = 0.9f; + private const float DEFAULT_BULB_DURATION = 1.5f; + + private const float DEFAULT_SIREN_PERIOD = 0.4f; + private const float DEFAULT_SIREN_MAX_EMISSION = 1f; + private const float DEFAULT_SIREN_DELAY = 0f; + private const float DEFAULT_SIREN_DURATION = 2f; + + /// + /// Base color of the glow animation. + /// + private readonly Color GLOW_BASE_COLOR = Color.black; + + /// + /// Override color. + /// + private readonly Color OVERRIDE_COLOR = new Color(128f / 255f, 128f / 255f, 128f / 255f); + + /// + /// Base color of the glowing sphere animations. + /// + private readonly Color SPHERE_BASE_COLOR = new Color(0.5f, 1.0f, 0.5f); + + /// + /// Color for an icon if it is inactive or "greyed out". + /// + private readonly Color INACTIVE_ICON_COLOR = new Color(1f, 1f, 1f, 0.117f); + + /// + /// Color for an icon if it is active or "coloured". + /// + private readonly Color ACTIVE_ICON_COLOR = new Color(1f, 1f, 1f, 1f); + + /// + /// Name of the emission color variable in the shader. + /// + private const string EMISSIVE_COLOR_VAR_NAME = "_EmissiveColor"; + + /// + /// Elements to highlight with a large glowing green sphere. + /// + private readonly List SPHERE_ELEMENTS = new List { }; + + private const string NEW_BUTTON_PATH = "ID_PanelTools/ToolSide/Actions/New"; + private const string SAVE_BUTTON_ICON_PATH = "ID_PanelTools/ToolSide/Actions/Save"; + private const string SAVE_SELECTED_BUTTON_PATH = "ID_PanelTools/ToolSide/Menu-Save/Save-Selected"; + private const string GRID_BUTTON_PATH = "ID_PanelTools/ToolSide/Actions/Blockmode"; + private const string TUTORIAL_BUTTON_PATH = "ID_PanelTools/ToolSide/Actions/Tutorial"; + private const string TAKE_A_TUTORIAL_BUTTON_PATH = "ID_PanelTools/ToolSide/TutorialPrompt/Btns/YesTutorial"; + + private const int RED_MATERIAL_ID = 8; + + private PeltzerController peltzerController; + private PaletteController paletteController; + + /// + /// Maps an Element to the GameObject it represents. + /// + private Dictionary elements = new Dictionary(); + + ChangeMaterialMenuItem[] allColourSwatches; + + List lightBulbs; + + /// + /// Maps Elements to spheres that are currently in the scene. + /// + private Dictionary currentSpheres = new Dictionary(); + + /// + /// Offsets that adjust attention-calling-spheres to align with their element, because + /// most controller elements are offcenter. + /// + private readonly Vector3 TRIGGER_OFFSET = new Vector3(0.0f, -0.04f, -0.03f); + private readonly Vector3 LEFT_GRIP_OFFSET = new Vector3(.02f, -.08f, 0.01f); + private readonly Vector3 RIGHT_GRIP_OFFSET = new Vector3(-.02f, -.08f, 0.01f); + + /// + /// Sizes of the different attention-calling-spheres. + /// + const float TRIGGER_SPHERE_SIZE_MAX = 0.1f; + const float TRIGGER_SPHERE_SIZE_MIN = 0.03f; + const float GRIP_SPHERE_SIZE_MAX = 0.03f; + const float GRIP_SPHERE_SIZE_MIN = 0.01f; + + private Dictionary currentlyGlowing = new Dictionary(); + + /// + /// Prefab that holds the glowing sphere. + /// + private GameObject spherePrefab; + + /// + /// List of Mesh IDs of meshes that are currently being highlighted with a glow effect. + /// + private List claimedMeshes = new List(); + + /// + /// Starting value for the interpolation param between sphere sizes. + /// + static float sizePct = 0.0f; + + public Glow defaultSirenGlow; + + /// + /// One-time setup. Call once before using this object. + /// + /// The PeltzerController to use. + /// The PaletteController to use. + public void Setup(PeltzerController peltzerController, PaletteController paletteController) + { + this.peltzerController = peltzerController; + this.paletteController = paletteController; + + // This only handles the red material as a special case. Ideally we would write this to be robust and allow + // the greying/recoloring of any swatch given materialId. But that is overkill for what we need right now. + allColourSwatches = ObjectFinder.ObjectById("ID_PanelTools").GetComponentsInChildren(true); + + for (int i = 0; i < allColourSwatches.Length; i++) + { + if (allColourSwatches[i].materialId == RED_MATERIAL_ID) + { + elements[Element.RED_PAINT_SWATCH] = allColourSwatches[i].gameObject; + } + } - /// - /// One-time setup. Call once before using this object. - /// - /// The PeltzerController to use. - /// The PaletteController to use. - public void Setup(PeltzerController peltzerController, PaletteController paletteController) { - this.peltzerController = peltzerController; - this.paletteController = paletteController; - - // This only handles the red material as a special case. Ideally we would write this to be robust and allow - // the greying/recoloring of any swatch given materialId. But that is overkill for what we need right now. - allColourSwatches = ObjectFinder.ObjectById("ID_PanelTools").GetComponentsInChildren(true); - - for (int i = 0; i < allColourSwatches.Length; i++) { - if (allColourSwatches[i].materialId == RED_MATERIAL_ID) { - elements[Element.RED_PAINT_SWATCH] = allColourSwatches[i].gameObject; - } - } - - elements[Element.PELTZER_TOUCHPAD_LEFT] = - peltzerController.controllerGeometry.touchpadLeft; - elements[Element.PELTZER_TOUCHPAD_RIGHT] = - peltzerController.controllerGeometry.touchpadRight; - elements[Element.PELTZER_TOUCHPAD_UP] = - peltzerController.controllerGeometry.touchpadUp; - elements[Element.PELTZER_TOUCHPAD_DOWN] = - peltzerController.controllerGeometry.touchpadDown; - elements[Element.PALETTE_TOUCHPAD_LEFT] = - paletteController.controllerGeometry.touchpadLeft; - elements[Element.PALETTE_TOUCHPAD_RIGHT] = - paletteController.controllerGeometry.touchpadRight; - elements[Element.PALETTE_TOUCHPAD_UP] = - paletteController.controllerGeometry.touchpadUp; - elements[Element.PALETTE_TOUCHPAD_DOWN] = - paletteController.controllerGeometry.touchpadDown; - elements[Element.PALETTE_THUMBSTICK] = - paletteController.controllerGeometry.thumbstick; - elements[Element.PELTZER_THUMBSTICK] = - peltzerController.controllerGeometry.thumbstick; - elements[Element.PELTZER_TRIGGER] = peltzerController.controllerGeometry.trigger; - elements[Element.PALETTE_TRIGGER] = paletteController.controllerGeometry.trigger; - elements[Element.PELTZER_GRIP_LEFT] = peltzerController.controllerGeometry.gripLeft; - elements[Element.PELTZER_GRIP_RIGHT] = peltzerController.controllerGeometry.gripRight; - elements[Element.PALETTE_GRIP_LEFT] = paletteController.controllerGeometry.gripLeft; - elements[Element.PALETTE_GRIP_RIGHT] = paletteController.controllerGeometry.gripRight; - elements[Element.PELTZER_MENU_BUTTON] = peltzerController.controllerGeometry.appMenuButton; - elements[Element.PELTZER_SYSTEM_BUTTON] = peltzerController.controllerGeometry.systemButton; - elements[Element.PELTZER_SECONDARY_BUTTON] = peltzerController.controllerGeometry.secondaryButton; - elements[Element.PALETTE_MENU_BUTTON] = paletteController.controllerGeometry.appMenuButton; - elements[Element.PALETTE_SYSTEM_BUTTON] = paletteController.controllerGeometry.systemButton; - elements[Element.PALETTE_SECONDARY_BUTTON] = paletteController.controllerGeometry.secondaryButton; - elements[Element.NEW_BUTTON] = FindOrDie(paletteController.gameObject, NEW_BUTTON_PATH); - elements[Element.SAVE_BUTTON_ICON] = FindOrDie(paletteController.gameObject, SAVE_BUTTON_ICON_PATH); - elements[Element.GRID_BUTTON] = FindOrDie(paletteController.gameObject, GRID_BUTTON_PATH); - elements[Element.TUTORIAL_BUTTON] = FindOrDie(paletteController.gameObject, TUTORIAL_BUTTON_PATH); - elements[Element.TAKE_A_TUTORIAL_BUTTON] = FindOrDie(paletteController.gameObject, TAKE_A_TUTORIAL_BUTTON_PATH); - elements[Element.SIREN] = ObjectFinder.ObjectById("ID_Siren"); - elements[Element.SAVE_SELECTED_BUTTON] = FindOrDie(paletteController.gameObject, SAVE_SELECTED_BUTTON_PATH); - - this.spherePrefab = Resources.Load("Prefabs/GlowOrb"); - - lightBulbs = new List{ + elements[Element.PELTZER_TOUCHPAD_LEFT] = + peltzerController.controllerGeometry.touchpadLeft; + elements[Element.PELTZER_TOUCHPAD_RIGHT] = + peltzerController.controllerGeometry.touchpadRight; + elements[Element.PELTZER_TOUCHPAD_UP] = + peltzerController.controllerGeometry.touchpadUp; + elements[Element.PELTZER_TOUCHPAD_DOWN] = + peltzerController.controllerGeometry.touchpadDown; + elements[Element.PALETTE_TOUCHPAD_LEFT] = + paletteController.controllerGeometry.touchpadLeft; + elements[Element.PALETTE_TOUCHPAD_RIGHT] = + paletteController.controllerGeometry.touchpadRight; + elements[Element.PALETTE_TOUCHPAD_UP] = + paletteController.controllerGeometry.touchpadUp; + elements[Element.PALETTE_TOUCHPAD_DOWN] = + paletteController.controllerGeometry.touchpadDown; + elements[Element.PALETTE_THUMBSTICK] = + paletteController.controllerGeometry.thumbstick; + elements[Element.PELTZER_THUMBSTICK] = + peltzerController.controllerGeometry.thumbstick; + elements[Element.PELTZER_TRIGGER] = peltzerController.controllerGeometry.trigger; + elements[Element.PALETTE_TRIGGER] = paletteController.controllerGeometry.trigger; + elements[Element.PELTZER_GRIP_LEFT] = peltzerController.controllerGeometry.gripLeft; + elements[Element.PELTZER_GRIP_RIGHT] = peltzerController.controllerGeometry.gripRight; + elements[Element.PALETTE_GRIP_LEFT] = paletteController.controllerGeometry.gripLeft; + elements[Element.PALETTE_GRIP_RIGHT] = paletteController.controllerGeometry.gripRight; + elements[Element.PELTZER_MENU_BUTTON] = peltzerController.controllerGeometry.appMenuButton; + elements[Element.PELTZER_SYSTEM_BUTTON] = peltzerController.controllerGeometry.systemButton; + elements[Element.PELTZER_SECONDARY_BUTTON] = peltzerController.controllerGeometry.secondaryButton; + elements[Element.PALETTE_MENU_BUTTON] = paletteController.controllerGeometry.appMenuButton; + elements[Element.PALETTE_SYSTEM_BUTTON] = paletteController.controllerGeometry.systemButton; + elements[Element.PALETTE_SECONDARY_BUTTON] = paletteController.controllerGeometry.secondaryButton; + elements[Element.NEW_BUTTON] = FindOrDie(paletteController.gameObject, NEW_BUTTON_PATH); + elements[Element.SAVE_BUTTON_ICON] = FindOrDie(paletteController.gameObject, SAVE_BUTTON_ICON_PATH); + elements[Element.GRID_BUTTON] = FindOrDie(paletteController.gameObject, GRID_BUTTON_PATH); + elements[Element.TUTORIAL_BUTTON] = FindOrDie(paletteController.gameObject, TUTORIAL_BUTTON_PATH); + elements[Element.TAKE_A_TUTORIAL_BUTTON] = FindOrDie(paletteController.gameObject, TAKE_A_TUTORIAL_BUTTON_PATH); + elements[Element.SIREN] = ObjectFinder.ObjectById("ID_Siren"); + elements[Element.SAVE_SELECTED_BUTTON] = FindOrDie(paletteController.gameObject, SAVE_SELECTED_BUTTON_PATH); + + this.spherePrefab = Resources.Load("Prefabs/GlowOrb"); + + lightBulbs = new List{ ObjectFinder.ObjectById("ID_Light_1"), ObjectFinder.ObjectById("ID_Light_2"), ObjectFinder.ObjectById("ID_Light_3"), @@ -312,462 +320,559 @@ public void Setup(PeltzerController peltzerController, PaletteController palette ObjectFinder.ObjectById("ID_Light_7"), ObjectFinder.ObjectById("ID_Light_8"), ObjectFinder.ObjectById("ID_Light_9")}; - } - - /// - /// Starts glowing the given element. - /// - /// The element to start glowing. - public void StartGlowing(Element which, Glow glow = null) { - if (elements[which] == null) return; - if (SPHERE_ELEMENTS.Contains(which) && !currentSpheres.ContainsKey(which)) { - // This element should be highlighted with a glowing sphere. - SetSphere(which); - } else if (!currentlyGlowing.ContainsKey(elements[which])) { - currentlyGlowing[elements[which]] = glow == null ? new Glow() : glow; - } - } - - public void StartGlowing(ControllerMode mode, Glow glow = null) { - if (mode == PeltzerMain.Instance.peltzerController.mode) { - StartGlowing(PeltzerMain.Instance.peltzerController.attachedToolHead - .GetComponent().materialObjects, glow); - } - - StartGlowing(PeltzerMain.Instance.paletteController.GetToolheadForMode(mode) - .GetComponent().materialObjects, glow); - } + } - public void StartGlowing(GameObject[] components, Glow glow = null) { - for (int i = 0; i < components.Length; i++) { - if (!currentlyGlowing.ContainsKey(components[i])) { - currentlyGlowing[components[i]] = glow == null ? new Glow() : glow; + /// + /// Starts glowing the given element. + /// + /// The element to start glowing. + public void StartGlowing(Element which, Glow glow = null) + { + if (elements[which] == null) return; + if (SPHERE_ELEMENTS.Contains(which) && !currentSpheres.ContainsKey(which)) + { + // This element should be highlighted with a glowing sphere. + SetSphere(which); + } + else if (!currentlyGlowing.ContainsKey(elements[which])) + { + currentlyGlowing[elements[which]] = glow == null ? new Glow() : glow; + } } - } - } - public void StartGlowing(GameObject obj, Glow glow = null) { - if (!currentlyGlowing.ContainsKey(obj)) { - currentlyGlowing[obj] = glow == null ? new Glow() : glow; - } - } + public void StartGlowing(ControllerMode mode, Glow glow = null) + { + if (mode == PeltzerMain.Instance.peltzerController.mode) + { + StartGlowing(PeltzerMain.Instance.peltzerController.attachedToolHead + .GetComponent().materialObjects, glow); + } - public void GlowTheSiren() { - StartGlowing(Element.SIREN, - new Glow(DEFAULT_SIREN_PERIOD, DEFAULT_SIREN_MAX_EMISSION, DEFAULT_SIREN_DELAY, DEFAULT_SIREN_DURATION)); - } + StartGlowing(PeltzerMain.Instance.paletteController.GetToolheadForMode(mode) + .GetComponent().materialObjects, glow); + } - public void CascadeGlowAllLightbulbs(float duration = DEFAULT_BULB_DURATION) { - for (int i = 0; i < lightBulbs.Count ; i++) { - float delay = i % 2 == 0 ? 0f : DEFAULT_BULB_PERIOD; - Glow glow = new Glow(DEFAULT_BULB_PERIOD, DEFAULT_BULB_MAX_EMISSION, delay, duration); - StartGlowing(lightBulbs[i], glow); - } - } + public void StartGlowing(GameObject[] components, Glow glow = null) + { + for (int i = 0; i < components.Length; i++) + { + if (!currentlyGlowing.ContainsKey(components[i])) + { + currentlyGlowing[components[i]] = glow == null ? new Glow() : glow; + } + } + } - public void GreyOut(ControllerMode mode) { - // The toolheads are cloned when attached to the controller. We need to handle greying out the cloned version. - if (mode == PeltzerMain.Instance.peltzerController.mode) { - PeltzerMain.Instance.peltzerController.attachedToolHead.GetComponent().ChangeToDisable(); - } + public void StartGlowing(GameObject obj, Glow glow = null) + { + if (!currentlyGlowing.ContainsKey(obj)) + { + currentlyGlowing[obj] = glow == null ? new Glow() : glow; + } + } - PeltzerMain.Instance.paletteController.GetToolheadForMode(mode).GetComponent() - .ChangeToDisable(); + public void GlowTheSiren() + { + StartGlowing(Element.SIREN, + new Glow(DEFAULT_SIREN_PERIOD, DEFAULT_SIREN_MAX_EMISSION, DEFAULT_SIREN_DELAY, DEFAULT_SIREN_DURATION)); + } - PeltzerMain.Instance.paletteController.GetToolheadForMode(mode).GetComponent().isActive - = false; - } + public void CascadeGlowAllLightbulbs(float duration = DEFAULT_BULB_DURATION) + { + for (int i = 0; i < lightBulbs.Count; i++) + { + float delay = i % 2 == 0 ? 0f : DEFAULT_BULB_PERIOD; + Glow glow = new Glow(DEFAULT_BULB_PERIOD, DEFAULT_BULB_MAX_EMISSION, delay, duration); + StartGlowing(lightBulbs[i], glow); + } + } - /// - /// Greys out an element by setting its override. - /// - /// The element to grey. - public void GreyOut(Element which, float greyAmount = GREY_MAX) { - GreyOut(elements[which], greyAmount); - } + public void GreyOut(ControllerMode mode) + { + // The toolheads are cloned when attached to the controller. We need to handle greying out the cloned version. + if (mode == PeltzerMain.Instance.peltzerController.mode) + { + PeltzerMain.Instance.peltzerController.attachedToolHead.GetComponent().ChangeToDisable(); + } - public void GreyOut(GameObject obj, float greyAmount = GREY_MAX) { - if (obj != null) { - Material mat = obj.GetComponent().material; - mat.SetFloat("_OverrideAmount", greyAmount); - mat.SetColor("_OverrideColor", OVERRIDE_COLOR); + PeltzerMain.Instance.paletteController.GetToolheadForMode(mode).GetComponent() + .ChangeToDisable(); - if (obj.GetComponent() != null) { - obj.GetComponent().isActive = false; + PeltzerMain.Instance.paletteController.GetToolheadForMode(mode).GetComponent().isActive + = false; } - // Grey out the icon on the buttons. - if (obj.GetComponentInChildren() != null) { - obj.GetComponentInChildren().color = INACTIVE_ICON_COLOR; + /// + /// Greys out an element by setting its override. + /// + /// The element to grey. + public void GreyOut(Element which, float greyAmount = GREY_MAX) + { + GreyOut(elements[which], greyAmount); } - // Grey out text on buttons. - if (obj.GetComponentInChildren() != null) { - obj.GetComponentInChildren().color = INACTIVE_ICON_COLOR; + public void GreyOut(GameObject obj, float greyAmount = GREY_MAX) + { + if (obj != null) + { + Material mat = obj.GetComponent().material; + mat.SetFloat("_OverrideAmount", greyAmount); + mat.SetColor("_OverrideColor", OVERRIDE_COLOR); + + if (obj.GetComponent() != null) + { + obj.GetComponent().isActive = false; + } + + // Grey out the icon on the buttons. + if (obj.GetComponentInChildren() != null) + { + obj.GetComponentInChildren().color = INACTIVE_ICON_COLOR; + } + + // Grey out text on buttons. + if (obj.GetComponentInChildren() != null) + { + obj.GetComponentInChildren().color = INACTIVE_ICON_COLOR; + } + } } - } - } - public void GreyOut (SpriteRenderer[] icons) { - for (int i = 0; i < icons.Length; i++) { - GreyOut(icons[i]); - } - } + public void GreyOut(SpriteRenderer[] icons) + { + for (int i = 0; i < icons.Length; i++) + { + GreyOut(icons[i]); + } + } - public void GreyOut(SpriteRenderer icon) { - icon.color = INACTIVE_ICON_COLOR; - } + public void GreyOut(SpriteRenderer icon) + { + icon.color = INACTIVE_ICON_COLOR; + } - /// - /// Greys out every element. - /// - public void GreyOutAll() { - foreach (Element element in elements.Keys) { - GreyOut(element); - } - - // The red swatch is in both lists, this will grey it out twice. - GreyOutAllColorSwatches(); - GreyOutAllToolheads(); - GreyOutAllTouchpadIcons(); - } + /// + /// Greys out every element. + /// + public void GreyOutAll() + { + foreach (Element element in elements.Keys) + { + GreyOut(element); + } - public void GreyOutAllColorSwatches() { - for (int i = 0; i < allColourSwatches.Length; i++) { - GreyOut(allColourSwatches[i].gameObject, 0.4f); - } - } + // The red swatch is in both lists, this will grey it out twice. + GreyOutAllColorSwatches(); + GreyOutAllToolheads(); + GreyOutAllTouchpadIcons(); + } - public void GreyOutAllTouchpadIcons() { - GameObject[] peltzerOverlays = PeltzerMain.Instance.peltzerController.controllerGeometry.overlays; - for (int i = 0; i < peltzerOverlays.Length; i++) { - GreyOut(peltzerOverlays[i].GetComponent().icons); - } + public void GreyOutAllColorSwatches() + { + for (int i = 0; i < allColourSwatches.Length; i++) + { + GreyOut(allColourSwatches[i].gameObject, 0.4f); + } + } - GameObject[] paletteOverlays = PeltzerMain.Instance.paletteController.controllerGeometry.overlays; - for (int i = 0; i < paletteOverlays.Length; i++) { - GreyOut(paletteOverlays[i].GetComponent().icons); - } - } + public void GreyOutAllTouchpadIcons() + { + GameObject[] peltzerOverlays = PeltzerMain.Instance.peltzerController.controllerGeometry.overlays; + for (int i = 0; i < peltzerOverlays.Length; i++) + { + GreyOut(peltzerOverlays[i].GetComponent().icons); + } - public void GreyOutAllToolheads() { - foreach (ControllerMode mode in supportedModes) { - GreyOut(mode); - } - } + GameObject[] paletteOverlays = PeltzerMain.Instance.paletteController.controllerGeometry.overlays; + for (int i = 0; i < paletteOverlays.Length; i++) + { + GreyOut(paletteOverlays[i].GetComponent().icons); + } + } - public void Recolor(ControllerMode mode) { - // The toolheads are cloned when attached to the controller. We need to handle recoloring the cloned version. - if (mode == PeltzerMain.Instance.peltzerController.mode) { - if (PeltzerMain.Instance.peltzerController.attachedToolHead != null) { - PeltzerMain.Instance.peltzerController.attachedToolHead.GetComponent().ChangeToEnable(); + public void GreyOutAllToolheads() + { + foreach (ControllerMode mode in supportedModes) + { + GreyOut(mode); + } } - } - PeltzerMain.Instance.paletteController.GetToolheadForMode(mode).GetComponent() - .ChangeToEnable(); + public void Recolor(ControllerMode mode) + { + // The toolheads are cloned when attached to the controller. We need to handle recoloring the cloned version. + if (mode == PeltzerMain.Instance.peltzerController.mode) + { + if (PeltzerMain.Instance.peltzerController.attachedToolHead != null) + { + PeltzerMain.Instance.peltzerController.attachedToolHead.GetComponent().ChangeToEnable(); + } + } - PeltzerMain.Instance.paletteController.GetToolheadForMode(mode).GetComponent().isActive - = true; + PeltzerMain.Instance.paletteController.GetToolheadForMode(mode).GetComponent() + .ChangeToEnable(); - if (mode == ControllerMode.move) { - PeltzerMain.Instance.GetMover().InvalidateCachedMaterial(); - } - } + PeltzerMain.Instance.paletteController.GetToolheadForMode(mode).GetComponent().isActive + = true; - /// - /// Recolors an element by setting its override amount back to zero. - /// - /// The element to recolor. - public void Recolor(Element which) { - Recolor(elements[which]); - } + if (mode == ControllerMode.move) + { + PeltzerMain.Instance.GetMover().InvalidateCachedMaterial(); + } + } - public void Recolor(GameObject obj) { - if (obj != null) { - obj.GetComponent().material.SetFloat("_OverrideAmount", 0f); + /// + /// Recolors an element by setting its override amount back to zero. + /// + /// The element to recolor. + public void Recolor(Element which) + { + Recolor(elements[which]); + } - if (obj.GetComponent() != null) { - obj.GetComponent().isActive = true; + public void Recolor(GameObject obj) + { + if (obj != null) + { + obj.GetComponent().material.SetFloat("_OverrideAmount", 0f); + + if (obj.GetComponent() != null) + { + obj.GetComponent().isActive = true; + } + + // Recolor the icons on the buttons. + if (obj.GetComponentInChildren() != null) + { + obj.GetComponentInChildren().color = ACTIVE_ICON_COLOR; + } + + // Grey out text on buttons. + if (obj.GetComponentInChildren() != null) + { + obj.GetComponentInChildren().color = ACTIVE_ICON_COLOR; + } + } } - // Recolor the icons on the buttons. - if (obj.GetComponentInChildren() != null) { - obj.GetComponentInChildren().color = ACTIVE_ICON_COLOR; + public void Recolor(SpriteRenderer[] icons) + { + for (int i = 0; i < icons.Length; i++) + { + Recolor(icons[i]); + } } - // Grey out text on buttons. - if (obj.GetComponentInChildren() != null) { - obj.GetComponentInChildren().color = ACTIVE_ICON_COLOR; + public void Recolor(SpriteRenderer icon) + { + icon.color = ACTIVE_ICON_COLOR; } - } - } - public void Recolor(SpriteRenderer[] icons) { - for (int i = 0; i < icons.Length; i++) { - Recolor(icons[i]); - } - } + /// + /// Recolors every element. + /// + public void RecolorAll() + { + foreach (Element element in elements.Keys) + { + Recolor(element); + } - public void Recolor(SpriteRenderer icon) { - icon.color = ACTIVE_ICON_COLOR; - } + // The red swatch is in both lists, this will recolor it out twice. + RecolorAllColorSwatches(); + RecolorAllToolheads(); + RecolorAllTouchpadIcons(); + } - /// - /// Recolors every element. - /// - public void RecolorAll() { - foreach (Element element in elements.Keys) { - Recolor(element); - } - - // The red swatch is in both lists, this will recolor it out twice. - RecolorAllColorSwatches(); - RecolorAllToolheads(); - RecolorAllTouchpadIcons(); - } + public void RecolorAllColorSwatches() + { + for (int i = 0; i < allColourSwatches.Length; i++) + { + Recolor(allColourSwatches[i].gameObject); + } + } - public void RecolorAllColorSwatches() { - for (int i = 0; i < allColourSwatches.Length; i++) { - Recolor(allColourSwatches[i].gameObject); - } - } + public void RecolorAllToolheads() + { + foreach (ControllerMode mode in supportedModes) + { + Recolor(mode); + } + } - public void RecolorAllToolheads() { - foreach (ControllerMode mode in supportedModes) { - Recolor(mode); - } - } + public void RecolorAllTouchpadIcons() + { + GameObject[] peltzerOverlays = PeltzerMain.Instance.peltzerController.controllerGeometry.overlays; + for (int i = 0; i < peltzerOverlays.Length; i++) + { + Recolor(peltzerOverlays[i].GetComponent().icons); + } - public void RecolorAllTouchpadIcons() { - GameObject[] peltzerOverlays = PeltzerMain.Instance.peltzerController.controllerGeometry.overlays; - for (int i = 0; i < peltzerOverlays.Length; i++) { - Recolor(peltzerOverlays[i].GetComponent().icons); - } + GameObject[] paletteOverlays = PeltzerMain.Instance.paletteController.controllerGeometry.overlays; + for (int i = 0; i < paletteOverlays.Length; i++) + { + Recolor(paletteOverlays[i].GetComponent().icons); + } + } - GameObject[] paletteOverlays = PeltzerMain.Instance.paletteController.controllerGeometry.overlays; - for (int i = 0; i < paletteOverlays.Length; i++) { - Recolor(paletteOverlays[i].GetComponent().icons); - } - } + /// + /// Positions and scales a glowing sphere effect in the scene on the trigger or grips. + /// + /// + private void SetSphere(Element which) + { + // This element should be highlighted with a glowing sphere. + GameObject newSphere = Instantiate(spherePrefab); + newSphere.transform.position = elements[which].transform.position; + newSphere.transform.parent = elements[which].transform; + switch (which) + { + case Element.PELTZER_TRIGGER: + newSphere.transform.localScale = new Vector3(TRIGGER_SPHERE_SIZE_MAX, TRIGGER_SPHERE_SIZE_MAX, + TRIGGER_SPHERE_SIZE_MAX); + newSphere.transform.localPosition += TRIGGER_OFFSET; + break; + case Element.PELTZER_GRIP_LEFT: + case Element.PALETTE_GRIP_LEFT: + newSphere.transform.localScale = new Vector3(GRIP_SPHERE_SIZE_MAX, GRIP_SPHERE_SIZE_MAX, + GRIP_SPHERE_SIZE_MAX); + newSphere.transform.localPosition += LEFT_GRIP_OFFSET; + break; + case Element.PELTZER_GRIP_RIGHT: + case Element.PALETTE_GRIP_RIGHT: + newSphere.transform.localScale = new Vector3(GRIP_SPHERE_SIZE_MAX, GRIP_SPHERE_SIZE_MAX, + GRIP_SPHERE_SIZE_MAX); + newSphere.transform.localPosition += RIGHT_GRIP_OFFSET; + break; + default: + break; + } + currentSpheres[which] = newSphere; + } - /// - /// Positions and scales a glowing sphere effect in the scene on the trigger or grips. - /// - /// - private void SetSphere(Element which) { - // This element should be highlighted with a glowing sphere. - GameObject newSphere = Instantiate(spherePrefab); - newSphere.transform.position = elements[which].transform.position; - newSphere.transform.parent = elements[which].transform; - switch (which) { - case Element.PELTZER_TRIGGER: - newSphere.transform.localScale = new Vector3(TRIGGER_SPHERE_SIZE_MAX, TRIGGER_SPHERE_SIZE_MAX, - TRIGGER_SPHERE_SIZE_MAX); - newSphere.transform.localPosition += TRIGGER_OFFSET; - break; - case Element.PELTZER_GRIP_LEFT: - case Element.PALETTE_GRIP_LEFT: - newSphere.transform.localScale = new Vector3(GRIP_SPHERE_SIZE_MAX, GRIP_SPHERE_SIZE_MAX, - GRIP_SPHERE_SIZE_MAX); - newSphere.transform.localPosition += LEFT_GRIP_OFFSET; - break; - case Element.PELTZER_GRIP_RIGHT: - case Element.PALETTE_GRIP_RIGHT: - newSphere.transform.localScale = new Vector3(GRIP_SPHERE_SIZE_MAX, GRIP_SPHERE_SIZE_MAX, - GRIP_SPHERE_SIZE_MAX); - newSphere.transform.localPosition += RIGHT_GRIP_OFFSET; - break; - default: - break; - } - currentSpheres[which] = newSphere; - } + public void StopGlowingAll() + { + foreach (Element element in elements.Keys) + { + StopGlowing(element); + } - public void StopGlowingAll() { - foreach (Element element in elements.Keys) { - StopGlowing(element); - } + foreach (ControllerMode mode in supportedModes) + { + StopGlowing(mode); + } + } - foreach (ControllerMode mode in supportedModes) { - StopGlowing(mode); - } - } + /// + /// Stops glowing the given element. + /// + /// The element to stop glowing. + public void StopGlowing(Element which) + { + if (currentSpheres != null && currentSpheres.ContainsKey(which)) + { + if (currentSpheres[which] != null) + { + Destroy(currentSpheres[which]); + currentSpheres.Remove(which); + } + } + else if (currentlyGlowing != null && elements[which] != null && + currentlyGlowing.ContainsKey(elements[which])) + { + currentlyGlowing.Remove(elements[which]); + SetEmissiveFactor(elements[which], 0, GLOW_BASE_COLOR); + } + } - /// - /// Stops glowing the given element. - /// - /// The element to stop glowing. - public void StopGlowing(Element which) { - if (currentSpheres != null && currentSpheres.ContainsKey(which)) { - if (currentSpheres[which] != null) { - Destroy(currentSpheres[which]); - currentSpheres.Remove(which); - } - } else if (currentlyGlowing != null && elements[which] != null && - currentlyGlowing.ContainsKey(elements[which])) { - currentlyGlowing.Remove(elements[which]); - SetEmissiveFactor(elements[which], 0, GLOW_BASE_COLOR); - } - } + public void StopGlowing(ControllerMode mode) + { + if (mode == PeltzerMain.Instance.peltzerController.mode) + { + StopGlowing(PeltzerMain.Instance.peltzerController.attachedToolHead + .GetComponent().materialObjects); + } - public void StopGlowing(ControllerMode mode) { - if (mode == PeltzerMain.Instance.peltzerController.mode) { - StopGlowing(PeltzerMain.Instance.peltzerController.attachedToolHead - .GetComponent().materialObjects); - } + StopGlowing(PeltzerMain.Instance.paletteController.GetToolheadForMode(mode) + .GetComponent().materialObjects); + } - StopGlowing(PeltzerMain.Instance.paletteController.GetToolheadForMode(mode) - .GetComponent().materialObjects); - } + public void StopGlowing(GameObject[] components) + { + for (int i = 0; i < components.Length; i++) + { + if (currentlyGlowing.ContainsKey(components[i])) + { + currentlyGlowing.Remove(components[i]); + SetEmissiveFactor(components[i], 0, GLOW_BASE_COLOR); + } + } + } - public void StopGlowing(GameObject[] components) { - for (int i = 0; i < components.Length; i++) { - if (currentlyGlowing.ContainsKey(components[i])) { - currentlyGlowing.Remove(components[i]); - SetEmissiveFactor(components[i], 0, GLOW_BASE_COLOR); + /// + /// Claim ownership of rendering the mesh to add a glowing style effect ot it. + /// + /// The mesh to glow. + public void StartMeshGlowing(int meshId) + { + if (claimedMeshes.Contains(meshId)) + { + return; + } + if (PeltzerMain.Instance.model.ClaimMesh(meshId, this) != -1) + { + claimedMeshes.Add(meshId); + PeltzerMain.Instance.highlightUtils.TurnOnMesh(meshId); + PeltzerMain.Instance.highlightUtils.SetMeshStyleToTutorial(meshId); + } + else + { + Debug.LogError("Mesh could not be claimed."); + } } - } - } - /// - /// Claim ownership of rendering the mesh to add a glowing style effect ot it. - /// - /// The mesh to glow. - public void StartMeshGlowing(int meshId) { - if (claimedMeshes.Contains(meshId)) { - return; - } - if (PeltzerMain.Instance.model.ClaimMesh(meshId, this) != -1) { - claimedMeshes.Add(meshId); - PeltzerMain.Instance.highlightUtils.TurnOnMesh(meshId); - PeltzerMain.Instance.highlightUtils.SetMeshStyleToTutorial(meshId); - } else { - Debug.LogError("Mesh could not be claimed."); - } - } + /// + /// Implement the IRenderMeshOwner interface to allow Model to reclaim the mesh + /// from this class if other tools need it. Only Model should call this method. + /// + /// The mesh to claim. + /// The new owner. + /// + public int ClaimMesh(int meshId, IMeshRenderOwner fosterRenderer) + { + if (claimedMeshes.Contains(meshId)) + { + claimedMeshes.Remove(meshId); + PeltzerMain.Instance.highlightUtils.TurnOffMesh(meshId); + return meshId; + } + // Didn't have the mesh. + return -1; + } - /// - /// Implement the IRenderMeshOwner interface to allow Model to reclaim the mesh - /// from this class if other tools need it. Only Model should call this method. - /// - /// The mesh to claim. - /// The new owner. - /// - public int ClaimMesh(int meshId, IMeshRenderOwner fosterRenderer) - { - if (claimedMeshes.Contains(meshId)) { - claimedMeshes.Remove(meshId); - PeltzerMain.Instance.highlightUtils.TurnOffMesh(meshId); - return meshId; - } - // Didn't have the mesh. - return -1; - } + /// + /// Reclaims if need be the mesh that should have the glowing style effect ie in the case of another + /// renderer taking and subsequently relinquishing ownership. + /// + /// The mesh to glow. + public void MakeSureMeshIsGlowing(int meshId) + { + if (!claimedMeshes.Contains(meshId) && PeltzerMain.Instance.model.ClaimMeshIfUnowned(meshId, this) != -1) + { + claimedMeshes.Add(meshId); + PeltzerMain.Instance.highlightUtils.TurnOnMesh(meshId); + PeltzerMain.Instance.highlightUtils.SetMeshStyleToTutorial(meshId); + } + } - /// - /// Reclaims if need be the mesh that should have the glowing style effect ie in the case of another - /// renderer taking and subsequently relinquishing ownership. - /// - /// The mesh to glow. - public void MakeSureMeshIsGlowing(int meshId) { - if (!claimedMeshes.Contains(meshId) && PeltzerMain.Instance.model.ClaimMeshIfUnowned(meshId, this) != -1) { - claimedMeshes.Add(meshId); - PeltzerMain.Instance.highlightUtils.TurnOnMesh(meshId); - PeltzerMain.Instance.highlightUtils.SetMeshStyleToTutorial(meshId); - } - } + /// + /// Stops a mesh from glowing. + /// + /// + public void StopMeshGlowing(int meshId) + { + if (claimedMeshes.Contains(meshId)) + { + claimedMeshes.Remove(meshId); + PeltzerMain.Instance.model.RelinquishMesh(meshId, this); + PeltzerMain.Instance.highlightUtils.TurnOffMesh(meshId); + } + } - /// - /// Stops a mesh from glowing. - /// - /// - public void StopMeshGlowing(int meshId) { - if (claimedMeshes.Contains(meshId)) { - claimedMeshes.Remove(meshId); - PeltzerMain.Instance.model.RelinquishMesh(meshId, this); - PeltzerMain.Instance.highlightUtils.TurnOffMesh(meshId); - } - } + /// + /// Disables attention on all elements. + /// + public void ResetAll() + { + foreach (GameObject obj in currentlyGlowing.Keys) + { + SetEmissiveFactor(obj, 0, GLOW_BASE_COLOR); + } + currentlyGlowing.Clear(); - /// - /// Disables attention on all elements. - /// - public void ResetAll() { - foreach (GameObject obj in currentlyGlowing.Keys) { - SetEmissiveFactor(obj, 0, GLOW_BASE_COLOR); - } - currentlyGlowing.Clear(); + RecolorAll(); + } - RecolorAll(); - } + /// + /// Update the interpolater for lerping between max and min sphere sizes. + /// + private void UpdateT() + { + sizePct += 0.8f * Time.deltaTime; + if (sizePct > 1.0f) + { + sizePct = 0.0f; + } + } - /// - /// Update the interpolater for lerping between max and min sphere sizes. - /// - private void UpdateT() { - sizePct += 0.8f * Time.deltaTime; - if (sizePct > 1.0f) { - sizePct = 0.0f; - } - } + private void Update() + { + UpdateT(); + + if (currentlyGlowing.Count > 0) + { + List elementsToStopGlowing = new List(currentlyGlowing.Count); + + // Update the glowing effect on all the currently glowing objects. + foreach (KeyValuePair glow in currentlyGlowing) + { + if (Time.time > glow.Value.startTime) + { + if (Time.time < glow.Value.endTime) + { + SetEmissiveFactor(glow.Key, Mathf.PingPong((Time.time + glow.Value.startTime) / glow.Value.period, glow.Value.maxEmission), + glow.Key.GetComponent().material.color); + } + else + { + elementsToStopGlowing.Add(glow.Key); + } + } + } + + // Remove any glows that are complete. + foreach (GameObject obj in elementsToStopGlowing) + { + currentlyGlowing.Remove(obj); + SetEmissiveFactor(obj, 0, GLOW_BASE_COLOR); + } + } - private void Update() { - UpdateT(); + foreach (KeyValuePair pair in currentSpheres) + { + SetEmissiveFactor(pair.Value, Mathf.PingPong(Time.time / GLOW_PERIOD, GLOW_MAX_EMISSION), + SPHERE_BASE_COLOR); - if (currentlyGlowing.Count > 0) { - List elementsToStopGlowing = new List(currentlyGlowing.Count); + // Lerp the size of the sphere so it pulses from large to small. + float start_size = pair.Key == Element.PELTZER_TRIGGER ? TRIGGER_SPHERE_SIZE_MIN : GRIP_SPHERE_SIZE_MIN; + float end_size = pair.Key == Element.PELTZER_TRIGGER ? TRIGGER_SPHERE_SIZE_MAX : GRIP_SPHERE_SIZE_MAX; - // Update the glowing effect on all the currently glowing objects. - foreach (KeyValuePair glow in currentlyGlowing) { - if (Time.time > glow.Value.startTime) { - if (Time.time < glow.Value.endTime) { - SetEmissiveFactor(glow.Key, Mathf.PingPong((Time.time + glow.Value.startTime) / glow.Value.period, glow.Value.maxEmission), - glow.Key.GetComponent().material.color); - } else { - elementsToStopGlowing.Add(glow.Key); + float new_size = Mathf.Lerp(start_size, end_size, sizePct); + pair.Value.transform.localScale = new Vector3(new_size, new_size, new_size); } - } } - // Remove any glows that are complete. - foreach (GameObject obj in elementsToStopGlowing) { - currentlyGlowing.Remove(obj); - SetEmissiveFactor(obj, 0, GLOW_BASE_COLOR); + public static void SetEmissiveFactor(GameObject obj, float factor, Color base_color) + { + Color highlightColor = base_color * Mathf.LinearToGammaSpace(factor); + obj.GetComponent().material.SetColor(EMISSIVE_COLOR_VAR_NAME, highlightColor); } - } - - foreach (KeyValuePair pair in currentSpheres) { - SetEmissiveFactor(pair.Value, Mathf.PingPong(Time.time / GLOW_PERIOD, GLOW_MAX_EMISSION), - SPHERE_BASE_COLOR); - - // Lerp the size of the sphere so it pulses from large to small. - float start_size = pair.Key == Element.PELTZER_TRIGGER ? TRIGGER_SPHERE_SIZE_MIN : GRIP_SPHERE_SIZE_MIN; - float end_size = pair.Key == Element.PELTZER_TRIGGER ? TRIGGER_SPHERE_SIZE_MAX : GRIP_SPHERE_SIZE_MAX; - - float new_size = Mathf.Lerp(start_size, end_size, sizePct); - pair.Value.transform.localScale = new Vector3(new_size, new_size, new_size); - } - } - - public static void SetEmissiveFactor(GameObject obj, float factor, Color base_color) { - Color highlightColor = base_color * Mathf.LinearToGammaSpace(factor); - obj.GetComponent().material.SetColor(EMISSIVE_COLOR_VAR_NAME, highlightColor); - } - /// - /// Finds the given child of the given GameObject by name. Throws an exception if the object - /// is not found. - /// - /// The GameObject where to search for the object. - /// The path to the object. - /// The GameObject. - private static GameObject FindOrDie(GameObject parent, string path) { - Transform transform = parent.transform.Find(path); - if (transform == null || transform.gameObject == null) { - throw new Exception("Can't find object '" + path + "' in parent: " + parent.name); - } - return transform.gameObject; + /// + /// Finds the given child of the given GameObject by name. Throws an exception if the object + /// is not found. + /// + /// The GameObject where to search for the object. + /// The path to the object. + /// The GameObject. + private static GameObject FindOrDie(GameObject parent, string path) + { + Transform transform = parent.transform.Find(path); + if (transform == null || transform.gameObject == null) + { + throw new Exception("Can't find object '" + path + "' in parent: " + parent.name); + } + return transform.gameObject; + } } - } } diff --git a/Assets/Scripts/tutorial/FloatingMessage.cs b/Assets/Scripts/tutorial/FloatingMessage.cs index d10e1b41..48a43d39 100644 --- a/Assets/Scripts/tutorial/FloatingMessage.cs +++ b/Assets/Scripts/tutorial/FloatingMessage.cs @@ -21,482 +21,532 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.tutorial { - public enum TextPosition { - NONE, CENTER, HALF_SIDE, FULL_SIDE, BOTTOM, CENTER_NO_TITLE - }; - - /// - /// Floating message that gets shown in front of the user to give tutorial instructions. - /// - public class FloatingMessage : MonoBehaviour { - /// - /// Fixed distance to keep from origin. - /// - private const float DISTANCE_FROM_USER = 2.5f; - - /// - /// Maximum angle between message and camera. If the angle becomes bigger, the message moves to follow - /// the camera's heading. - /// - private const float MAX_ANGLE_FROM_CAMERA = 45.0f; - - /// - /// Duration of the fade-in animation when showing a new message. - /// - private const float FADE_IN_DURATION = 0.5f; - - /// - /// Starting alpha value for the fade in animation. - /// - private const float FADE_START_ALPHA = 0.7f; +namespace com.google.apps.peltzer.client.tutorial +{ + public enum TextPosition + { + NONE, CENTER, HALF_SIDE, FULL_SIDE, BOTTOM, CENTER_NO_TITLE + }; /// - /// String prefix for source of the progress bar images. Intermediate images are named "bar_1", "bar_2", - /// etc. and are bookended by "bar_empty" and "bar_full". + /// Floating message that gets shown in front of the user to give tutorial instructions. /// - private const string PROGRESS_BAR_IMAGE_PATH_PREFIX = "Tutorial/Textures/bar_"; - - /// - /// Max number of progress bar images; used to check that we are not over-incrementing and attempting - /// to find an image that does not actually exist. - /// - private const int PROGRESS_BAR_MAX = 13; + public class FloatingMessage : MonoBehaviour + { + /// + /// Fixed distance to keep from origin. + /// + private const float DISTANCE_FROM_USER = 2.5f; + + /// + /// Maximum angle between message and camera. If the angle becomes bigger, the message moves to follow + /// the camera's heading. + /// + private const float MAX_ANGLE_FROM_CAMERA = 45.0f; + + /// + /// Duration of the fade-in animation when showing a new message. + /// + private const float FADE_IN_DURATION = 0.5f; + + /// + /// Starting alpha value for the fade in animation. + /// + private const float FADE_START_ALPHA = 0.7f; + + /// + /// String prefix for source of the progress bar images. Intermediate images are named "bar_1", "bar_2", + /// etc. and are bookended by "bar_empty" and "bar_full". + /// + private const string PROGRESS_BAR_IMAGE_PATH_PREFIX = "Tutorial/Textures/bar_"; + + /// + /// Max number of progress bar images; used to check that we are not over-incrementing and attempting + /// to find an image that does not actually exist. + /// + private const int PROGRESS_BAR_MAX = 13; + + /// + /// Current fill index of the progress bar. + /// + private int currentProgressBarCount = 0; + + private static readonly Color TEXT_COLOR_NORMAL = new Color(0.3f, 0.3f, 0.3f, 1.0f); + private static readonly Color TEXT_COLOR_SUCCESS = new Color(1.0f, 1.0f, 1.0f, 1.0f); + private static readonly Color BACKGROUND_NORMAL = new Color(1.0f, 1.0f, 1.0f, 0.8f); + private static readonly Color BACKGROUND_SUCCESS = new Color(86f / 255f, 196f / 255f, 22f / 255f, 1.0f); + + public GameObject billboardHolder; + public GameObject billboard; + public GameObject rocks; + private TextMeshPro fullSideMessageText; + private TextMeshPro halfSideMessageText; + private TextMeshPro centerMessageText; + private TextMeshPro centerNoTitleMessageText; + private TextMeshPro bottomMessageText; + private TextMeshPro headerText; + private GameObject progressBarHolder; + private RawImage messageProgressBar; + + // Particle systems that make up the confetti effect. + private ParticleSystem confetti1; + private ParticleSystem confetti2; + private ParticleSystem confetti3; + private ParticleSystem confetti4; + private ParticleSystem finalConfetti1; + private ParticleSystem finalConfetti2; + private ParticleSystem finalConfetti3; + private ParticleSystem finalConfetti4; + private float CONFETTI_RADIUS_MIN = 0.25f; + private float CONFETTI_RADIUS_MAX = 0.65f; + private float CONFETTI_RADIUS_FINAL = 1f; + + // Animations associated with various messages. + private GameObject moveGIF; + private GameObject zoomGIF; + private GameObject selectSphereGIF; + private GameObject insertSphereGIF; + private GameObject smallerSphereGIF; + private GameObject insertAnotherSphereGIF; + private GameObject choosePaintbrushGIF; + private GameObject chooseColorGIF; + private GameObject paintColorGIF; + private GameObject chooseGrabGIF; + private GameObject multiselectGIF; + private GameObject copyGIF; + private GameObject chooseEraserGIF; + private GameObject eraseGIF; + + // Animating the message's position and rotation. + private const float ANIMATION_DURATION = 1.5f; + // Animating the message appearing/disappearing. + private const float ANIMATION_SCALE_DURATION = 0.3f; + // Time to wait before hiding the post. + private const float WAIT_TO_HIDE_DURATION = 0.25f; + // The power of the quadratic function used to animate. + private const float ANIMATION_QUADRATIC = 5f; + + private bool animating; + private float timeStartedAnimating; + private Vector3 positionAtAnimationStart; + private Quaternion rotationAtAnimationStart; + private Vector3 targetPosition; + private Quaternion targetRotation; + + /// + /// Indicates at what rotation (from forward) we are currently showing the message. + /// + private Quaternion messageRotation = Quaternion.identity; + + /// + /// Time (as given by Time.realtimeSinceStartup) when the last message was shown. + /// Used to compute animation. + /// + private float messageShownTime; + + /// + /// The time that the post should be hidden. + /// + private float timeToHide; + /// + /// Whether we are animating the post out by scaling it down. + /// + private bool isScaleOutAnimation; + /// + /// Whether we are animating the post in by scaling it up. + /// + private bool isScaleInAnimation; + /// + /// Whether we are animating the post by rotating it either up or down. + /// + private bool isRotationAnimation; + /// + /// Whether the post is waiting to hide. + /// + private bool waitingToHide; + + /// + /// Advances the FloatingMessage progress bar by incrementing the state index and fetching the + /// new image. + /// + public void IncrementProgressBar() + { + currentProgressBarCount += 1; + // Make sure we do not look for a image that does not exist. + if (currentProgressBarCount > PROGRESS_BAR_MAX) + { + messageProgressBar.texture = Resources.Load(PROGRESS_BAR_IMAGE_PATH_PREFIX + "full"); + return; + } + string progressBarImagePath = PROGRESS_BAR_IMAGE_PATH_PREFIX + currentProgressBarCount.ToString(); + messageProgressBar.texture = Resources.Load(progressBarImagePath); + } - /// - /// Current fill index of the progress bar. - /// - private int currentProgressBarCount = 0; - - private static readonly Color TEXT_COLOR_NORMAL = new Color(0.3f, 0.3f, 0.3f, 1.0f); - private static readonly Color TEXT_COLOR_SUCCESS = new Color(1.0f, 1.0f, 1.0f, 1.0f); - private static readonly Color BACKGROUND_NORMAL = new Color(1.0f, 1.0f, 1.0f, 0.8f); - private static readonly Color BACKGROUND_SUCCESS = new Color(86f/255f, 196f/255f, 22f/255f, 1.0f); - - public GameObject billboardHolder; - public GameObject billboard; - public GameObject rocks; - private TextMeshPro fullSideMessageText; - private TextMeshPro halfSideMessageText; - private TextMeshPro centerMessageText; - private TextMeshPro centerNoTitleMessageText; - private TextMeshPro bottomMessageText; - private TextMeshPro headerText; - private GameObject progressBarHolder; - private RawImage messageProgressBar; - - // Particle systems that make up the confetti effect. - private ParticleSystem confetti1; - private ParticleSystem confetti2; - private ParticleSystem confetti3; - private ParticleSystem confetti4; - private ParticleSystem finalConfetti1; - private ParticleSystem finalConfetti2; - private ParticleSystem finalConfetti3; - private ParticleSystem finalConfetti4; - private float CONFETTI_RADIUS_MIN = 0.25f; - private float CONFETTI_RADIUS_MAX = 0.65f; - private float CONFETTI_RADIUS_FINAL = 1f; - - // Animations associated with various messages. - private GameObject moveGIF; - private GameObject zoomGIF; - private GameObject selectSphereGIF; - private GameObject insertSphereGIF; - private GameObject smallerSphereGIF; - private GameObject insertAnotherSphereGIF; - private GameObject choosePaintbrushGIF; - private GameObject chooseColorGIF; - private GameObject paintColorGIF; - private GameObject chooseGrabGIF; - private GameObject multiselectGIF; - private GameObject copyGIF; - private GameObject chooseEraserGIF; - private GameObject eraseGIF; - - // Animating the message's position and rotation. - private const float ANIMATION_DURATION = 1.5f; - // Animating the message appearing/disappearing. - private const float ANIMATION_SCALE_DURATION = 0.3f; - // Time to wait before hiding the post. - private const float WAIT_TO_HIDE_DURATION = 0.25f; - // The power of the quadratic function used to animate. - private const float ANIMATION_QUADRATIC = 5f; - - private bool animating; - private float timeStartedAnimating; - private Vector3 positionAtAnimationStart; - private Quaternion rotationAtAnimationStart; - private Vector3 targetPosition; - private Quaternion targetRotation; + /// + /// Stops all confetti particle systems. + /// + private void StopConfetti() + { + confetti1.Stop(); + confetti2.Stop(); + confetti3.Stop(); + confetti4.Stop(); + finalConfetti1.Stop(); + finalConfetti2.Stop(); + finalConfetti3.Stop(); + finalConfetti4.Stop(); + } - /// - /// Indicates at what rotation (from forward) we are currently showing the message. - /// - private Quaternion messageRotation = Quaternion.identity; + private void PlayAndSizeConfetti(ParticleSystem confetti, float size) + { + ParticleSystem.ShapeModule shape = confetti.shape; + shape.radius = size; + confetti.Play(); + } - /// - /// Time (as given by Time.realtimeSinceStartup) when the last message was shown. - /// Used to compute animation. - /// - private float messageShownTime; + /// + /// Plays a celebratory confetti effect. + /// + public void PlayConfetti() + { + PlayAndSizeConfetti(confetti1, UnityEngine.Random.Range(CONFETTI_RADIUS_MIN, CONFETTI_RADIUS_MAX)); + PlayAndSizeConfetti(confetti2, UnityEngine.Random.Range(CONFETTI_RADIUS_MIN, CONFETTI_RADIUS_MAX)); + PlayAndSizeConfetti(confetti3, UnityEngine.Random.Range(CONFETTI_RADIUS_MIN, CONFETTI_RADIUS_MAX)); + PlayAndSizeConfetti(confetti4, UnityEngine.Random.Range(CONFETTI_RADIUS_MIN, CONFETTI_RADIUS_MAX)); + } - /// - /// The time that the post should be hidden. - /// - private float timeToHide; - /// - /// Whether we are animating the post out by scaling it down. - /// - private bool isScaleOutAnimation; - /// - /// Whether we are animating the post in by scaling it up. - /// - private bool isScaleInAnimation; - /// - /// Whether we are animating the post by rotating it either up or down. - /// - private bool isRotationAnimation; - /// - /// Whether the post is waiting to hide. - /// - private bool waitingToHide; + public void PlayFinalConfetti() + { + PlayAndSizeConfetti(finalConfetti1, CONFETTI_RADIUS_FINAL); + PlayAndSizeConfetti(finalConfetti2, CONFETTI_RADIUS_FINAL); + PlayAndSizeConfetti(finalConfetti3, CONFETTI_RADIUS_FINAL); + PlayAndSizeConfetti(finalConfetti4, CONFETTI_RADIUS_FINAL); + } - /// - /// Advances the FloatingMessage progress bar by incrementing the state index and fetching the - /// new image. - /// - public void IncrementProgressBar() { - currentProgressBarCount += 1; - // Make sure we do not look for a image that does not exist. - if (currentProgressBarCount > PROGRESS_BAR_MAX) { - messageProgressBar.texture = Resources.Load(PROGRESS_BAR_IMAGE_PATH_PREFIX + "full"); - return; - } - string progressBarImagePath = PROGRESS_BAR_IMAGE_PATH_PREFIX + currentProgressBarCount.ToString(); - messageProgressBar.texture = Resources.Load(progressBarImagePath); - } + public void PositionBillboard() + { + timeStartedAnimating = Time.time; + animating = true; + isScaleInAnimation = true; + isScaleOutAnimation = false; + waitingToHide = false; + + // Move the post so it's in front of the user. + Quaternion yRotation = Quaternion.Euler(0f, Camera.main.transform.rotation.eulerAngles.y, 0f); + Vector3 forwardPosition = Camera.main.transform.position + (yRotation * Vector3.forward * DISTANCE_FROM_USER); + billboardHolder.transform.position = + new Vector3(forwardPosition.x, billboardHolder.transform.position.y, forwardPosition.z); + + // Rotate the post so its pointing towards the user but laying on the ground. + float yAngle = Camera.main.transform.eulerAngles.y; + billboard.transform.rotation = Quaternion.Euler(90f, yAngle, 0); + + rotationAtAnimationStart = billboard.transform.rotation; + targetRotation = Quaternion.Euler(0f, yAngle, 0f); + + billboard.transform.localScale = Vector3.zero; + rocks.transform.localScale = Vector3.zero; + billboardHolder.SetActive(true); + StopConfetti(); + } - /// - /// Stops all confetti particle systems. - /// - private void StopConfetti() { - confetti1.Stop(); - confetti2.Stop(); - confetti3.Stop(); - confetti4.Stop(); - finalConfetti1.Stop(); - finalConfetti2.Stop(); - finalConfetti3.Stop(); - finalConfetti4.Stop(); - } + public void FadeOutBillboard() + { + timeStartedAnimating = Time.time; + animating = true; + waitingToHide = true; - private void PlayAndSizeConfetti(ParticleSystem confetti, float size) { - ParticleSystem.ShapeModule shape = confetti.shape; - shape.radius = size; - confetti.Play(); - } + float yAngle = Camera.main.transform.eulerAngles.y; - /// - /// Plays a celebratory confetti effect. - /// - public void PlayConfetti() { - PlayAndSizeConfetti(confetti1, UnityEngine.Random.Range(CONFETTI_RADIUS_MIN, CONFETTI_RADIUS_MAX)); - PlayAndSizeConfetti(confetti2, UnityEngine.Random.Range(CONFETTI_RADIUS_MIN, CONFETTI_RADIUS_MAX)); - PlayAndSizeConfetti(confetti3, UnityEngine.Random.Range(CONFETTI_RADIUS_MIN, CONFETTI_RADIUS_MAX)); - PlayAndSizeConfetti(confetti4, UnityEngine.Random.Range(CONFETTI_RADIUS_MIN, CONFETTI_RADIUS_MAX)); - } + rotationAtAnimationStart = billboard.transform.rotation; + targetRotation = Quaternion.Euler(90f, yAngle, 0f); + } - public void PlayFinalConfetti() { - PlayAndSizeConfetti(finalConfetti1, CONFETTI_RADIUS_FINAL); - PlayAndSizeConfetti(finalConfetti2, CONFETTI_RADIUS_FINAL); - PlayAndSizeConfetti(finalConfetti3, CONFETTI_RADIUS_FINAL); - PlayAndSizeConfetti(finalConfetti4, CONFETTI_RADIUS_FINAL); - } + /// + /// Reset the progress bar to an empty state. + /// + public void ResetProgressBar() + { + messageProgressBar.texture = Resources.Load(PROGRESS_BAR_IMAGE_PATH_PREFIX + "empty"); + currentProgressBarCount = 0; + } - public void PositionBillboard() { - timeStartedAnimating = Time.time; - animating = true; - isScaleInAnimation = true; - isScaleOutAnimation = false; - waitingToHide = false; - - // Move the post so it's in front of the user. - Quaternion yRotation = Quaternion.Euler(0f, Camera.main.transform.rotation.eulerAngles.y, 0f); - Vector3 forwardPosition = Camera.main.transform.position + (yRotation * Vector3.forward * DISTANCE_FROM_USER); - billboardHolder.transform.position = - new Vector3(forwardPosition.x, billboardHolder.transform.position.y, forwardPosition.z); - - // Rotate the post so its pointing towards the user but laying on the ground. - float yAngle = Camera.main.transform.eulerAngles.y; - billboard.transform.rotation = Quaternion.Euler(90f, yAngle, 0); - - rotationAtAnimationStart = billboard.transform.rotation; - targetRotation = Quaternion.Euler(0f, yAngle, 0f); - - billboard.transform.localScale = Vector3.zero; - rocks.transform.localScale = Vector3.zero; - billboardHolder.SetActive(true); - StopConfetti(); - } + public void ShowHeader(string header) + { + headerText.SetText(header); + } - public void FadeOutBillboard() { - timeStartedAnimating = Time.time; - animating = true; - waitingToHide = true; + /// + /// Shows a message in the given style. + /// + /// The message to show. + /// Whether or not to play a confetti effect. + /// Whether or not to show the header phrase. + public void Show(string message, TextPosition textPosition, bool playConfetti = false, + bool showHeader = false) + { + bool wasActive = billboardHolder.activeSelf; + billboardHolder.SetActive(true); + + // Show the message and set up the background color for the box. + if (textPosition == TextPosition.CENTER) + { + centerMessageText.SetText(message); + centerNoTitleMessageText.SetText(""); + fullSideMessageText.SetText(""); + halfSideMessageText.SetText(""); + bottomMessageText.SetText(""); + } + else if (textPosition == TextPosition.HALF_SIDE) + { + centerMessageText.SetText(""); + centerNoTitleMessageText.SetText(""); + fullSideMessageText.SetText(""); + halfSideMessageText.SetText(message); + bottomMessageText.SetText(""); + } + else if (textPosition == TextPosition.FULL_SIDE) + { + centerMessageText.SetText(""); + centerNoTitleMessageText.SetText(""); + fullSideMessageText.SetText(message); + halfSideMessageText.SetText(""); + bottomMessageText.SetText(""); + } + else if (textPosition == TextPosition.BOTTOM) + { + centerMessageText.SetText(""); + centerNoTitleMessageText.SetText(""); + fullSideMessageText.SetText(""); + halfSideMessageText.SetText(""); + bottomMessageText.SetText(message); + } + else if (textPosition == TextPosition.CENTER_NO_TITLE) + { + centerMessageText.SetText(""); + centerNoTitleMessageText.SetText(message); + fullSideMessageText.SetText(""); + halfSideMessageText.SetText(""); + bottomMessageText.SetText(""); + } - float yAngle = Camera.main.transform.eulerAngles.y; + messageShownTime = Time.realtimeSinceStartup; - rotationAtAnimationStart = billboard.transform.rotation; - targetRotation = Quaternion.Euler(90f, yAngle, 0f); - } + if (playConfetti) + { + PlayConfetti(); + } - /// - /// Reset the progress bar to an empty state. - /// - public void ResetProgressBar() { - messageProgressBar.texture = Resources.Load(PROGRESS_BAR_IMAGE_PATH_PREFIX + "empty"); - currentProgressBarCount = 0; - } + if (!wasActive) + { + StopConfetti(); + } + } - public void ShowHeader(string header) { - headerText.SetText(header); - } + /// + /// Hides the message that's currently being shown. + /// + public void Hide() + { + billboardHolder.SetActive(false); + } - /// - /// Shows a message in the given style. - /// - /// The message to show. - /// Whether or not to play a confetti effect. - /// Whether or not to show the header phrase. - public void Show(string message, TextPosition textPosition, bool playConfetti = false, - bool showHeader = false) { - bool wasActive = billboardHolder.activeSelf; - billboardHolder.SetActive(true); - - // Show the message and set up the background color for the box. - if (textPosition == TextPosition.CENTER) { - centerMessageText.SetText(message); - centerNoTitleMessageText.SetText(""); - fullSideMessageText.SetText(""); - halfSideMessageText.SetText(""); - bottomMessageText.SetText(""); - } else if (textPosition == TextPosition.HALF_SIDE) { - centerMessageText.SetText(""); - centerNoTitleMessageText.SetText(""); - fullSideMessageText.SetText(""); - halfSideMessageText.SetText(message); - bottomMessageText.SetText(""); - } else if (textPosition == TextPosition.FULL_SIDE) { - centerMessageText.SetText(""); - centerNoTitleMessageText.SetText(""); - fullSideMessageText.SetText(message); - halfSideMessageText.SetText(""); - bottomMessageText.SetText(""); - } else if (textPosition == TextPosition.BOTTOM) { - centerMessageText.SetText(""); - centerNoTitleMessageText.SetText(""); - fullSideMessageText.SetText(""); - halfSideMessageText.SetText(""); - bottomMessageText.SetText(message); - } else if (textPosition == TextPosition.CENTER_NO_TITLE) { - centerMessageText.SetText(""); - centerNoTitleMessageText.SetText(message); - fullSideMessageText.SetText(""); - halfSideMessageText.SetText(""); - bottomMessageText.SetText(""); - } - - messageShownTime = Time.realtimeSinceStartup; - - if (playConfetti) { - PlayConfetti(); - } - - if (!wasActive) { - StopConfetti(); - } - } + public void ShowProgressBar(bool show) + { + progressBarHolder.SetActive(show); + } - /// - /// Hides the message that's currently being shown. - /// - public void Hide() { - billboardHolder.SetActive(false); - } + public void ShowGIF(String name) + { + // turn off other GIF instances. + HideAllGIFs(); + + // Turn on specified instance + switch (name) + { + case "MOVE": + moveGIF.SetActive(true); + break; + case "ZOOM": + zoomGIF.SetActive(true); + break; + case "SELECT_SPHERE": + selectSphereGIF.SetActive(true); + break; + case "INSERT_SPHERE": + insertSphereGIF.SetActive(true); + break; + case "SMALLER_SPHERE": + smallerSphereGIF.SetActive(true); + break; + case "INSERT_ANOTHER_SPHERE": + insertAnotherSphereGIF.SetActive(true); + break; + case "CHOOSE_PAINTBRUSH": + choosePaintbrushGIF.SetActive(true); + break; + case "CHOOSE_COLOR": + chooseColorGIF.SetActive(true); + break; + case "PAINT_COLOR": + paintColorGIF.SetActive(true); + break; + case "CHOOSE_GRAB": + chooseGrabGIF.SetActive(true); + break; + case "MULTISELECT": + multiselectGIF.SetActive(true); + break; + case "COPY": + copyGIF.SetActive(true); + break; + case "CHOOSE_ERASER": + chooseEraserGIF.SetActive(true); + break; + case "ERASE": + eraseGIF.SetActive(true); + break; + } + } - public void ShowProgressBar(bool show) { - progressBarHolder.SetActive(show); - } - - public void ShowGIF(String name) { - // turn off other GIF instances. - HideAllGIFs(); - - // Turn on specified instance - switch(name) { - case "MOVE": - moveGIF.SetActive(true); - break; - case "ZOOM": - zoomGIF.SetActive(true); - break; - case "SELECT_SPHERE": - selectSphereGIF.SetActive(true); - break; - case "INSERT_SPHERE": - insertSphereGIF.SetActive(true); - break; - case "SMALLER_SPHERE": - smallerSphereGIF.SetActive(true); - break; - case "INSERT_ANOTHER_SPHERE": - insertAnotherSphereGIF.SetActive(true); - break; - case "CHOOSE_PAINTBRUSH": - choosePaintbrushGIF.SetActive(true); - break; - case "CHOOSE_COLOR": - chooseColorGIF.SetActive(true); - break; - case "PAINT_COLOR": - paintColorGIF.SetActive(true); - break; - case "CHOOSE_GRAB": - chooseGrabGIF.SetActive(true); - break; - case "MULTISELECT": - multiselectGIF.SetActive(true); - break; - case "COPY": - copyGIF.SetActive(true); - break; - case "CHOOSE_ERASER": - chooseEraserGIF.SetActive(true); - break; - case "ERASE": - eraseGIF.SetActive(true); - break; - } - } + /// + /// Hide all instances of the GIF objects. + /// + public void HideAllGIFs() + { + moveGIF.SetActive(false); + zoomGIF.SetActive(false); + selectSphereGIF.SetActive(false); + insertSphereGIF.SetActive(false); + smallerSphereGIF.SetActive(false); + insertAnotherSphereGIF.SetActive(false); + choosePaintbrushGIF.SetActive(false); + chooseColorGIF.SetActive(false); + paintColorGIF.SetActive(false); + chooseGrabGIF.SetActive(false); + multiselectGIF.SetActive(false); + copyGIF.SetActive(false); + chooseEraserGIF.SetActive(false); + eraseGIF.SetActive(false); + } - /// - /// Hide all instances of the GIF objects. - /// - public void HideAllGIFs() { - moveGIF.SetActive(false); - zoomGIF.SetActive(false); - selectSphereGIF.SetActive(false); - insertSphereGIF.SetActive(false); - smallerSphereGIF.SetActive(false); - insertAnotherSphereGIF.SetActive(false); - choosePaintbrushGIF.SetActive(false); - chooseColorGIF.SetActive(false); - paintColorGIF.SetActive(false); - chooseGrabGIF.SetActive(false); - multiselectGIF.SetActive(false); - copyGIF.SetActive(false); - chooseEraserGIF.SetActive(false); - eraseGIF.SetActive(false); - } - - public void Setup() { - billboard = ObjectFinder.ObjectById("ID_Billboard"); - billboardHolder = ObjectFinder.ObjectById("ID_BillboardHolder"); - rocks = ObjectFinder.ObjectById("ID_Rocks"); - fullSideMessageText = ObjectFinder.ComponentById("ID_BillboardFullSideText"); - halfSideMessageText = ObjectFinder.ComponentById("ID_BillboardHalfSideText"); - centerMessageText = ObjectFinder.ComponentById("ID_BillboardCenterText"); - centerNoTitleMessageText = ObjectFinder.ComponentById("ID_BillboardCenterNoTitleText"); - bottomMessageText = ObjectFinder.ComponentById("ID_BillboardBottomText"); - headerText = ObjectFinder.ComponentById("ID_BillboardHeaderText"); - messageProgressBar = ObjectFinder.ComponentById("ID_TutorialProgressBar"); - progressBarHolder = ObjectFinder.ObjectById("ID_ProgressBarCanvas"); - - confetti1 = ObjectFinder.ComponentById("ID_Confetti1"); - confetti2 = ObjectFinder.ComponentById("ID_Confetti2"); - confetti3 = ObjectFinder.ComponentById("ID_Confetti3"); - confetti4 = ObjectFinder.ComponentById("ID_Confetti4"); - - finalConfetti1 = ObjectFinder.ComponentById("ID_FinalConfetti1"); - finalConfetti2 = ObjectFinder.ComponentById("ID_FinalConfetti2"); - finalConfetti3 = ObjectFinder.ComponentById("ID_FinalConfetti3"); - finalConfetti4 = ObjectFinder.ComponentById("ID_FinalConfetti4"); - - moveGIF = ObjectFinder.ComponentById("ID_MOVE_GIF").gameObject; - zoomGIF = ObjectFinder.ComponentById("ID_ZOOM_GIF").gameObject; - selectSphereGIF = ObjectFinder.ComponentById("ID_SELECT_SPHERE_GIF").gameObject; - insertSphereGIF = ObjectFinder.ComponentById("ID_INSERT_SPHERE_GIF").gameObject; - smallerSphereGIF = ObjectFinder.ComponentById("ID_SMALLER_SPHERE_GIF").gameObject; - insertAnotherSphereGIF = ObjectFinder.ComponentById("ID_INSERT_ANOTHER_SPHERE_GIF").gameObject; - choosePaintbrushGIF = ObjectFinder.ComponentById("ID_CHOOSE_PAINTBRUSH_GIF").gameObject; - chooseColorGIF = ObjectFinder.ComponentById("ID_CHOOSE_COLOR_GIF").gameObject; - paintColorGIF = ObjectFinder.ComponentById("ID_PAINT_COLOR_GIF").gameObject; - chooseGrabGIF = ObjectFinder.ComponentById("ID_CHOOSE_GRAB_GIF").gameObject; - multiselectGIF = ObjectFinder.ComponentById("ID_MULTISELECT_GIF").gameObject; - copyGIF = ObjectFinder.ComponentById("ID_COPY_GIF").gameObject; - chooseEraserGIF = ObjectFinder.ComponentById("ID_CHOOSE_ERASER_GIF").gameObject; - eraseGIF = ObjectFinder.ComponentById("ID_ERASE_GIF").gameObject; - // Hide the GIFs - HideAllGIFs(); - - // Make sure the confetti is not playing. - StopConfetti(); - - // Always start with an empty progress bar. - messageProgressBar.texture = Resources.Load(PROGRESS_BAR_IMAGE_PATH_PREFIX + "empty"); - - // Start inactive. - billboardHolder.SetActive(false); - } + public void Setup() + { + billboard = ObjectFinder.ObjectById("ID_Billboard"); + billboardHolder = ObjectFinder.ObjectById("ID_BillboardHolder"); + rocks = ObjectFinder.ObjectById("ID_Rocks"); + fullSideMessageText = ObjectFinder.ComponentById("ID_BillboardFullSideText"); + halfSideMessageText = ObjectFinder.ComponentById("ID_BillboardHalfSideText"); + centerMessageText = ObjectFinder.ComponentById("ID_BillboardCenterText"); + centerNoTitleMessageText = ObjectFinder.ComponentById("ID_BillboardCenterNoTitleText"); + bottomMessageText = ObjectFinder.ComponentById("ID_BillboardBottomText"); + headerText = ObjectFinder.ComponentById("ID_BillboardHeaderText"); + messageProgressBar = ObjectFinder.ComponentById("ID_TutorialProgressBar"); + progressBarHolder = ObjectFinder.ObjectById("ID_ProgressBarCanvas"); + + confetti1 = ObjectFinder.ComponentById("ID_Confetti1"); + confetti2 = ObjectFinder.ComponentById("ID_Confetti2"); + confetti3 = ObjectFinder.ComponentById("ID_Confetti3"); + confetti4 = ObjectFinder.ComponentById("ID_Confetti4"); + + finalConfetti1 = ObjectFinder.ComponentById("ID_FinalConfetti1"); + finalConfetti2 = ObjectFinder.ComponentById("ID_FinalConfetti2"); + finalConfetti3 = ObjectFinder.ComponentById("ID_FinalConfetti3"); + finalConfetti4 = ObjectFinder.ComponentById("ID_FinalConfetti4"); + + moveGIF = ObjectFinder.ComponentById("ID_MOVE_GIF").gameObject; + zoomGIF = ObjectFinder.ComponentById("ID_ZOOM_GIF").gameObject; + selectSphereGIF = ObjectFinder.ComponentById("ID_SELECT_SPHERE_GIF").gameObject; + insertSphereGIF = ObjectFinder.ComponentById("ID_INSERT_SPHERE_GIF").gameObject; + smallerSphereGIF = ObjectFinder.ComponentById("ID_SMALLER_SPHERE_GIF").gameObject; + insertAnotherSphereGIF = ObjectFinder.ComponentById("ID_INSERT_ANOTHER_SPHERE_GIF").gameObject; + choosePaintbrushGIF = ObjectFinder.ComponentById("ID_CHOOSE_PAINTBRUSH_GIF").gameObject; + chooseColorGIF = ObjectFinder.ComponentById("ID_CHOOSE_COLOR_GIF").gameObject; + paintColorGIF = ObjectFinder.ComponentById("ID_PAINT_COLOR_GIF").gameObject; + chooseGrabGIF = ObjectFinder.ComponentById("ID_CHOOSE_GRAB_GIF").gameObject; + multiselectGIF = ObjectFinder.ComponentById("ID_MULTISELECT_GIF").gameObject; + copyGIF = ObjectFinder.ComponentById("ID_COPY_GIF").gameObject; + chooseEraserGIF = ObjectFinder.ComponentById("ID_CHOOSE_ERASER_GIF").gameObject; + eraseGIF = ObjectFinder.ComponentById("ID_ERASE_GIF").gameObject; + // Hide the GIFs + HideAllGIFs(); + + // Make sure the confetti is not playing. + StopConfetti(); + + // Always start with an empty progress bar. + messageProgressBar.texture = Resources.Load(PROGRESS_BAR_IMAGE_PATH_PREFIX + "empty"); + + // Start inactive. + billboardHolder.SetActive(false); + } - private void Update() { - if (!billboard.activeInHierarchy) return; - - if (animating) { - if (isScaleInAnimation) { - float pctDone = - Math3d.CubicBezierEasing(0f, 0f, 0.2f, 1f, (Time.time - timeStartedAnimating) / ANIMATION_SCALE_DURATION); - if (pctDone > 1) { - billboard.transform.localScale = Vector3.one; - rocks.transform.localScale = Vector3.one; - isScaleInAnimation = false; - isRotationAnimation = true; - timeStartedAnimating = Time.time; - } else { - billboard.transform.localScale = Vector3.Lerp(Vector3.zero, Vector3.one, pctDone); - rocks.transform.localScale = Vector3.Lerp(Vector3.zero, Vector3.one, pctDone); - } - } else if (isRotationAnimation) { - float pctDone = - Math3d.CubicBezierEasing(0f, 0f, 0f, 1f, (Time.time - timeStartedAnimating) / ANIMATION_DURATION); - if (pctDone > 1) { - billboard.transform.rotation = targetRotation; - AudioLibrary audioLibrary = PeltzerMain.Instance.audioLibrary; - audioLibrary.PlayClip(audioLibrary.genericSelectSound); - if (waitingToHide) { - isScaleOutAnimation = true; - isRotationAnimation = false; - timeStartedAnimating = Time.time; - } else { - animating = false; + private void Update() + { + if (!billboard.activeInHierarchy) return; + + if (animating) + { + if (isScaleInAnimation) + { + float pctDone = + Math3d.CubicBezierEasing(0f, 0f, 0.2f, 1f, (Time.time - timeStartedAnimating) / ANIMATION_SCALE_DURATION); + if (pctDone > 1) + { + billboard.transform.localScale = Vector3.one; + rocks.transform.localScale = Vector3.one; + isScaleInAnimation = false; + isRotationAnimation = true; + timeStartedAnimating = Time.time; + } + else + { + billboard.transform.localScale = Vector3.Lerp(Vector3.zero, Vector3.one, pctDone); + rocks.transform.localScale = Vector3.Lerp(Vector3.zero, Vector3.one, pctDone); + } + } + else if (isRotationAnimation) + { + float pctDone = + Math3d.CubicBezierEasing(0f, 0f, 0f, 1f, (Time.time - timeStartedAnimating) / ANIMATION_DURATION); + if (pctDone > 1) + { + billboard.transform.rotation = targetRotation; + AudioLibrary audioLibrary = PeltzerMain.Instance.audioLibrary; + audioLibrary.PlayClip(audioLibrary.genericSelectSound); + if (waitingToHide) + { + isScaleOutAnimation = true; + isRotationAnimation = false; + timeStartedAnimating = Time.time; + } + else + { + animating = false; + } + } + else + { + billboard.transform.rotation = Quaternion.Lerp(rotationAtAnimationStart, targetRotation, pctDone); + } + } + else if (isScaleOutAnimation) + { + float pctDone = + Math3d.CubicBezierEasing(0f, 0f, 0.2f, 1f, (Time.time - timeStartedAnimating) / ANIMATION_SCALE_DURATION); + + if (pctDone > 1) + { + animating = false; + Hide(); + } + else + { + billboard.transform.localScale = Vector3.Lerp(Vector3.one, Vector3.zero, pctDone); + rocks.transform.localScale = Vector3.Lerp(Vector3.one, Vector3.zero, pctDone); + } + } } - } else { - billboard.transform.rotation = Quaternion.Lerp(rotationAtAnimationStart, targetRotation, pctDone); - } - } else if (isScaleOutAnimation) { - float pctDone = - Math3d.CubicBezierEasing(0f, 0f, 0.2f, 1f, (Time.time - timeStartedAnimating) / ANIMATION_SCALE_DURATION); - - if (pctDone > 1) { - animating = false; - Hide(); - } else { - billboard.transform.localScale = Vector3.Lerp(Vector3.one, Vector3.zero, pctDone); - rocks.transform.localScale = Vector3.Lerp(Vector3.one, Vector3.zero, pctDone); - } } - } } - } } diff --git a/Assets/Scripts/tutorial/GIFDisplay.cs b/Assets/Scripts/tutorial/GIFDisplay.cs index 4ba9b216..fa00c697 100644 --- a/Assets/Scripts/tutorial/GIFDisplay.cs +++ b/Assets/Scripts/tutorial/GIFDisplay.cs @@ -18,28 +18,31 @@ /// /// This class acts as a pseudo GIF, providing a simple mechanic for looping frame based animation. /// -public class GIFDisplay : MonoBehaviour { - /// - /// The speed of the animation specified as the number of frames per second. - /// - public int framesPerSecond = 2; - /// - /// Arrays of Textures representing the frames of the animation. - /// - public Texture[] frames; - public Texture[] riftFrames; +public class GIFDisplay : MonoBehaviour +{ + /// + /// The speed of the animation specified as the number of frames per second. + /// + public int framesPerSecond = 2; + /// + /// Arrays of Textures representing the frames of the animation. + /// + public Texture[] frames; + public Texture[] riftFrames; - /// - /// Determines which frame to play based on frames per second in a loop. - /// - void Update() { - Texture[] framesToUse = Config.Instance.VrHardware == VrHardware.Vive ? frames : riftFrames; + /// + /// Determines which frame to play based on frames per second in a loop. + /// + void Update() + { + Texture[] framesToUse = Config.Instance.VrHardware == VrHardware.Vive ? frames : riftFrames; - if (frames.Length == 0) { - Debug.Log("no frames!"); - return; + if (frames.Length == 0) + { + Debug.Log("no frames!"); + return; + } + int idx = Mathf.FloorToInt((Time.time * framesPerSecond) % framesToUse.Length); + gameObject.transform.GetComponent().material.mainTexture = framesToUse[idx]; } - int idx = Mathf.FloorToInt((Time.time * framesPerSecond) % framesToUse.Length); - gameObject.transform.GetComponent().material.mainTexture = framesToUse[idx]; - } } diff --git a/Assets/Scripts/tutorial/ITutorialStep.cs b/Assets/Scripts/tutorial/ITutorialStep.cs index ca0cb46f..d9a55125 100644 --- a/Assets/Scripts/tutorial/ITutorialStep.cs +++ b/Assets/Scripts/tutorial/ITutorialStep.cs @@ -14,46 +14,48 @@ using com.google.apps.peltzer.client.model.core; -namespace com.google.apps.peltzer.client.tutorial { - /// - /// Defines a step in a tutorial. - /// - public interface ITutorialStep { +namespace com.google.apps.peltzer.client.tutorial +{ /// - /// Called when preparing the tutorial step for display. The implementation is expected to set up the app's state - /// (load the appropriate models, place meshes, enable/disable tools, modes, etc) and display the appropriate - /// user guidance (tooltips, animations, etc). The implementation should suppose that the previous state of the - /// app is what it was at the end of the OnFinish() call for the previous step, as step[i].OnPrepare() is - /// called after step[i - 1].OnFinish(). + /// Defines a step in a tutorial. /// - void OnPrepare(); + public interface ITutorialStep + { + /// + /// Called when preparing the tutorial step for display. The implementation is expected to set up the app's state + /// (load the appropriate models, place meshes, enable/disable tools, modes, etc) and display the appropriate + /// user guidance (tooltips, animations, etc). The implementation should suppose that the previous state of the + /// app is what it was at the end of the OnFinish() call for the previous step, as step[i].OnPrepare() is + /// called after step[i - 1].OnFinish(). + /// + void OnPrepare(); - /// - /// Called when the user issues a model command while in this step. The step has the option to accept or reject - /// the mutation. - /// - /// The command. - /// True if the command is accepted and should be executed, false if the command is rejected. - bool OnCommand(Command command); + /// + /// Called when the user issues a model command while in this step. The step has the option to accept or reject + /// the mutation. + /// + /// The command. + /// True if the command is accepted and should be executed, false if the command is rejected. + bool OnCommand(Command command); - /// - /// Called once per frame while this step is active. The implementation should validate to check whether or not - /// the user has completed the required action for this step. - /// - /// True if the user has completed the step. False if not. - bool OnValidate(); + /// + /// Called once per frame while this step is active. The implementation should validate to check whether or not + /// the user has completed the required action for this step. + /// + /// True if the user has completed the step. False if not. + bool OnValidate(); - /// - /// Called when the step finishes (after OnValidate() returns true). The implementation is supposed to do any - /// necessary cleanup (typically the opposite of any state changes introduced on OnPrepare, unless those - /// state changes should persist to the next step). - /// - void OnFinish(); + /// + /// Called when the step finishes (after OnValidate() returns true). The implementation is supposed to do any + /// necessary cleanup (typically the opposite of any state changes introduced on OnPrepare, unless those + /// state changes should persist to the next step). + /// + void OnFinish(); - /// - /// Should be called when the step finishes if the step had any state variables that should be reset, or if the - /// tutorial is exitted in the middle of an ongoing step. - /// - void ResetState(); - } + /// + /// Should be called when the step finishes if the step had any state variables that should be reset, or if the + /// tutorial is exitted in the middle of an ongoing step. + /// + void ResetState(); + } } diff --git a/Assets/Scripts/tutorial/IceCreamTutorial.cs b/Assets/Scripts/tutorial/IceCreamTutorial.cs index e1809b2a..1e883c07 100644 --- a/Assets/Scripts/tutorial/IceCreamTutorial.cs +++ b/Assets/Scripts/tutorial/IceCreamTutorial.cs @@ -21,1410 +21,1581 @@ using com.google.apps.peltzer.client.model.render; using com.google.apps.peltzer.client.tools; -namespace com.google.apps.peltzer.client.tutorial { - /// - /// IceCream tutorial. It teaches the user to insert primitives, move, and copy objects. - /// - public class IceCreamTutorial : Tutorial { - private const string POLY_MODEL_RESOURCE_PATH = "Tutorial/IceCreamTutorial"; - private const int SCOOP_PLACEHOLDER_MESH_ID = 10000; - private const int CHERRY_PLACEHOLDER_MESH_ID = 10001; - private const int SCOOP_SCALE_DELTA = 5; // the scale of the sphere for a scoop. - private const int CHERRY_SCALE_DELTA = 0; // the expected scale of the sphere for the cherry. - private const float HAPTIC_PULSE_STRENGTH = 0.15f; - private const float HAPTIC_PULSE_DURATION = 0.15f; - private const float HAPTIC_PULSE_FREQUENCY = 30f; - private const float SUCCESS_SOUND_MINIMUM_PITCH = 0.9f; - private const float SUCCESS_SOUND_MAXIMUM_PITCH = 1.1f; - private const float BUZZ_INTERVAL_DURATION = 1f; - - // Placeholder meshes: used to indicate where things should end up. - private MMesh scoopPlaceHolder; - private MMesh cherryPlaceHolder; - - // IDs of the inserted and cloned cherries. - private int newCherryMeshId; - private int clonedCherryMeshId; - - private static float hapticNotifyTime = 0f; - - // The pitch of the next success sound, and by how much to increment it. - private float nextSuccessSoundPitch; - private float successSoundPitchIncrement; - - public void PlaySuccessSound(bool playMinPitch = false) { - float currentPitch = playMinPitch ? SUCCESS_SOUND_MINIMUM_PITCH : nextSuccessSoundPitch; - - AudioLibrary audioLibrary = PeltzerMain.Instance.audioLibrary; - audioLibrary.PlayClip(audioLibrary.successSound, currentPitch); - - if (!playMinPitch) { - nextSuccessSoundPitch += successSoundPitchIncrement; - } - } - - /// - /// Prepares the tutorial. - /// - public override void OnPrepare() { - PeltzerMain main = PeltzerMain.Instance; - - // Default the pitch correctly. - nextSuccessSoundPitch = SUCCESS_SOUND_MINIMUM_PITCH; - - // Start with the shape tool. - main.peltzerController.mode = ControllerMode.insertVolume; - // Only allow the shape tool for now. - main.restrictionManager.SetOnlyAllowedControllerMode(ControllerMode.insertVolume); - // Allow shape selection. - main.restrictionManager.shapesMenuAllowed = false; - // Do not allow throw to delete objects. - main.restrictionManager.throwAwayAllowed = false; - // Do not allow save or new operations. - main.restrictionManager.menuActionsAllowed = false; - // Allow tutorial menu actions. - main.restrictionManager.tutorialMenuActionsAllowed = true; - // Do not allow changing colors. - main.restrictionManager.changingColorsAllowed = false; - main.restrictionManager.SetOnlyAllowedColor(-2); - // Do not allow switching to the Poly menu. - main.restrictionManager.menuSwitchAllowed = false; - // Do not allow undo/redo until the multiselect step. - main.restrictionManager.undoRedoAllowed = false; - // Do not allow volume filling during insertion. - main.restrictionManager.volumeFillingAllowed = false; - main.restrictionManager.showingWorldBoundingBoxAllowed = false; - - // Do not allow snapping. - main.restrictionManager.snappingAllowed = false; - // Do not allow mesh movement. - main.restrictionManager.movingMeshesAllowed = false; - // Do not allow copying. - main.restrictionManager.copyingAllowed = false; - // Increase the selection radius for the whole tutorial. - main.restrictionManager.increasedMultiSelectRadiusAllowed = true; - main.restrictionManager.SetTouchpadAllowed(TouchpadLocation.NONE); - - // Allow touchpad highlighting/greying out. - main.restrictionManager.SetTouchpadHighlightingAllowed(true); - // Grey out everything to start. - main.attentionCaller.GreyOutAll(); - main.attentionCaller.Recolor(AttentionCaller.Element.SIREN); - main.attentionCaller.Recolor(AttentionCaller.Element.TUTORIAL_BUTTON); - - // Don't let toolheads on the palette change color once we've greyed out all the tools. - main.restrictionManager.toolheadColorChangeAllowed = false; - successSoundPitchIncrement = (SUCCESS_SOUND_MAXIMUM_PITCH - SUCCESS_SOUND_MINIMUM_PITCH) / steps.Count; - - PeltzerMain.Instance.GetFloatingMessage().ResetProgressBar(); - PeltzerMain.Instance.GetFloatingMessage().PositionBillboard(); - PeltzerMain.Instance.GetFloatingMessage().Show("Let's learn the basics!", TextPosition.CENTER); - PeltzerMain.Instance.GetFloatingMessage().ShowHeader(""); - PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); - - PeltzerMain.Instance.GetFloatingMessage().ShowProgressBar(/*show*/ false); - } - +namespace com.google.apps.peltzer.client.tutorial +{ /// - /// Tutorial step where the user gets introduced to the tutorial. + /// IceCream tutorial. It teaches the user to insert primitives, move, and copy objects. /// - private class IntroductionStep : ITutorialStep { - float endTime; - float startMessageTime; - float introTime = 1.5f; - float startMessageDuration = 0.1f; - IceCreamTutorial tutorial; - - public IntroductionStep(IceCreamTutorial tutorial) { - this.tutorial = tutorial; - } - - public void OnPrepare() { - endTime = Time.time + introTime; - startMessageTime = Time.time + startMessageDuration; - } - - public bool OnCommand(Command command) { - // No commands allowed in this step. - return false; - } - - public bool OnValidate() { - if (endTime != 0f && Time.time > endTime) { - return true; + public class IceCreamTutorial : Tutorial + { + private const string POLY_MODEL_RESOURCE_PATH = "Tutorial/IceCreamTutorial"; + private const int SCOOP_PLACEHOLDER_MESH_ID = 10000; + private const int CHERRY_PLACEHOLDER_MESH_ID = 10001; + private const int SCOOP_SCALE_DELTA = 5; // the scale of the sphere for a scoop. + private const int CHERRY_SCALE_DELTA = 0; // the expected scale of the sphere for the cherry. + private const float HAPTIC_PULSE_STRENGTH = 0.15f; + private const float HAPTIC_PULSE_DURATION = 0.15f; + private const float HAPTIC_PULSE_FREQUENCY = 30f; + private const float SUCCESS_SOUND_MINIMUM_PITCH = 0.9f; + private const float SUCCESS_SOUND_MAXIMUM_PITCH = 1.1f; + private const float BUZZ_INTERVAL_DURATION = 1f; + + // Placeholder meshes: used to indicate where things should end up. + private MMesh scoopPlaceHolder; + private MMesh cherryPlaceHolder; + + // IDs of the inserted and cloned cherries. + private int newCherryMeshId; + private int clonedCherryMeshId; + + private static float hapticNotifyTime = 0f; + + // The pitch of the next success sound, and by how much to increment it. + private float nextSuccessSoundPitch; + private float successSoundPitchIncrement; + + public void PlaySuccessSound(bool playMinPitch = false) + { + float currentPitch = playMinPitch ? SUCCESS_SOUND_MINIMUM_PITCH : nextSuccessSoundPitch; + + AudioLibrary audioLibrary = PeltzerMain.Instance.audioLibrary; + audioLibrary.PlayClip(audioLibrary.successSound, currentPitch); + + if (!playMinPitch) + { + nextSuccessSoundPitch += successSoundPitchIncrement; + } } - if (startMessageTime != 0f && Time.time > startMessageTime) { - PeltzerMain.Instance.GetFloatingMessage().Show("Who doesn't like\nice cream?", TextPosition.CENTER); - // Load the broken ice cream scene (one ice cream with missing scoop & cherry). - PeltzerMain.Instance.tutorialManager.LoadAndAlignTutorialModel( - POLY_MODEL_RESOURCE_PATH, - -PeltzerMain.Instance.GetFloatingMessage().billboard.transform.forward, - PeltzerMain.Instance.GetFloatingMessage().billboard.transform.position); - - // Find the key objects we will need (objects the user will interact with and placeholders for - // correct positions). - tutorial.scoopPlaceHolder = PeltzerMain.Instance.model.GetMesh(SCOOP_PLACEHOLDER_MESH_ID); - tutorial.cherryPlaceHolder = PeltzerMain.Instance.model.GetMesh(CHERRY_PLACEHOLDER_MESH_ID); - - // Set the material for the placeholder meshes. - PeltzerMain.Instance.model.ChangeAllFaceProperties(SCOOP_PLACEHOLDER_MESH_ID, - new FaceProperties(MaterialRegistry.PINK_WIREFRAME_ID)); - PeltzerMain.Instance.model.ChangeAllFaceProperties(CHERRY_PLACEHOLDER_MESH_ID, - new FaceProperties(MaterialRegistry.PINK_WIREFRAME_ID)); - - // Hide the placeholders for now (we'll bring them back later). - PeltzerMain.Instance.model.HideMeshForTestOrTutorial(SCOOP_PLACEHOLDER_MESH_ID); - PeltzerMain.Instance.model.HideMeshForTestOrTutorial(CHERRY_PLACEHOLDER_MESH_ID); - - // Set the colour of the shape tool to 'white' - PeltzerMain.Instance.peltzerController.currentMaterial = MaterialRegistry.WHITE_ID; - PeltzerMain.Instance.peltzerController.ChangeToolColor(); - - startMessageTime = 0f; + /// + /// Prepares the tutorial. + /// + public override void OnPrepare() + { + PeltzerMain main = PeltzerMain.Instance; + + // Default the pitch correctly. + nextSuccessSoundPitch = SUCCESS_SOUND_MINIMUM_PITCH; + + // Start with the shape tool. + main.peltzerController.mode = ControllerMode.insertVolume; + // Only allow the shape tool for now. + main.restrictionManager.SetOnlyAllowedControllerMode(ControllerMode.insertVolume); + // Allow shape selection. + main.restrictionManager.shapesMenuAllowed = false; + // Do not allow throw to delete objects. + main.restrictionManager.throwAwayAllowed = false; + // Do not allow save or new operations. + main.restrictionManager.menuActionsAllowed = false; + // Allow tutorial menu actions. + main.restrictionManager.tutorialMenuActionsAllowed = true; + // Do not allow changing colors. + main.restrictionManager.changingColorsAllowed = false; + main.restrictionManager.SetOnlyAllowedColor(-2); + // Do not allow switching to the Poly menu. + main.restrictionManager.menuSwitchAllowed = false; + // Do not allow undo/redo until the multiselect step. + main.restrictionManager.undoRedoAllowed = false; + // Do not allow volume filling during insertion. + main.restrictionManager.volumeFillingAllowed = false; + main.restrictionManager.showingWorldBoundingBoxAllowed = false; + + // Do not allow snapping. + main.restrictionManager.snappingAllowed = false; + // Do not allow mesh movement. + main.restrictionManager.movingMeshesAllowed = false; + // Do not allow copying. + main.restrictionManager.copyingAllowed = false; + // Increase the selection radius for the whole tutorial. + main.restrictionManager.increasedMultiSelectRadiusAllowed = true; + main.restrictionManager.SetTouchpadAllowed(TouchpadLocation.NONE); + + // Allow touchpad highlighting/greying out. + main.restrictionManager.SetTouchpadHighlightingAllowed(true); + // Grey out everything to start. + main.attentionCaller.GreyOutAll(); + main.attentionCaller.Recolor(AttentionCaller.Element.SIREN); + main.attentionCaller.Recolor(AttentionCaller.Element.TUTORIAL_BUTTON); + + // Don't let toolheads on the palette change color once we've greyed out all the tools. + main.restrictionManager.toolheadColorChangeAllowed = false; + successSoundPitchIncrement = (SUCCESS_SOUND_MAXIMUM_PITCH - SUCCESS_SOUND_MINIMUM_PITCH) / steps.Count; + + PeltzerMain.Instance.GetFloatingMessage().ResetProgressBar(); + PeltzerMain.Instance.GetFloatingMessage().PositionBillboard(); + PeltzerMain.Instance.GetFloatingMessage().Show("Let's learn the basics!", TextPosition.CENTER); + PeltzerMain.Instance.GetFloatingMessage().ShowHeader(""); + PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); + + PeltzerMain.Instance.GetFloatingMessage().ShowProgressBar(/*show*/ false); } - return false; - } + /// + /// Tutorial step where the user gets introduced to the tutorial. + /// + private class IntroductionStep : ITutorialStep + { + float endTime; + float startMessageTime; + float introTime = 1.5f; + float startMessageDuration = 0.1f; + IceCreamTutorial tutorial; + + public IntroductionStep(IceCreamTutorial tutorial) + { + this.tutorial = tutorial; + } - public void ResetState() { - startMessageTime = 0f; - endTime = 0f; - } + public void OnPrepare() + { + endTime = Time.time + introTime; + startMessageTime = Time.time + startMessageDuration; + } - public void OnFinish() { - // Reset state variables. - ResetState(); - } - } + public bool OnCommand(Command command) + { + // No commands allowed in this step. + return false; + } - /// - /// Tutorial step where the user is instructed to manipulate world-space. - /// - private class ZoomAndMoveStep : ITutorialStep { - float nextMessageTime; - float nextMessageDuration = 3.0f; - float timeFirstStartedMoving = 0; - float timeStoppedMoving = 0; - bool wasMovingLastFrame = false; - bool isShowingMoveZoomGIF = false; - - private IceCreamTutorial iceCreamTutorial; - - public ZoomAndMoveStep(IceCreamTutorial iceCreamTutorial) { - this.iceCreamTutorial = iceCreamTutorial; - } - - public void OnPrepare() { - // Show instructions. - // TODO This instruction should be rewritten. - PeltzerMain.Instance.GetFloatingMessage().ShowHeader("Moving"); - PeltzerMain.Instance.GetFloatingMessage().Show("Move the cone closer to you", TextPosition.FULL_SIDE); - PeltzerMain.Instance.GetFloatingMessage().ShowGIF("MOVE"); - PeltzerMain.Instance.GetFloatingMessage().ShowProgressBar(/*show*/ true); - PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_GRIP_LEFT); - PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PALETTE_GRIP_LEFT); - PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_GRIP_RIGHT); - PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PALETTE_GRIP_RIGHT); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_GRIP_LEFT); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PALETTE_GRIP_LEFT); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_GRIP_RIGHT); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PALETTE_GRIP_RIGHT); - - nextMessageTime = Time.time + nextMessageDuration; - } - - public bool OnCommand(Command command) { - // No commands allowed in this step. - return false; - } - - public bool OnValidate() { - // We'll wait for 3 seconds after the user starts moving, or until they've stopped zooming for 0.75 seconds. - if ((timeStoppedMoving != 0 && Time.time - timeStoppedMoving > 0.75f) - || (timeFirstStartedMoving != 0 && Time.time - timeFirstStartedMoving > 3f)) { - PeltzerMain.Instance.GetFloatingMessage().ShowHeader(""); - PeltzerMain.Instance.GetFloatingMessage().Show("Looking good", TextPosition.CENTER_NO_TITLE, /*play confetti*/ true); - PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); - return true; - } + public bool OnValidate() + { + if (endTime != 0f && Time.time > endTime) + { + return true; + } + + if (startMessageTime != 0f && Time.time > startMessageTime) + { + PeltzerMain.Instance.GetFloatingMessage().Show("Who doesn't like\nice cream?", TextPosition.CENTER); + // Load the broken ice cream scene (one ice cream with missing scoop & cherry). + PeltzerMain.Instance.tutorialManager.LoadAndAlignTutorialModel( + POLY_MODEL_RESOURCE_PATH, + -PeltzerMain.Instance.GetFloatingMessage().billboard.transform.forward, + PeltzerMain.Instance.GetFloatingMessage().billboard.transform.position); + + // Find the key objects we will need (objects the user will interact with and placeholders for + // correct positions). + tutorial.scoopPlaceHolder = PeltzerMain.Instance.model.GetMesh(SCOOP_PLACEHOLDER_MESH_ID); + tutorial.cherryPlaceHolder = PeltzerMain.Instance.model.GetMesh(CHERRY_PLACEHOLDER_MESH_ID); + + // Set the material for the placeholder meshes. + PeltzerMain.Instance.model.ChangeAllFaceProperties(SCOOP_PLACEHOLDER_MESH_ID, + new FaceProperties(MaterialRegistry.PINK_WIREFRAME_ID)); + PeltzerMain.Instance.model.ChangeAllFaceProperties(CHERRY_PLACEHOLDER_MESH_ID, + new FaceProperties(MaterialRegistry.PINK_WIREFRAME_ID)); + + // Hide the placeholders for now (we'll bring them back later). + PeltzerMain.Instance.model.HideMeshForTestOrTutorial(SCOOP_PLACEHOLDER_MESH_ID); + PeltzerMain.Instance.model.HideMeshForTestOrTutorial(CHERRY_PLACEHOLDER_MESH_ID); + + // Set the colour of the shape tool to 'white' + PeltzerMain.Instance.peltzerController.currentMaterial = MaterialRegistry.WHITE_ID; + PeltzerMain.Instance.peltzerController.ChangeToolColor(); + + startMessageTime = 0f; + } + + return false; + } - if (PeltzerMain.Instance.Zoomer.moving) { - if (timeFirstStartedMoving == 0) { - timeFirstStartedMoving = Time.time; - } - wasMovingLastFrame = true; - } else { - if (wasMovingLastFrame) { - timeStoppedMoving = Time.time; - } - wasMovingLastFrame = false; + public void ResetState() + { + startMessageTime = 0f; + endTime = 0f; + } + + public void OnFinish() + { + // Reset state variables. + ResetState(); + } } - return false; - } - - public void ResetState() { - isShowingMoveZoomGIF = false; - timeFirstStartedMoving = 0; - timeStoppedMoving = 0; - wasMovingLastFrame = false; - } - - public void OnFinish() { - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_GRIP_LEFT); - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PALETTE_GRIP_LEFT); - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_GRIP_RIGHT); - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PALETTE_GRIP_RIGHT); - - // Hide the placeholder. - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_GRIP_LEFT); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PALETTE_GRIP_LEFT); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_GRIP_RIGHT); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PALETTE_GRIP_RIGHT); - PeltzerMain.Instance.GetFloatingMessage().ShowHeader("Inserting"); - PeltzerMain.Instance.GetFloatingMessage().Show("How about another scoop?", TextPosition.CENTER); - PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); - iceCreamTutorial.PlaySuccessSound(); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, - PeltzerMain.Instance.attentionCaller.defaultSirenGlow); - PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); - - PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); - - // Reset state variables. - ResetState(); - } - } + /// + /// Tutorial step where the user is instructed to manipulate world-space. + /// + private class ZoomAndMoveStep : ITutorialStep + { + float nextMessageTime; + float nextMessageDuration = 3.0f; + float timeFirstStartedMoving = 0; + float timeStoppedMoving = 0; + bool wasMovingLastFrame = false; + bool isShowingMoveZoomGIF = false; + + private IceCreamTutorial iceCreamTutorial; + + public ZoomAndMoveStep(IceCreamTutorial iceCreamTutorial) + { + this.iceCreamTutorial = iceCreamTutorial; + } - /// - /// Tutorial step where the user is instructed to select the sphere primitive. - /// - private class SelectSpherePrimitiveStep : ITutorialStep { - private IceCreamTutorial tutorial; - private float nextBuzzTime; - - public SelectSpherePrimitiveStep(IceCreamTutorial tutorial) { - this.tutorial = tutorial; - } - - public void OnPrepare() { - // Set the default scale of the shape to be quite big (for the first scoop). - PeltzerMain.Instance.GetVolumeInserter().SetScaleTo(SCOOP_SCALE_DELTA); - // Move the volumeInserter over to the cylinder. - PeltzerMain.Instance.peltzerController.shapesMenu - .SetShapeMenuItem((int)Primitives.Shape.CYLINDER, /* showMenu */ false); - - PeltzerMain main = PeltzerMain.Instance; - // Allow the touchpad's left button. - main.restrictionManager.touchpadLeftAllowed = true; - // Recolor the touchpad's left button. - main.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_THUMBSTICK); - main.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TOUCHPAD_LEFT); - main.attentionCaller.Recolor( - main.peltzerController.controllerGeometry.volumeInserterOverlay.GetComponent().leftIcon); - // Call attention to the touchpad's left button. - main.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_THUMBSTICK); - main.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TOUCHPAD_LEFT); - PeltzerMain.Instance.restrictionManager.shapesMenuAllowed = true; - - // Look at me. - main.peltzerController.LookAtMe(); - nextBuzzTime = Time.time + BUZZ_INTERVAL_DURATION; - - // Disallow scaling. - main.restrictionManager.scaleOnVolumeInsertionAllowed = false; - // Show instructions. - PeltzerMain.Instance.GetFloatingMessage().Show("Switch to a sphere shape", TextPosition.FULL_SIDE); - PeltzerMain.Instance.GetFloatingMessage().ShowGIF("SELECT_SPHERE"); - } - - public bool OnCommand(Command command) { - // No commands are allowed in this step (no model mutations). - return false; - } - - public bool OnValidate() { - if (PeltzerMain.Instance.peltzerController.shapesMenu.CurrentItemId == (int)Primitives.Shape.CYLINDER - && Time.time > nextBuzzTime) { - PeltzerMain.Instance.peltzerController.LookAtMe(); - nextBuzzTime = Time.time + BUZZ_INTERVAL_DURATION; - } + public void OnPrepare() + { + // Show instructions. + // TODO This instruction should be rewritten. + PeltzerMain.Instance.GetFloatingMessage().ShowHeader("Moving"); + PeltzerMain.Instance.GetFloatingMessage().Show("Move the cone closer to you", TextPosition.FULL_SIDE); + PeltzerMain.Instance.GetFloatingMessage().ShowGIF("MOVE"); + PeltzerMain.Instance.GetFloatingMessage().ShowProgressBar(/*show*/ true); + PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_GRIP_LEFT); + PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PALETTE_GRIP_LEFT); + PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_GRIP_RIGHT); + PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PALETTE_GRIP_RIGHT); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_GRIP_LEFT); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PALETTE_GRIP_LEFT); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_GRIP_RIGHT); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PALETTE_GRIP_RIGHT); + + nextMessageTime = Time.time + nextMessageDuration; + } - // This step is done when the user selects the sphere primitive. - return PeltzerMain.Instance.peltzerController.shapesMenu.CurrentItemId == (int)Primitives.Shape.SPHERE; - } - - public void ResetState() { - // Nothing to reset. - } - - public void OnFinish() { - // You did it! - PeltzerMain.Instance.peltzerController.YouDidIt(); - - PeltzerMain.Instance.restrictionManager.shapesMenuAllowed = false; - PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed = false; - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_THUMBSTICK); - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TOUCHPAD_LEFT); - PeltzerMain.Instance.attentionCaller.GreyOut( - PeltzerMain.Instance.peltzerController.controllerGeometry.volumeInserterOverlay.GetComponent().leftIcon); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_THUMBSTICK); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TOUCHPAD_LEFT); - // Congratulate the user on this outstanding victory. - PeltzerMain.Instance.GetFloatingMessage().Show("Nice\nThat's the one", TextPosition.CENTER, true); - PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); - tutorial.PlaySuccessSound(); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, - PeltzerMain.Instance.attentionCaller.defaultSirenGlow); - PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); - PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); - } - } + public bool OnCommand(Command command) + { + // No commands allowed in this step. + return false; + } - /// - /// Tutorial step where the user is instructed to place an additional scoop. - /// - private class PlaceScoopStep : ITutorialStep { - private const float DISTANCE_TOLERANCE = 0.025f; - private IceCreamTutorial tutorial; - private bool spherePlaced; - private bool completed = false; - - public PlaceScoopStep(IceCreamTutorial tutorial) { - this.tutorial = tutorial; - } - - public void OnPrepare() { - // Allow volume insertion so the user can place the body. - PeltzerMain.Instance.restrictionManager.volumeInsertionAllowed = true; - // Show the body placeholder to guide the user. - PeltzerMain.Instance.model.UnhideMeshForTestOrTutorial(SCOOP_PLACEHOLDER_MESH_ID); - // Recolor the volumeInserter toolhead since the user is going to use it to place the sphere. - PeltzerMain.Instance.attentionCaller.Recolor(ControllerMode.insertVolume); - // Call attention to trigger. - PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - // Show instructions. - PeltzerMain.Instance.GetFloatingMessage().Show("Pile it on top", TextPosition.FULL_SIDE); - PeltzerMain.Instance.GetFloatingMessage().ShowGIF("INSERT_SPHERE"); - } - - public bool OnCommand(Command command) { - // Hack to allow us to delete the wireframe. - if (completed && command is DeleteMeshCommand) { - return true; - } + public bool OnValidate() + { + // We'll wait for 3 seconds after the user starts moving, or until they've stopped zooming for 0.75 seconds. + if ((timeStoppedMoving != 0 && Time.time - timeStoppedMoving > 0.75f) + || (timeFirstStartedMoving != 0 && Time.time - timeFirstStartedMoving > 3f)) + { + PeltzerMain.Instance.GetFloatingMessage().ShowHeader(""); + PeltzerMain.Instance.GetFloatingMessage().Show("Looking good", TextPosition.CENTER_NO_TITLE, /*play confetti*/ true); + PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); + return true; + } + + if (PeltzerMain.Instance.Zoomer.moving) + { + if (timeFirstStartedMoving == 0) + { + timeFirstStartedMoving = Time.time; + } + wasMovingLastFrame = true; + } + else + { + if (wasMovingLastFrame) + { + timeStoppedMoving = Time.time; + } + wasMovingLastFrame = false; + } + + return false; + } - if (spherePlaced || !(command is AddMeshCommand)) { - // Only adding meshes is allowed, and only while the body has not yet been placed. - return false; - } - // Check if the mesh the user wants to add is at the correct position. - float dist = Vector3.Distance(((AddMeshCommand)command).GetMeshClone().offset, - tutorial.scoopPlaceHolder.offset); - if (dist > DISTANCE_TOLERANCE) { - PeltzerMain.Instance.GetFloatingMessage() - .Show("Place it in the right spot", TextPosition.FULL_SIDE); - return false; - } - spherePlaced = true; - PeltzerMain.Instance.GetFloatingMessage().Show("That's better", TextPosition.FULL_SIDE, true); - return true; - } - - public bool OnValidate() { - // This step is done when the user has placed the body at the right place. - return spherePlaced; - } - - public void ResetState() { - spherePlaced = false; - } - - public void OnFinish() { - // You did it! - PeltzerMain.Instance.peltzerController.YouDidIt(); - - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.restrictionManager.volumeInsertionAllowed = false; - // Stop showing the placeholder mesh. - PeltzerMain.Instance.model.HideMeshForTestOrTutorial(SCOOP_PLACEHOLDER_MESH_ID); - completed = true; - PeltzerMain.Instance.model.ApplyCommand(new DeleteMeshCommand(SCOOP_PLACEHOLDER_MESH_ID)); - PeltzerMain.Instance.GetFloatingMessage().Show("Now for the cherry\non top", TextPosition.CENTER, true); - PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); - tutorial.PlaySuccessSound(); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, - PeltzerMain.Instance.attentionCaller.defaultSirenGlow); - PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); - PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); - - // Reset state variables. - ResetState(); - } - } + public void ResetState() + { + isShowingMoveZoomGIF = false; + timeFirstStartedMoving = 0; + timeStoppedMoving = 0; + wasMovingLastFrame = false; + } - /// - /// Tutorial step where the user is instructed to scale down the sphere for a cherry. - /// - private class ScaleSphereStep : ITutorialStep { - private bool encouragementShown; - private float nextBuzzTime; - - private IceCreamTutorial iceCreamTutorial; - - public ScaleSphereStep(IceCreamTutorial iceCreamTutorial) { - this.iceCreamTutorial = iceCreamTutorial; - } - - public void OnPrepare() { - PeltzerMain main = PeltzerMain.Instance; - // Allow primitive scaling, but nothing else. - main.restrictionManager.scaleOnVolumeInsertionAllowed = true; - // Show instructions. - PeltzerMain.Instance.GetFloatingMessage().Show("That's too big\nSize it down", TextPosition.FULL_SIDE); - PeltzerMain.Instance.GetFloatingMessage().ShowGIF("SMALLER_SPHERE"); - // Allow the touchpad's down button. - main.restrictionManager.touchpadDownAllowed = true; - main.restrictionManager.touchpadUpAllowed = false; - // Call attention to the touchpad's up button. - PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_THUMBSTICK); - main.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TOUCHPAD_DOWN); - PeltzerMain.Instance.attentionCaller.Recolor( - PeltzerMain.Instance.peltzerController.controllerGeometry.volumeInserterOverlay.GetComponent().downIcon); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_THUMBSTICK); - main.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TOUCHPAD_DOWN); - - // Look at me. - main.peltzerController.LookAtMe(); - } - - public bool OnCommand(Command command) { - // No commands are allowed in this step (no model mutations). - return false; - } - - public bool OnValidate() { - if (PeltzerMain.Instance.GetVolumeInserter().scaleDelta == SCOOP_SCALE_DELTA - && Time.time > nextBuzzTime) { - PeltzerMain.Instance.peltzerController.LookAtMe(); - nextBuzzTime = Time.time + BUZZ_INTERVAL_DURATION; + public void OnFinish() + { + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_GRIP_LEFT); + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PALETTE_GRIP_LEFT); + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_GRIP_RIGHT); + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PALETTE_GRIP_RIGHT); + + // Hide the placeholder. + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_GRIP_LEFT); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PALETTE_GRIP_LEFT); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_GRIP_RIGHT); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PALETTE_GRIP_RIGHT); + PeltzerMain.Instance.GetFloatingMessage().ShowHeader("Inserting"); + PeltzerMain.Instance.GetFloatingMessage().Show("How about another scoop?", TextPosition.CENTER); + PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); + iceCreamTutorial.PlaySuccessSound(); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, + PeltzerMain.Instance.attentionCaller.defaultSirenGlow); + PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); + + PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); + + // Reset state variables. + ResetState(); + } } - if (!encouragementShown && PeltzerMain.Instance.GetVolumeInserter().scaleDelta < SCOOP_SCALE_DELTA) { - PeltzerMain.Instance.GetFloatingMessage().Show("Keep on going", TextPosition.FULL_SIDE); - encouragementShown = true; - } - // TODO add : You're almost there! - - // This step is done when the user has the right size selected. - // We allow <= because if the user overshoots it with the continuous press, we don't care. - // (the tutorial will still work). - return PeltzerMain.Instance.GetVolumeInserter().scaleDelta <= CHERRY_SCALE_DELTA; - } - - public void ResetState() { - encouragementShown = false; - } - - public void OnFinish() { - // You did it! - PeltzerMain.Instance.peltzerController.YouDidIt(); - - PeltzerMain.Instance.restrictionManager.scaleOnVolumeInsertionAllowed = false; - PeltzerMain.Instance.restrictionManager.touchpadDownAllowed = false; - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_THUMBSTICK); - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TOUCHPAD_DOWN); - PeltzerMain.Instance.attentionCaller.GreyOut( - PeltzerMain.Instance.peltzerController.controllerGeometry.volumeInserterOverlay.GetComponent().downIcon); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_THUMBSTICK); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TOUCHPAD_DOWN); - PeltzerMain.Instance.GetFloatingMessage().Show("Perfect", TextPosition.CENTER, true); - PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); - iceCreamTutorial.PlaySuccessSound(); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, - PeltzerMain.Instance.attentionCaller.defaultSirenGlow); - PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); - PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); - - // Reset state variables. - ResetState(); - } - } + /// + /// Tutorial step where the user is instructed to select the sphere primitive. + /// + private class SelectSpherePrimitiveStep : ITutorialStep + { + private IceCreamTutorial tutorial; + private float nextBuzzTime; + + public SelectSpherePrimitiveStep(IceCreamTutorial tutorial) + { + this.tutorial = tutorial; + } - /// - /// Tutorial step where the user is instructed to place an cherry. - /// - private class PlaceCherryStep : ITutorialStep { - private const float DISTANCE_TOLERANCE = 0.03f; - private IceCreamTutorial tutorial; - private bool cherryPlaced; - - public PlaceCherryStep(IceCreamTutorial tutorial) { - this.tutorial = tutorial; - } - - public void OnPrepare() { - // Allow volume insertion so the user can place the body. - PeltzerMain.Instance.restrictionManager.volumeInsertionAllowed = true; - // Show the body placeholder to guide the user. - PeltzerMain.Instance.model.UnhideMeshForTestOrTutorial(CHERRY_PLACEHOLDER_MESH_ID); - - // Call attention to trigger. - PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - // Show instructions. - PeltzerMain.Instance.GetFloatingMessage().Show("Place the cherry on top", TextPosition.FULL_SIDE); - PeltzerMain.Instance.GetFloatingMessage().ShowGIF("INSERT_ANOTHER_SPHERE"); - } - - public bool OnCommand(Command command) { - // Hack to allow deleting the wireframe. - if (cherryPlaced && command is DeleteMeshCommand) { - return true; - } + public void OnPrepare() + { + // Set the default scale of the shape to be quite big (for the first scoop). + PeltzerMain.Instance.GetVolumeInserter().SetScaleTo(SCOOP_SCALE_DELTA); + // Move the volumeInserter over to the cylinder. + PeltzerMain.Instance.peltzerController.shapesMenu + .SetShapeMenuItem((int)Primitives.Shape.CYLINDER, /* showMenu */ false); + + PeltzerMain main = PeltzerMain.Instance; + // Allow the touchpad's left button. + main.restrictionManager.touchpadLeftAllowed = true; + // Recolor the touchpad's left button. + main.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_THUMBSTICK); + main.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TOUCHPAD_LEFT); + main.attentionCaller.Recolor( + main.peltzerController.controllerGeometry.volumeInserterOverlay.GetComponent().leftIcon); + // Call attention to the touchpad's left button. + main.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_THUMBSTICK); + main.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TOUCHPAD_LEFT); + PeltzerMain.Instance.restrictionManager.shapesMenuAllowed = true; + + // Look at me. + main.peltzerController.LookAtMe(); + nextBuzzTime = Time.time + BUZZ_INTERVAL_DURATION; + + // Disallow scaling. + main.restrictionManager.scaleOnVolumeInsertionAllowed = false; + // Show instructions. + PeltzerMain.Instance.GetFloatingMessage().Show("Switch to a sphere shape", TextPosition.FULL_SIDE); + PeltzerMain.Instance.GetFloatingMessage().ShowGIF("SELECT_SPHERE"); + } - if (cherryPlaced || !(command is AddMeshCommand)) { - // Only adding meshes is allowed, and only while the body has not yet been placed. - return false; - } - // Check if the mesh the user wants to add is at the correct position. - float dist = Vector3.Distance(((AddMeshCommand)command).GetMeshClone().offset, - tutorial.cherryPlaceHolder.offset); - if (dist > DISTANCE_TOLERANCE) { - PeltzerMain.Instance.GetFloatingMessage().Show("Try again", TextPosition.FULL_SIDE); - return false; - } - cherryPlaced = true; - PeltzerMain.Instance.GetFloatingMessage().Show("", TextPosition.CENTER, true); - PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); - tutorial.newCherryMeshId = ((AddMeshCommand)command).GetMeshId(); - return true; - } - - public bool OnValidate() { - // This step is done when the user has placed the cherry at the right place. - return cherryPlaced; - } - - public void ResetState() { - cherryPlaced = false; - } - - public void OnFinish() { - // You did it! - PeltzerMain.Instance.peltzerController.YouDidIt(); - - PeltzerMain.Instance.attentionCaller.GreyOut(ControllerMode.insertVolume); - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.restrictionManager.volumeInsertionAllowed = false; - // Stop showing the placeholder mesh. - PeltzerMain.Instance.model.HideMeshForTestOrTutorial(CHERRY_PLACEHOLDER_MESH_ID); - PeltzerMain.Instance.model.ApplyCommand(new DeleteMeshCommand(CHERRY_PLACEHOLDER_MESH_ID)); - PeltzerMain.Instance.GetFloatingMessage().ShowHeader(""); - PeltzerMain.Instance.GetFloatingMessage() - .Show("Let's paint it RED", TextPosition.CENTER_NO_TITLE); - PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); - tutorial.PlaySuccessSound(); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, - PeltzerMain.Instance.attentionCaller.defaultSirenGlow); - PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); - PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); - - // Reset state variables. - ResetState(); - } - } + public bool OnCommand(Command command) + { + // No commands are allowed in this step (no model mutations). + return false; + } - /// - /// Tutorial step where the user is asked to switch to the paint tool. - /// - private class SwitchToPaintToolStep : ITutorialStep { - private IceCreamTutorial iceCreamTutorial; - private float nextBuzzTime; - - public SwitchToPaintToolStep(IceCreamTutorial iceCreamTutorial) { - this.iceCreamTutorial = iceCreamTutorial; - } - - public void OnPrepare() { - // Only allow volume insertion (current tool) and the paint tool. - PeltzerMain.Instance.restrictionManager.SetAllowedControllerModes( - new List() { ControllerMode.insertVolume, ControllerMode.paintMesh }); - // Call attention to trigger. - PeltzerMain.Instance.attentionCaller.Recolor(ControllerMode.paintMesh); - PeltzerMain.Instance.attentionCaller.StartGlowing(ControllerMode.paintMesh); - PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - // Show instructions. - PeltzerMain.Instance.GetFloatingMessage().ShowHeader("Painting"); - PeltzerMain.Instance.GetFloatingMessage().Show("Pick up the paint brush", TextPosition.BOTTOM); - PeltzerMain.Instance.GetFloatingMessage().ShowGIF("CHOOSE_PAINTBRUSH"); - // Set all the colors to be allowed so that they aren't greyed out. However changingColors is still disabled - // so the user can't select them. We just want them to be colorful for the ripple animation. - PeltzerMain.Instance.attentionCaller.RecolorAllColorSwatches(); - - // Look at me. - PeltzerMain.Instance.paletteController.LookAtMe(); - } - - public bool OnCommand(Command command) { - // No model mutations for now, thank you very much. - return false; - } - - public bool OnValidate() { - if (Time.time > nextBuzzTime) { - PeltzerMain.Instance.paletteController.LookAtMe(); - nextBuzzTime = Time.time + BUZZ_INTERVAL_DURATION; - } + public bool OnValidate() + { + if (PeltzerMain.Instance.peltzerController.shapesMenu.CurrentItemId == (int)Primitives.Shape.CYLINDER + && Time.time > nextBuzzTime) + { + PeltzerMain.Instance.peltzerController.LookAtMe(); + nextBuzzTime = Time.time + BUZZ_INTERVAL_DURATION; + } + + // This step is done when the user selects the sphere primitive. + return PeltzerMain.Instance.peltzerController.shapesMenu.CurrentItemId == (int)Primitives.Shape.SPHERE; + } - // This step is done when the user has selected the paint tool. - return PeltzerMain.Instance.peltzerController.mode == ControllerMode.paintMesh; - } - - public void ResetState() { - // Nothing to reset. - } - - public void OnFinish() { - // You did it! - PeltzerMain.Instance.peltzerController.YouDidIt(); - - // Only the paint tool is allowed from now on (so the user can't switch to a different one). - PeltzerMain.Instance.restrictionManager.SetOnlyAllowedControllerMode(ControllerMode.paintMesh); - // To not distract from the ripple animation, delay the confetti effect until the next step. - iceCreamTutorial.PlaySuccessSound(); - PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); - PeltzerMain.Instance.attentionCaller.StopGlowing(ControllerMode.paintMesh); - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - } - } + public void ResetState() + { + // Nothing to reset. + } - /// - /// Tutorial step where the user is asked to switch to the paint tool. - /// - private class SelectRedColorStep : ITutorialStep { - private int previousMaterial; - private float startRippleTime; - private float rippleDuration = 2.5f; - private ChangeMaterialMenuItem[] allColourSwatches; - - private IceCreamTutorial iceCreamTutorial; - - public SelectRedColorStep(IceCreamTutorial iceCreamTutorial) { - this.iceCreamTutorial = iceCreamTutorial; - } - - public void OnPrepare() { - allColourSwatches = PeltzerMain.Instance.paletteController.transform.GetComponentsInChildren(true); - - // Allow color selection for this step. - PeltzerMain.Instance.restrictionManager.SetOnlyAllowedColor(8); - PeltzerMain.Instance.attentionCaller.GreyOutAllColorSwatches(); - PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.RED_PAINT_SWATCH); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.RED_PAINT_SWATCH); - PeltzerMain.Instance.restrictionManager.changingColorsAllowed = true; - // Disallow controller commands and mesh selection. - PeltzerMain.Instance.restrictionManager.SetAllowedControllerModes(null); - PeltzerMain.Instance.restrictionManager.onlySelectableMeshIdForTutorial = -1; - // Call attention to trigger. - PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - // Make note of the currently selected material to be able to detect when a new - // color is chosen. - previousMaterial = PeltzerMain.Instance.peltzerController.currentMaterial; - - PeltzerMain.Instance.GetPainter().StartFullRipple(); - startRippleTime = Time.time + rippleDuration; - - // Show instructions, and play confetti effect this step. - PeltzerMain.Instance.GetFloatingMessage().Show("Flip the palette to select RED", TextPosition.BOTTOM); - PeltzerMain.Instance.GetFloatingMessage().ShowGIF("CHOOSE_COLOR"); - } - - public bool OnCommand(Command command) { - // No model mutations for now, thank you very much. - return false; - } - - public bool OnValidate() { - if (Time.time > startRippleTime) { - PeltzerMain.Instance.GetPainter().StartFullRipple(); - startRippleTime = Time.time + rippleDuration; + public void OnFinish() + { + // You did it! + PeltzerMain.Instance.peltzerController.YouDidIt(); + + PeltzerMain.Instance.restrictionManager.shapesMenuAllowed = false; + PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed = false; + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_THUMBSTICK); + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TOUCHPAD_LEFT); + PeltzerMain.Instance.attentionCaller.GreyOut( + PeltzerMain.Instance.peltzerController.controllerGeometry.volumeInserterOverlay.GetComponent().leftIcon); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_THUMBSTICK); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TOUCHPAD_LEFT); + // Congratulate the user on this outstanding victory. + PeltzerMain.Instance.GetFloatingMessage().Show("Nice\nThat's the one", TextPosition.CENTER, true); + PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); + tutorial.PlaySuccessSound(); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, + PeltzerMain.Instance.attentionCaller.defaultSirenGlow); + PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); + PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); + } } - // If the user selects a color other than red (or red orange), display an error message. - if (PeltzerMain.Instance.peltzerController.currentMaterial != MaterialRegistry.RED_ID && - PeltzerMain.Instance.peltzerController.currentMaterial != MaterialRegistry.DEEP_ORANGE_ID && - PeltzerMain.Instance.peltzerController.currentMaterial != previousMaterial) { - PeltzerMain.Instance.GetFloatingMessage().Show("That's not RED", TextPosition.BOTTOM); - return false; - } + /// + /// Tutorial step where the user is instructed to place an additional scoop. + /// + private class PlaceScoopStep : ITutorialStep + { + private const float DISTANCE_TOLERANCE = 0.025f; + private IceCreamTutorial tutorial; + private bool spherePlaced; + private bool completed = false; + + public PlaceScoopStep(IceCreamTutorial tutorial) + { + this.tutorial = tutorial; + } - // This step is done when the user has selected the red colour. - return PeltzerMain.Instance.peltzerController.currentMaterial == MaterialRegistry.RED_ID - || PeltzerMain.Instance.peltzerController.currentMaterial == MaterialRegistry.DEEP_ORANGE_ID; - } - - public void ResetState() { - // Nothing to reset. - } - - public void OnFinish() { - // You did it! - PeltzerMain.Instance.peltzerController.YouDidIt(); - - // Only the paint tool is allowed from now on (so the user can't switch to a different one). - PeltzerMain.Instance.restrictionManager.SetOnlyAllowedControllerMode(ControllerMode.paintMesh); - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.restrictionManager.changingColorsAllowed = false; - PeltzerMain.Instance.restrictionManager.SetOnlyAllowedColor(-2); - PeltzerMain.Instance.attentionCaller.GreyOutAllColorSwatches(); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.RED_PAINT_SWATCH); - PeltzerMain.Instance.GetFloatingMessage().Show("Nice", TextPosition.CENTER, true); - PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); - iceCreamTutorial.PlaySuccessSound(); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, - PeltzerMain.Instance.attentionCaller.defaultSirenGlow); - PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); - PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); - } - } + public void OnPrepare() + { + // Allow volume insertion so the user can place the body. + PeltzerMain.Instance.restrictionManager.volumeInsertionAllowed = true; + // Show the body placeholder to guide the user. + PeltzerMain.Instance.model.UnhideMeshForTestOrTutorial(SCOOP_PLACEHOLDER_MESH_ID); + // Recolor the volumeInserter toolhead since the user is going to use it to place the sphere. + PeltzerMain.Instance.attentionCaller.Recolor(ControllerMode.insertVolume); + // Call attention to trigger. + PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + // Show instructions. + PeltzerMain.Instance.GetFloatingMessage().Show("Pile it on top", TextPosition.FULL_SIDE); + PeltzerMain.Instance.GetFloatingMessage().ShowGIF("INSERT_SPHERE"); + } - /// - /// Tutorial step where the user is instructed to paint the newly-inserted cherry. - /// - private class PaintCherryStep : ITutorialStep { - private IceCreamTutorial tutorial; - private bool cherryPainted; - - public PaintCherryStep(IceCreamTutorial tutorial) { - this.tutorial = tutorial; - } - - public void OnPrepare() { - // Show instructions. - PeltzerMain.Instance.GetFloatingMessage().Show("Paint the cherry RED", TextPosition.HALF_SIDE); - PeltzerMain.Instance.GetFloatingMessage().ShowGIF("PAINT_COLOR"); - PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StartMeshGlowing(tutorial.newCherryMeshId); - PeltzerMain.Instance.restrictionManager.onlySelectableMeshIdForTutorial = tutorial.newCherryMeshId; - } - - public bool OnCommand(Command command) { - if (cherryPainted) { - return false; - } - ChangeFacePropertiesCommand paintCommand = null; - - if (command is ChangeFacePropertiesCommand) { - // If there is a single command, check it paints the cherry. - paintCommand = (ChangeFacePropertiesCommand)command; - if (paintCommand.GetMeshId() != tutorial.newCherryMeshId) { - return false; - } - } else if (command is CompositeCommand) { - // Else if there is a list of commands, check that at least one of them paints the cherry. - List compositeCommandEntries = ((CompositeCommand)command).GetCommands(); - foreach (Command compositeCommandEntry in compositeCommandEntries) { - if (compositeCommandEntry is ChangeFacePropertiesCommand) { - paintCommand = (ChangeFacePropertiesCommand)compositeCommandEntry; - if (paintCommand.GetMeshId() != tutorial.newCherryMeshId) { - paintCommand = null; - continue; - } - } - } - if (paintCommand == null) { - return false; - } - } + public bool OnCommand(Command command) + { + // Hack to allow us to delete the wireframe. + if (completed && command is DeleteMeshCommand) + { + return true; + } + + if (spherePlaced || !(command is AddMeshCommand)) + { + // Only adding meshes is allowed, and only while the body has not yet been placed. + return false; + } + // Check if the mesh the user wants to add is at the correct position. + float dist = Vector3.Distance(((AddMeshCommand)command).GetMeshClone().offset, + tutorial.scoopPlaceHolder.offset); + if (dist > DISTANCE_TOLERANCE) + { + PeltzerMain.Instance.GetFloatingMessage() + .Show("Place it in the right spot", TextPosition.FULL_SIDE); + return false; + } + spherePlaced = true; + PeltzerMain.Instance.GetFloatingMessage().Show("That's better", TextPosition.FULL_SIDE, true); + return true; + } - cherryPainted = true; - return true; - } - - public bool OnValidate() { - // Make sure the new cherry is always glowing to call attention to it. - PeltzerMain.Instance.attentionCaller.MakeSureMeshIsGlowing(tutorial.newCherryMeshId); - return cherryPainted; - } - - public void ResetState() { - cherryPainted = false; - } - - public void OnFinish() { - // You did it! - PeltzerMain.Instance.peltzerController.YouDidIt(); - - // Hide the placeholder. - PeltzerMain.Instance.attentionCaller.GreyOut(ControllerMode.paintMesh); - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StopMeshGlowing(tutorial.newCherryMeshId); - PeltzerMain.Instance.GetFloatingMessage().ShowHeader(""); - PeltzerMain.Instance.GetFloatingMessage() - .Show("Your friend wants one\nLet's make a copy", TextPosition.CENTER_NO_TITLE, true); - PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); - tutorial.PlaySuccessSound(); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, - PeltzerMain.Instance.attentionCaller.defaultSirenGlow); - PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); - PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); - - // Reset state variables. - ResetState(); - } - } + public bool OnValidate() + { + // This step is done when the user has placed the body at the right place. + return spherePlaced; + } - /// - /// Tutorial step where the user is asked to switch to the grab tool. - /// - private class SwitchToGrabToolStep : ITutorialStep { - private IceCreamTutorial iceCreamTutorial; - private float nextBuzzTime; - - public SwitchToGrabToolStep(IceCreamTutorial iceCreamTutorial) { - this.iceCreamTutorial = iceCreamTutorial; - } - - public void OnPrepare() { - // Only allow paint (current tool) and the move tool. - PeltzerMain.Instance.restrictionManager.SetAllowedControllerModes( - new List() { ControllerMode.paintMesh, ControllerMode.move }); - // Call attention to the trigger. - PeltzerMain.Instance.attentionCaller.Recolor(ControllerMode.move); - PeltzerMain.Instance.attentionCaller.StartGlowing(ControllerMode.move); - PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - // Show instructions. - PeltzerMain.Instance.GetFloatingMessage().ShowHeader("Copying"); - PeltzerMain.Instance.GetFloatingMessage().Show("Choose the grab tool", TextPosition.BOTTOM); - PeltzerMain.Instance.GetFloatingMessage().ShowGIF("CHOOSE_GRAB"); - - // Look at me. - PeltzerMain.Instance.paletteController.LookAtMe(); - } - - public bool OnCommand(Command command) { - // No model mutations for now, thank you very much. - return false; - } - - public bool OnValidate() { - if (Time.time > nextBuzzTime) { - PeltzerMain.Instance.paletteController.LookAtMe(); - nextBuzzTime = Time.time + BUZZ_INTERVAL_DURATION; + public void ResetState() + { + spherePlaced = false; + } + + public void OnFinish() + { + // You did it! + PeltzerMain.Instance.peltzerController.YouDidIt(); + + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.restrictionManager.volumeInsertionAllowed = false; + // Stop showing the placeholder mesh. + PeltzerMain.Instance.model.HideMeshForTestOrTutorial(SCOOP_PLACEHOLDER_MESH_ID); + completed = true; + PeltzerMain.Instance.model.ApplyCommand(new DeleteMeshCommand(SCOOP_PLACEHOLDER_MESH_ID)); + PeltzerMain.Instance.GetFloatingMessage().Show("Now for the cherry\non top", TextPosition.CENTER, true); + PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); + tutorial.PlaySuccessSound(); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, + PeltzerMain.Instance.attentionCaller.defaultSirenGlow); + PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); + PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); + + // Reset state variables. + ResetState(); + } } - // This step is done when the user has selected the move tool. - return PeltzerMain.Instance.peltzerController.mode == ControllerMode.move; - } - - public void ResetState() { - // Nothing to reset. - } - - public void OnFinish() { - // You did it! - PeltzerMain.Instance.paletteController.YouDidIt(); - - // Only the move tool is allowed from now on (so the user can't switch to a different one). - PeltzerMain.Instance.restrictionManager.SetOnlyAllowedControllerMode(ControllerMode.move); - PeltzerMain.Instance.GetFloatingMessage().Show("First select everything", TextPosition.CENTER, true); - PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); - iceCreamTutorial.PlaySuccessSound(); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, - PeltzerMain.Instance.attentionCaller.defaultSirenGlow); - PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); - PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); - PeltzerMain.Instance.attentionCaller.StopGlowing(ControllerMode.move); - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - } - } + /// + /// Tutorial step where the user is instructed to scale down the sphere for a cherry. + /// + private class ScaleSphereStep : ITutorialStep + { + private bool encouragementShown; + private float nextBuzzTime; - /// - /// Tutorial step where the user is instructed to multi-select the everything (the entire cone). - /// - private class MultiSelectEverythingStep : ITutorialStep { - private static string DEFAULT_INSTRUCTION = - "Click then drag through shapes"; - bool triggerDown; - int numMeshes; - bool userTriedAndFailed; - - private IceCreamTutorial iceCreamTutorial; - - public MultiSelectEverythingStep(IceCreamTutorial iceCreamTutorial) { - this.iceCreamTutorial = iceCreamTutorial; - } - - public void OnPrepare() { - PeltzerMain main = PeltzerMain.Instance; - numMeshes = main.model.GetNumberOfMeshes(); - // Call attention to the trigger. - main.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); - main.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - - // Show instructions. - PeltzerMain.Instance.GetFloatingMessage().Show(DEFAULT_INSTRUCTION, TextPosition.HALF_SIDE); - PeltzerMain.Instance.GetFloatingMessage().ShowGIF("MULTISELECT"); - PeltzerMain.Instance.restrictionManager.onlySelectableMeshIdForTutorial = null; - PeltzerMain.Instance.controllerMain.ControllerActionHandler += OnControllerEvent; - triggerDown = false; - } - - private void OnControllerEvent(object sender, ControllerEventArgs args) { - if (args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN) { - triggerDown = true; - } + private IceCreamTutorial iceCreamTutorial; - if (args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.UP) { - triggerDown = false; - } - } - - public bool OnCommand(Command command) { - // No commands are allowed in this step (no model mutations). - return false; - } - - public bool OnValidate() { - Selector selector = PeltzerMain.Instance.GetSelector(); - List selectedMeshIds = new List(selector.selectedMeshes); - - if (selector.isMultiSelecting) { - if (selectedMeshIds.Count == numMeshes) { - // Success! Stop them from being able to deselect. - PeltzerMain.Instance.restrictionManager.deselectAllowed = false; - return true; - } else if (selectedMeshIds.Count > 0) { - // They didn't get everything yet but are still multi-selecting. - } - } else if (selectedMeshIds.Count > 0){ - // They've stopped multi-selecting. If the user doesn't have all the ice cream cone clear the selection. - selector.ClearState(); - // Play error sound. - // Play error haptics. - PeltzerMain.Instance.GetFloatingMessage().Show("Missed some\nTry again", TextPosition.HALF_SIDE); - userTriedAndFailed = true; - } + public ScaleSphereStep(IceCreamTutorial iceCreamTutorial) + { + this.iceCreamTutorial = iceCreamTutorial; + } - if (triggerDown) { - PeltzerMain.Instance.GetFloatingMessage().Show("Wave through everything", TextPosition.HALF_SIDE); - userTriedAndFailed = false; - } else if (!userTriedAndFailed){ - PeltzerMain.Instance.GetFloatingMessage().Show(DEFAULT_INSTRUCTION, TextPosition.HALF_SIDE); - } + public void OnPrepare() + { + PeltzerMain main = PeltzerMain.Instance; + // Allow primitive scaling, but nothing else. + main.restrictionManager.scaleOnVolumeInsertionAllowed = true; + // Show instructions. + PeltzerMain.Instance.GetFloatingMessage().Show("That's too big\nSize it down", TextPosition.FULL_SIDE); + PeltzerMain.Instance.GetFloatingMessage().ShowGIF("SMALLER_SPHERE"); + // Allow the touchpad's down button. + main.restrictionManager.touchpadDownAllowed = true; + main.restrictionManager.touchpadUpAllowed = false; + // Call attention to the touchpad's up button. + PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_THUMBSTICK); + main.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TOUCHPAD_DOWN); + PeltzerMain.Instance.attentionCaller.Recolor( + PeltzerMain.Instance.peltzerController.controllerGeometry.volumeInserterOverlay.GetComponent().downIcon); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_THUMBSTICK); + main.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TOUCHPAD_DOWN); + + // Look at me. + main.peltzerController.LookAtMe(); + } - return false; - } - - public void ResetState() { - triggerDown = false; - userTriedAndFailed = false; - } - - public void OnFinish() { - // You did it! - PeltzerMain.Instance.peltzerController.YouDidIt(); - - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - // Disallow undo/redo. - PeltzerMain.Instance.restrictionManager.undoRedoAllowed = false; - // Congratulate the user on this outstanding victory. - PeltzerMain.Instance.GetFloatingMessage().Show("Now that everything is selected let's copy it", TextPosition.CENTER, true); - PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); - iceCreamTutorial.PlaySuccessSound(); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, - PeltzerMain.Instance.attentionCaller.defaultSirenGlow); - PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); - PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); - - // Reset state variables. - ResetState(); - } - } + public bool OnCommand(Command command) + { + // No commands are allowed in this step (no model mutations). + return false; + } - // TODO could add a 'group' step here, then ungroup to remove the cherry. + public bool OnValidate() + { + if (PeltzerMain.Instance.GetVolumeInserter().scaleDelta == SCOOP_SCALE_DELTA + && Time.time > nextBuzzTime) + { + PeltzerMain.Instance.peltzerController.LookAtMe(); + nextBuzzTime = Time.time + BUZZ_INTERVAL_DURATION; + } + + if (!encouragementShown && PeltzerMain.Instance.GetVolumeInserter().scaleDelta < SCOOP_SCALE_DELTA) + { + PeltzerMain.Instance.GetFloatingMessage().Show("Keep on going", TextPosition.FULL_SIDE); + encouragementShown = true; + } + // TODO add : You're almost there! + + // This step is done when the user has the right size selected. + // We allow <= because if the user overshoots it with the continuous press, we don't care. + // (the tutorial will still work). + return PeltzerMain.Instance.GetVolumeInserter().scaleDelta <= CHERRY_SCALE_DELTA; + } - /// - /// Tutorial step where the user is instructed to copy the ice cream. They can place the new cone anywhere. - /// - private class CopyIceCreamStep : ITutorialStep { - private IceCreamTutorial tutorial; - private bool copyHappened; - private bool waitingForTriggerDown = false; - private bool triggerHasBeenPressed = false; - private float nextBuzzTime; - - public CopyIceCreamStep(IceCreamTutorial tutorial) { - this.tutorial = tutorial; - } - - private void OnControllerEvent(object sender, ControllerEventArgs args) { - if (waitingForTriggerDown - && args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN) { - triggerHasBeenPressed = true; - } - } - - public void OnPrepare() { - // Show instructions. - PeltzerMain.Instance.GetFloatingMessage().Show("Copy the cone", TextPosition.FULL_SIDE); - PeltzerMain.Instance.GetFloatingMessage().ShowGIF("COPY"); - - PeltzerMain.Instance.restrictionManager.copyingAllowed = true; - - // Allow the touchpad's left button. - PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed = true; - PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_THUMBSTICK); - PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TOUCHPAD_LEFT); - PeltzerMain.Instance.attentionCaller.Recolor( - PeltzerMain.Instance.peltzerController.controllerGeometry.moveOverlay.GetComponent().leftIcon); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_THUMBSTICK); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TOUCHPAD_LEFT); - PeltzerMain.Instance.controllerMain.ControllerActionHandler += OnControllerEvent; - - // Look at me. - PeltzerMain.Instance.peltzerController.LookAtMe(); - } - - public bool OnCommand(Command command) { - if (copyHappened) { - return false; - } + public void ResetState() + { + encouragementShown = false; + } - // We expect a CompositeCommand. We require that it contains at least a copy of the cherry as the minimal - // requirement to be able to continue the tutorial (rather than requiring everything, and risking some edge - // case blocking the tutorial. - if (!(command is CompositeCommand)) { - return false; - } - CompositeCommand compositeCommand = (CompositeCommand)command; - foreach (Command compositeCommandEntry in compositeCommand.GetCommands()) { - if (compositeCommandEntry is CopyMeshCommand) { - PeltzerMain.Instance.restrictionManager.deselectAllowed = true; - CopyMeshCommand copyMeshCommand = (CopyMeshCommand)compositeCommandEntry; - if (copyMeshCommand.copiedFromId == tutorial.newCherryMeshId) { - tutorial.clonedCherryMeshId = copyMeshCommand.GetCopyMeshId(); - copyHappened = true; - return true; - } - } + public void OnFinish() + { + // You did it! + PeltzerMain.Instance.peltzerController.YouDidIt(); + + PeltzerMain.Instance.restrictionManager.scaleOnVolumeInsertionAllowed = false; + PeltzerMain.Instance.restrictionManager.touchpadDownAllowed = false; + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_THUMBSTICK); + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TOUCHPAD_DOWN); + PeltzerMain.Instance.attentionCaller.GreyOut( + PeltzerMain.Instance.peltzerController.controllerGeometry.volumeInserterOverlay.GetComponent().downIcon); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_THUMBSTICK); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TOUCHPAD_DOWN); + PeltzerMain.Instance.GetFloatingMessage().Show("Perfect", TextPosition.CENTER, true); + PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); + iceCreamTutorial.PlaySuccessSound(); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, + PeltzerMain.Instance.attentionCaller.defaultSirenGlow); + PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); + PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); + + // Reset state variables. + ResetState(); + } } - return false; - } - public bool OnValidate() { - bool copyButtonPressed = PeltzerMain.Instance.GetMover().currentMoveType == Mover.MoveType.CLONE; + /// + /// Tutorial step where the user is instructed to place an cherry. + /// + private class PlaceCherryStep : ITutorialStep + { + private const float DISTANCE_TOLERANCE = 0.03f; + private IceCreamTutorial tutorial; + private bool cherryPlaced; + + public PlaceCherryStep(IceCreamTutorial tutorial) + { + this.tutorial = tutorial; + } + + public void OnPrepare() + { + // Allow volume insertion so the user can place the body. + PeltzerMain.Instance.restrictionManager.volumeInsertionAllowed = true; + // Show the body placeholder to guide the user. + PeltzerMain.Instance.model.UnhideMeshForTestOrTutorial(CHERRY_PLACEHOLDER_MESH_ID); + + // Call attention to trigger. + PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + // Show instructions. + PeltzerMain.Instance.GetFloatingMessage().Show("Place the cherry on top", TextPosition.FULL_SIDE); + PeltzerMain.Instance.GetFloatingMessage().ShowGIF("INSERT_ANOTHER_SPHERE"); + } + + public bool OnCommand(Command command) + { + // Hack to allow deleting the wireframe. + if (cherryPlaced && command is DeleteMeshCommand) + { + return true; + } + + if (cherryPlaced || !(command is AddMeshCommand)) + { + // Only adding meshes is allowed, and only while the body has not yet been placed. + return false; + } + // Check if the mesh the user wants to add is at the correct position. + float dist = Vector3.Distance(((AddMeshCommand)command).GetMeshClone().offset, + tutorial.cherryPlaceHolder.offset); + if (dist > DISTANCE_TOLERANCE) + { + PeltzerMain.Instance.GetFloatingMessage().Show("Try again", TextPosition.FULL_SIDE); + return false; + } + cherryPlaced = true; + PeltzerMain.Instance.GetFloatingMessage().Show("", TextPosition.CENTER, true); + PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); + tutorial.newCherryMeshId = ((AddMeshCommand)command).GetMeshId(); + return true; + } - if (!copyButtonPressed && Time.time > nextBuzzTime) { - PeltzerMain.Instance.peltzerController.LookAtMe(); - nextBuzzTime = Time.time + BUZZ_INTERVAL_DURATION; + public bool OnValidate() + { + // This step is done when the user has placed the cherry at the right place. + return cherryPlaced; + } + + public void ResetState() + { + cherryPlaced = false; + } + + public void OnFinish() + { + // You did it! + PeltzerMain.Instance.peltzerController.YouDidIt(); + + PeltzerMain.Instance.attentionCaller.GreyOut(ControllerMode.insertVolume); + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.restrictionManager.volumeInsertionAllowed = false; + // Stop showing the placeholder mesh. + PeltzerMain.Instance.model.HideMeshForTestOrTutorial(CHERRY_PLACEHOLDER_MESH_ID); + PeltzerMain.Instance.model.ApplyCommand(new DeleteMeshCommand(CHERRY_PLACEHOLDER_MESH_ID)); + PeltzerMain.Instance.GetFloatingMessage().ShowHeader(""); + PeltzerMain.Instance.GetFloatingMessage() + .Show("Let's paint it RED", TextPosition.CENTER_NO_TITLE); + PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); + tutorial.PlaySuccessSound(); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, + PeltzerMain.Instance.attentionCaller.defaultSirenGlow); + PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); + PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); + + // Reset state variables. + ResetState(); + } } - if (copyButtonPressed && !waitingForTriggerDown) { - // Allow the touchpad's left button. - PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed = false; - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_THUMBSTICK); - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TOUCHPAD_LEFT); - PeltzerMain.Instance.attentionCaller.GreyOut( - PeltzerMain.Instance.peltzerController.controllerGeometry.moveOverlay.GetComponent().leftIcon); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_THUMBSTICK); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TOUCHPAD_LEFT); - - PeltzerMain.Instance.GetFloatingMessage().Show("Place it next to the original", TextPosition.FULL_SIDE); - PeltzerMain.Instance.GetFloatingMessage().ShowGIF("INSERT_SPHERE"); - // Call attention to trigger. - PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - waitingForTriggerDown = true; + /// + /// Tutorial step where the user is asked to switch to the paint tool. + /// + private class SwitchToPaintToolStep : ITutorialStep + { + private IceCreamTutorial iceCreamTutorial; + private float nextBuzzTime; + + public SwitchToPaintToolStep(IceCreamTutorial iceCreamTutorial) + { + this.iceCreamTutorial = iceCreamTutorial; + } + + public void OnPrepare() + { + // Only allow volume insertion (current tool) and the paint tool. + PeltzerMain.Instance.restrictionManager.SetAllowedControllerModes( + new List() { ControllerMode.insertVolume, ControllerMode.paintMesh }); + // Call attention to trigger. + PeltzerMain.Instance.attentionCaller.Recolor(ControllerMode.paintMesh); + PeltzerMain.Instance.attentionCaller.StartGlowing(ControllerMode.paintMesh); + PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + // Show instructions. + PeltzerMain.Instance.GetFloatingMessage().ShowHeader("Painting"); + PeltzerMain.Instance.GetFloatingMessage().Show("Pick up the paint brush", TextPosition.BOTTOM); + PeltzerMain.Instance.GetFloatingMessage().ShowGIF("CHOOSE_PAINTBRUSH"); + // Set all the colors to be allowed so that they aren't greyed out. However changingColors is still disabled + // so the user can't select them. We just want them to be colorful for the ripple animation. + PeltzerMain.Instance.attentionCaller.RecolorAllColorSwatches(); + + // Look at me. + PeltzerMain.Instance.paletteController.LookAtMe(); + } + + public bool OnCommand(Command command) + { + // No model mutations for now, thank you very much. + return false; + } + + public bool OnValidate() + { + if (Time.time > nextBuzzTime) + { + PeltzerMain.Instance.paletteController.LookAtMe(); + nextBuzzTime = Time.time + BUZZ_INTERVAL_DURATION; + } + + // This step is done when the user has selected the paint tool. + return PeltzerMain.Instance.peltzerController.mode == ControllerMode.paintMesh; + } + + public void ResetState() + { + // Nothing to reset. + } + + public void OnFinish() + { + // You did it! + PeltzerMain.Instance.peltzerController.YouDidIt(); + + // Only the paint tool is allowed from now on (so the user can't switch to a different one). + PeltzerMain.Instance.restrictionManager.SetOnlyAllowedControllerMode(ControllerMode.paintMesh); + // To not distract from the ripple animation, delay the confetti effect until the next step. + iceCreamTutorial.PlaySuccessSound(); + PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); + PeltzerMain.Instance.attentionCaller.StopGlowing(ControllerMode.paintMesh); + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + } } - return copyHappened && (waitingForTriggerDown && triggerHasBeenPressed); - } - - public void ResetState() { - copyHappened = false; - waitingForTriggerDown = false; - triggerHasBeenPressed = false; - } - - public void OnFinish() { - // You did it! - PeltzerMain.Instance.peltzerController.YouDidIt(); - PeltzerMain.Instance.restrictionManager.copyingAllowed = false; - PeltzerMain.Instance.restrictionManager.movingMeshesAllowed = false; - PeltzerMain.Instance.GetFloatingMessage().ShowHeader(""); - PeltzerMain.Instance.GetFloatingMessage().Show("Your friend doesn't want a cherry", TextPosition.CENTER_NO_TITLE, true); - PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); - tutorial.PlaySuccessSound(); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, - PeltzerMain.Instance.attentionCaller.defaultSirenGlow); - PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); - PeltzerMain.Instance.attentionCaller.GreyOut(ControllerMode.move); - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - - // Stop the user from selecting anything now. - PeltzerMain.Instance.restrictionManager.onlySelectableMeshIdForTutorial = -1; - PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); - - // Reset state variables. - ResetState(); - } - } + /// + /// Tutorial step where the user is asked to switch to the paint tool. + /// + private class SelectRedColorStep : ITutorialStep + { + private int previousMaterial; + private float startRippleTime; + private float rippleDuration = 2.5f; + private ChangeMaterialMenuItem[] allColourSwatches; + + private IceCreamTutorial iceCreamTutorial; + + public SelectRedColorStep(IceCreamTutorial iceCreamTutorial) + { + this.iceCreamTutorial = iceCreamTutorial; + } - /// - /// Tutorial step where the user is asked to switch to the delete tool. - /// - private class SwitchToDeleteToolStep : ITutorialStep { - private IceCreamTutorial iceCreamTutorial; - private float nextBuzzTime; - - public SwitchToDeleteToolStep(IceCreamTutorial iceCreamTutorial) { - this.iceCreamTutorial = iceCreamTutorial; - } - - public void OnPrepare() { - // Only allow move (current tool) and the delete tool. - PeltzerMain.Instance.restrictionManager.SetAllowedControllerModes( - new List() { ControllerMode.move, ControllerMode.delete }); - PeltzerMain.Instance.attentionCaller.Recolor(ControllerMode.delete); - PeltzerMain.Instance.attentionCaller.StartGlowing(ControllerMode.delete); - // Call attention to trigger. - PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - // Show instructions. - PeltzerMain.Instance.GetFloatingMessage().ShowHeader("Erasing"); - PeltzerMain.Instance.GetFloatingMessage().Show("Grab the eraser", TextPosition.BOTTOM); - PeltzerMain.Instance.GetFloatingMessage().ShowGIF("CHOOSE_ERASER"); - - // Look at me. - PeltzerMain.Instance.paletteController.LookAtMe(); - } - - public bool OnCommand(Command command) { - // No model mutations for now, thank you very much. - return false; - } - - public bool OnValidate() { - if (Time.time > nextBuzzTime) { - PeltzerMain.Instance.paletteController.LookAtMe(); - nextBuzzTime = Time.time + BUZZ_INTERVAL_DURATION; + public void OnPrepare() + { + allColourSwatches = PeltzerMain.Instance.paletteController.transform.GetComponentsInChildren(true); + + // Allow color selection for this step. + PeltzerMain.Instance.restrictionManager.SetOnlyAllowedColor(8); + PeltzerMain.Instance.attentionCaller.GreyOutAllColorSwatches(); + PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.RED_PAINT_SWATCH); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.RED_PAINT_SWATCH); + PeltzerMain.Instance.restrictionManager.changingColorsAllowed = true; + // Disallow controller commands and mesh selection. + PeltzerMain.Instance.restrictionManager.SetAllowedControllerModes(null); + PeltzerMain.Instance.restrictionManager.onlySelectableMeshIdForTutorial = -1; + // Call attention to trigger. + PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + // Make note of the currently selected material to be able to detect when a new + // color is chosen. + previousMaterial = PeltzerMain.Instance.peltzerController.currentMaterial; + + PeltzerMain.Instance.GetPainter().StartFullRipple(); + startRippleTime = Time.time + rippleDuration; + + // Show instructions, and play confetti effect this step. + PeltzerMain.Instance.GetFloatingMessage().Show("Flip the palette to select RED", TextPosition.BOTTOM); + PeltzerMain.Instance.GetFloatingMessage().ShowGIF("CHOOSE_COLOR"); + } + + public bool OnCommand(Command command) + { + // No model mutations for now, thank you very much. + return false; + } + + public bool OnValidate() + { + if (Time.time > startRippleTime) + { + PeltzerMain.Instance.GetPainter().StartFullRipple(); + startRippleTime = Time.time + rippleDuration; + } + + // If the user selects a color other than red (or red orange), display an error message. + if (PeltzerMain.Instance.peltzerController.currentMaterial != MaterialRegistry.RED_ID && + PeltzerMain.Instance.peltzerController.currentMaterial != MaterialRegistry.DEEP_ORANGE_ID && + PeltzerMain.Instance.peltzerController.currentMaterial != previousMaterial) + { + PeltzerMain.Instance.GetFloatingMessage().Show("That's not RED", TextPosition.BOTTOM); + return false; + } + + // This step is done when the user has selected the red colour. + return PeltzerMain.Instance.peltzerController.currentMaterial == MaterialRegistry.RED_ID + || PeltzerMain.Instance.peltzerController.currentMaterial == MaterialRegistry.DEEP_ORANGE_ID; + } + + public void ResetState() + { + // Nothing to reset. + } + + public void OnFinish() + { + // You did it! + PeltzerMain.Instance.peltzerController.YouDidIt(); + + // Only the paint tool is allowed from now on (so the user can't switch to a different one). + PeltzerMain.Instance.restrictionManager.SetOnlyAllowedControllerMode(ControllerMode.paintMesh); + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.restrictionManager.changingColorsAllowed = false; + PeltzerMain.Instance.restrictionManager.SetOnlyAllowedColor(-2); + PeltzerMain.Instance.attentionCaller.GreyOutAllColorSwatches(); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.RED_PAINT_SWATCH); + PeltzerMain.Instance.GetFloatingMessage().Show("Nice", TextPosition.CENTER, true); + PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); + iceCreamTutorial.PlaySuccessSound(); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, + PeltzerMain.Instance.attentionCaller.defaultSirenGlow); + PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); + PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); + } } - // This step is done when the user has selected the delete tool. - return PeltzerMain.Instance.peltzerController.mode == ControllerMode.delete; - } - - public void ResetState() { - // Nothing to reset. - } - - public void OnFinish() { - // You did it! - PeltzerMain.Instance.paletteController.YouDidIt(); - - // Only the delete tool is allowed from now on (so the user can't switch to a different one). - PeltzerMain.Instance.restrictionManager.SetOnlyAllowedControllerMode(ControllerMode.delete); - PeltzerMain.Instance.attentionCaller.StopGlowing(ControllerMode.delete); - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.GetFloatingMessage().Show("Let's get rid of it", TextPosition.CENTER, true); - PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); - iceCreamTutorial.PlaySuccessSound(); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, - PeltzerMain.Instance.attentionCaller.defaultSirenGlow); - PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); - PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); - } - } + /// + /// Tutorial step where the user is instructed to paint the newly-inserted cherry. + /// + private class PaintCherryStep : ITutorialStep + { + private IceCreamTutorial tutorial; + private bool cherryPainted; + + public PaintCherryStep(IceCreamTutorial tutorial) + { + this.tutorial = tutorial; + } - /// - /// Tutorial step where the user is instructed to delete the second cherry. - /// - private class DeleteSecondCherryStep : ITutorialStep { - private IceCreamTutorial tutorial; - private bool deletedCherry; - - public DeleteSecondCherryStep(IceCreamTutorial tutorial) { - this.tutorial = tutorial; - } - - public void OnPrepare() { - // Show instructions. - PeltzerMain.Instance.GetFloatingMessage().Show("Erase the\nnew cherry", TextPosition.HALF_SIDE); - PeltzerMain.Instance.GetFloatingMessage().ShowGIF("ERASE"); - PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StartMeshGlowing(tutorial.clonedCherryMeshId); - PeltzerMain.Instance.restrictionManager.onlySelectableMeshIdForTutorial = tutorial.clonedCherryMeshId; - } - - // Only allow a DeleteMesh command on the cherry, which is sufficient but not necessary for this - // step. - // We want the tutorial to complete at the point the user sees the cherry disappear, which will - // be before the command is sent, as the delete tool waits until the trigger is released to issue commands. - public bool OnCommand(Command command) { - if (deletedCherry) { - return false; + public void OnPrepare() + { + // Show instructions. + PeltzerMain.Instance.GetFloatingMessage().Show("Paint the cherry RED", TextPosition.HALF_SIDE); + PeltzerMain.Instance.GetFloatingMessage().ShowGIF("PAINT_COLOR"); + PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StartMeshGlowing(tutorial.newCherryMeshId); + PeltzerMain.Instance.restrictionManager.onlySelectableMeshIdForTutorial = tutorial.newCherryMeshId; + } + + public bool OnCommand(Command command) + { + if (cherryPainted) + { + return false; + } + ChangeFacePropertiesCommand paintCommand = null; + + if (command is ChangeFacePropertiesCommand) + { + // If there is a single command, check it paints the cherry. + paintCommand = (ChangeFacePropertiesCommand)command; + if (paintCommand.GetMeshId() != tutorial.newCherryMeshId) + { + return false; + } + } + else if (command is CompositeCommand) + { + // Else if there is a list of commands, check that at least one of them paints the cherry. + List compositeCommandEntries = ((CompositeCommand)command).GetCommands(); + foreach (Command compositeCommandEntry in compositeCommandEntries) + { + if (compositeCommandEntry is ChangeFacePropertiesCommand) + { + paintCommand = (ChangeFacePropertiesCommand)compositeCommandEntry; + if (paintCommand.GetMeshId() != tutorial.newCherryMeshId) + { + paintCommand = null; + continue; + } + } + } + if (paintCommand == null) + { + return false; + } + } + + cherryPainted = true; + return true; + } + + public bool OnValidate() + { + // Make sure the new cherry is always glowing to call attention to it. + PeltzerMain.Instance.attentionCaller.MakeSureMeshIsGlowing(tutorial.newCherryMeshId); + return cherryPainted; + } + + public void ResetState() + { + cherryPainted = false; + } + + public void OnFinish() + { + // You did it! + PeltzerMain.Instance.peltzerController.YouDidIt(); + + // Hide the placeholder. + PeltzerMain.Instance.attentionCaller.GreyOut(ControllerMode.paintMesh); + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StopMeshGlowing(tutorial.newCherryMeshId); + PeltzerMain.Instance.GetFloatingMessage().ShowHeader(""); + PeltzerMain.Instance.GetFloatingMessage() + .Show("Your friend wants one\nLet's make a copy", TextPosition.CENTER_NO_TITLE, true); + PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); + tutorial.PlaySuccessSound(); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, + PeltzerMain.Instance.attentionCaller.defaultSirenGlow); + PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); + PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); + + // Reset state variables. + ResetState(); + } } - // We expect a CompositeCommand. We require that it contains at least a copy of the cherry as the minimal - // requirement to be able to continue the tutorial (rather than requiring everything, and risking some edge - // case blocking the tutorial. - if (!(command is CompositeCommand)) { - return false; + /// + /// Tutorial step where the user is asked to switch to the grab tool. + /// + private class SwitchToGrabToolStep : ITutorialStep + { + private IceCreamTutorial iceCreamTutorial; + private float nextBuzzTime; + + public SwitchToGrabToolStep(IceCreamTutorial iceCreamTutorial) + { + this.iceCreamTutorial = iceCreamTutorial; + } + + public void OnPrepare() + { + // Only allow paint (current tool) and the move tool. + PeltzerMain.Instance.restrictionManager.SetAllowedControllerModes( + new List() { ControllerMode.paintMesh, ControllerMode.move }); + // Call attention to the trigger. + PeltzerMain.Instance.attentionCaller.Recolor(ControllerMode.move); + PeltzerMain.Instance.attentionCaller.StartGlowing(ControllerMode.move); + PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + // Show instructions. + PeltzerMain.Instance.GetFloatingMessage().ShowHeader("Copying"); + PeltzerMain.Instance.GetFloatingMessage().Show("Choose the grab tool", TextPosition.BOTTOM); + PeltzerMain.Instance.GetFloatingMessage().ShowGIF("CHOOSE_GRAB"); + + // Look at me. + PeltzerMain.Instance.paletteController.LookAtMe(); + } + + public bool OnCommand(Command command) + { + // No model mutations for now, thank you very much. + return false; + } + + public bool OnValidate() + { + if (Time.time > nextBuzzTime) + { + PeltzerMain.Instance.paletteController.LookAtMe(); + nextBuzzTime = Time.time + BUZZ_INTERVAL_DURATION; + } + + // This step is done when the user has selected the move tool. + return PeltzerMain.Instance.peltzerController.mode == ControllerMode.move; + } + + public void ResetState() + { + // Nothing to reset. + } + + public void OnFinish() + { + // You did it! + PeltzerMain.Instance.paletteController.YouDidIt(); + + // Only the move tool is allowed from now on (so the user can't switch to a different one). + PeltzerMain.Instance.restrictionManager.SetOnlyAllowedControllerMode(ControllerMode.move); + PeltzerMain.Instance.GetFloatingMessage().Show("First select everything", TextPosition.CENTER, true); + PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); + iceCreamTutorial.PlaySuccessSound(); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, + PeltzerMain.Instance.attentionCaller.defaultSirenGlow); + PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); + PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); + PeltzerMain.Instance.attentionCaller.StopGlowing(ControllerMode.move); + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + } } - CompositeCommand compositeCommand = (CompositeCommand)command; - foreach (Command compositeCommandEntry in compositeCommand.GetCommands()) { - if (compositeCommandEntry is DeleteMeshCommand) { - DeleteMeshCommand deleteMeshCommand = (DeleteMeshCommand)compositeCommandEntry; - if (deleteMeshCommand.MeshId == tutorial.clonedCherryMeshId) { - deletedCherry = true; - return true; - } - } + + /// + /// Tutorial step where the user is instructed to multi-select the everything (the entire cone). + /// + private class MultiSelectEverythingStep : ITutorialStep + { + private static string DEFAULT_INSTRUCTION = + "Click then drag through shapes"; + bool triggerDown; + int numMeshes; + bool userTriedAndFailed; + + private IceCreamTutorial iceCreamTutorial; + + public MultiSelectEverythingStep(IceCreamTutorial iceCreamTutorial) + { + this.iceCreamTutorial = iceCreamTutorial; + } + + public void OnPrepare() + { + PeltzerMain main = PeltzerMain.Instance; + numMeshes = main.model.GetNumberOfMeshes(); + // Call attention to the trigger. + main.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); + main.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + + // Show instructions. + PeltzerMain.Instance.GetFloatingMessage().Show(DEFAULT_INSTRUCTION, TextPosition.HALF_SIDE); + PeltzerMain.Instance.GetFloatingMessage().ShowGIF("MULTISELECT"); + PeltzerMain.Instance.restrictionManager.onlySelectableMeshIdForTutorial = null; + PeltzerMain.Instance.controllerMain.ControllerActionHandler += OnControllerEvent; + triggerDown = false; + } + + private void OnControllerEvent(object sender, ControllerEventArgs args) + { + if (args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN) + { + triggerDown = true; + } + + if (args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.UP) + { + triggerDown = false; + } + } + + public bool OnCommand(Command command) + { + // No commands are allowed in this step (no model mutations). + return false; + } + + public bool OnValidate() + { + Selector selector = PeltzerMain.Instance.GetSelector(); + List selectedMeshIds = new List(selector.selectedMeshes); + + if (selector.isMultiSelecting) + { + if (selectedMeshIds.Count == numMeshes) + { + // Success! Stop them from being able to deselect. + PeltzerMain.Instance.restrictionManager.deselectAllowed = false; + return true; + } + else if (selectedMeshIds.Count > 0) + { + // They didn't get everything yet but are still multi-selecting. + } + } + else if (selectedMeshIds.Count > 0) + { + // They've stopped multi-selecting. If the user doesn't have all the ice cream cone clear the selection. + selector.ClearState(); + // Play error sound. + // Play error haptics. + PeltzerMain.Instance.GetFloatingMessage().Show("Missed some\nTry again", TextPosition.HALF_SIDE); + userTriedAndFailed = true; + } + + if (triggerDown) + { + PeltzerMain.Instance.GetFloatingMessage().Show("Wave through everything", TextPosition.HALF_SIDE); + userTriedAndFailed = false; + } + else if (!userTriedAndFailed) + { + PeltzerMain.Instance.GetFloatingMessage().Show(DEFAULT_INSTRUCTION, TextPosition.HALF_SIDE); + } + + return false; + } + + public void ResetState() + { + triggerDown = false; + userTriedAndFailed = false; + } + + public void OnFinish() + { + // You did it! + PeltzerMain.Instance.peltzerController.YouDidIt(); + + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + // Disallow undo/redo. + PeltzerMain.Instance.restrictionManager.undoRedoAllowed = false; + // Congratulate the user on this outstanding victory. + PeltzerMain.Instance.GetFloatingMessage().Show("Now that everything is selected let's copy it", TextPosition.CENTER, true); + PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); + iceCreamTutorial.PlaySuccessSound(); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, + PeltzerMain.Instance.attentionCaller.defaultSirenGlow); + PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); + PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); + + // Reset state variables. + ResetState(); + } } - return false; - } - public bool OnValidate() { - if (deletedCherry || !PeltzerMain.Instance.model.HasMesh(tutorial.clonedCherryMeshId) || - PeltzerMain.Instance.model.MeshIsMarkedForDeletion(tutorial.clonedCherryMeshId)) { - return true; + // TODO could add a 'group' step here, then ungroup to remove the cherry. + + /// + /// Tutorial step where the user is instructed to copy the ice cream. They can place the new cone anywhere. + /// + private class CopyIceCreamStep : ITutorialStep + { + private IceCreamTutorial tutorial; + private bool copyHappened; + private bool waitingForTriggerDown = false; + private bool triggerHasBeenPressed = false; + private float nextBuzzTime; + + public CopyIceCreamStep(IceCreamTutorial tutorial) + { + this.tutorial = tutorial; + } + + private void OnControllerEvent(object sender, ControllerEventArgs args) + { + if (waitingForTriggerDown + && args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN) + { + triggerHasBeenPressed = true; + } + } + + public void OnPrepare() + { + // Show instructions. + PeltzerMain.Instance.GetFloatingMessage().Show("Copy the cone", TextPosition.FULL_SIDE); + PeltzerMain.Instance.GetFloatingMessage().ShowGIF("COPY"); + + PeltzerMain.Instance.restrictionManager.copyingAllowed = true; + + // Allow the touchpad's left button. + PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed = true; + PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_THUMBSTICK); + PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TOUCHPAD_LEFT); + PeltzerMain.Instance.attentionCaller.Recolor( + PeltzerMain.Instance.peltzerController.controllerGeometry.moveOverlay.GetComponent().leftIcon); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_THUMBSTICK); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TOUCHPAD_LEFT); + PeltzerMain.Instance.controllerMain.ControllerActionHandler += OnControllerEvent; + + // Look at me. + PeltzerMain.Instance.peltzerController.LookAtMe(); + } + + public bool OnCommand(Command command) + { + if (copyHappened) + { + return false; + } + + // We expect a CompositeCommand. We require that it contains at least a copy of the cherry as the minimal + // requirement to be able to continue the tutorial (rather than requiring everything, and risking some edge + // case blocking the tutorial. + if (!(command is CompositeCommand)) + { + return false; + } + CompositeCommand compositeCommand = (CompositeCommand)command; + foreach (Command compositeCommandEntry in compositeCommand.GetCommands()) + { + if (compositeCommandEntry is CopyMeshCommand) + { + PeltzerMain.Instance.restrictionManager.deselectAllowed = true; + CopyMeshCommand copyMeshCommand = (CopyMeshCommand)compositeCommandEntry; + if (copyMeshCommand.copiedFromId == tutorial.newCherryMeshId) + { + tutorial.clonedCherryMeshId = copyMeshCommand.GetCopyMeshId(); + copyHappened = true; + return true; + } + } + } + return false; + } + + public bool OnValidate() + { + bool copyButtonPressed = PeltzerMain.Instance.GetMover().currentMoveType == Mover.MoveType.CLONE; + + if (!copyButtonPressed && Time.time > nextBuzzTime) + { + PeltzerMain.Instance.peltzerController.LookAtMe(); + nextBuzzTime = Time.time + BUZZ_INTERVAL_DURATION; + } + + if (copyButtonPressed && !waitingForTriggerDown) + { + // Allow the touchpad's left button. + PeltzerMain.Instance.restrictionManager.touchpadLeftAllowed = false; + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_THUMBSTICK); + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TOUCHPAD_LEFT); + PeltzerMain.Instance.attentionCaller.GreyOut( + PeltzerMain.Instance.peltzerController.controllerGeometry.moveOverlay.GetComponent().leftIcon); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_THUMBSTICK); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TOUCHPAD_LEFT); + + PeltzerMain.Instance.GetFloatingMessage().Show("Place it next to the original", TextPosition.FULL_SIDE); + PeltzerMain.Instance.GetFloatingMessage().ShowGIF("INSERT_SPHERE"); + // Call attention to trigger. + PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + waitingForTriggerDown = true; + } + + return copyHappened && (waitingForTriggerDown && triggerHasBeenPressed); + } + + public void ResetState() + { + copyHappened = false; + waitingForTriggerDown = false; + triggerHasBeenPressed = false; + } + + public void OnFinish() + { + // You did it! + PeltzerMain.Instance.peltzerController.YouDidIt(); + PeltzerMain.Instance.restrictionManager.copyingAllowed = false; + PeltzerMain.Instance.restrictionManager.movingMeshesAllowed = false; + PeltzerMain.Instance.GetFloatingMessage().ShowHeader(""); + PeltzerMain.Instance.GetFloatingMessage().Show("Your friend doesn't want a cherry", TextPosition.CENTER_NO_TITLE, true); + PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); + tutorial.PlaySuccessSound(); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, + PeltzerMain.Instance.attentionCaller.defaultSirenGlow); + PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); + PeltzerMain.Instance.attentionCaller.GreyOut(ControllerMode.move); + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + + // Stop the user from selecting anything now. + PeltzerMain.Instance.restrictionManager.onlySelectableMeshIdForTutorial = -1; + PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); + + // Reset state variables. + ResetState(); + } } - // Make sure the cherry stays glowing. - PeltzerMain.Instance.attentionCaller.MakeSureMeshIsGlowing(tutorial.clonedCherryMeshId); - return false; - } - - public void ResetState() { - deletedCherry = false; - } - - public void OnFinish() { - // You did it! - PeltzerMain.Instance.peltzerController.YouDidIt(); - - PeltzerMain.Instance.attentionCaller.GreyOut(ControllerMode.delete); - // Hide the placeholder. - PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); - PeltzerMain.Instance.attentionCaller.StopMeshGlowing(tutorial.clonedCherryMeshId); - PeltzerMain.Instance.GetFloatingMessage().ShowHeader(""); - PeltzerMain.Instance.GetFloatingMessage().Show("That's better", TextPosition.CENTER_NO_TITLE, true); - PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); - tutorial.PlaySuccessSound(); - PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, - PeltzerMain.Instance.attentionCaller.defaultSirenGlow); - PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); - PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); - - // Reset state variables. - ResetState(); - } - } - /// - /// End of tutorial -- display a 'finished' message for a short while then time out. - /// - private class EndTutorialStep : ITutorialStep { - private IceCreamTutorial iceCreamTutorial; - - public EndTutorialStep(IceCreamTutorial iceCreamTutorial) { - this.iceCreamTutorial = iceCreamTutorial; - } - - float startTime = 0.0f; - float congratsTime; - float congratsDuration = 0.1f; - // How long we should wait on this step; very little time because the end animation is ~4 - // seconds and begins when this step is over. - float duration = 2f; - - public void OnPrepare() { - // Show instructions. - PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); - PeltzerMain.Instance.GetFloatingMessage().Show("", TextPosition.CENTER); - PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); - - // Re-enable menu actions. - PeltzerMain.Instance.restrictionManager.menuActionsAllowed = true; - startTime = Time.time; - congratsTime = Time.time + congratsDuration; - } - - public bool OnCommand(Command command) { - // No commands allowed in this step. - return false; - } - - public bool OnValidate() { - // This step is complete after a certain time duration. - if (Time.time > startTime + duration) { - return true; + /// + /// Tutorial step where the user is asked to switch to the delete tool. + /// + private class SwitchToDeleteToolStep : ITutorialStep + { + private IceCreamTutorial iceCreamTutorial; + private float nextBuzzTime; + + public SwitchToDeleteToolStep(IceCreamTutorial iceCreamTutorial) + { + this.iceCreamTutorial = iceCreamTutorial; + } + + public void OnPrepare() + { + // Only allow move (current tool) and the delete tool. + PeltzerMain.Instance.restrictionManager.SetAllowedControllerModes( + new List() { ControllerMode.move, ControllerMode.delete }); + PeltzerMain.Instance.attentionCaller.Recolor(ControllerMode.delete); + PeltzerMain.Instance.attentionCaller.StartGlowing(ControllerMode.delete); + // Call attention to trigger. + PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + // Show instructions. + PeltzerMain.Instance.GetFloatingMessage().ShowHeader("Erasing"); + PeltzerMain.Instance.GetFloatingMessage().Show("Grab the eraser", TextPosition.BOTTOM); + PeltzerMain.Instance.GetFloatingMessage().ShowGIF("CHOOSE_ERASER"); + + // Look at me. + PeltzerMain.Instance.paletteController.LookAtMe(); + } + + public bool OnCommand(Command command) + { + // No model mutations for now, thank you very much. + return false; + } + + public bool OnValidate() + { + if (Time.time > nextBuzzTime) + { + PeltzerMain.Instance.paletteController.LookAtMe(); + nextBuzzTime = Time.time + BUZZ_INTERVAL_DURATION; + } + + // This step is done when the user has selected the delete tool. + return PeltzerMain.Instance.peltzerController.mode == ControllerMode.delete; + } + + public void ResetState() + { + // Nothing to reset. + } + + public void OnFinish() + { + // You did it! + PeltzerMain.Instance.paletteController.YouDidIt(); + + // Only the delete tool is allowed from now on (so the user can't switch to a different one). + PeltzerMain.Instance.restrictionManager.SetOnlyAllowedControllerMode(ControllerMode.delete); + PeltzerMain.Instance.attentionCaller.StopGlowing(ControllerMode.delete); + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.GetFloatingMessage().Show("Let's get rid of it", TextPosition.CENTER, true); + PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); + iceCreamTutorial.PlaySuccessSound(); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, + PeltzerMain.Instance.attentionCaller.defaultSirenGlow); + PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); + PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); + } } - if (congratsTime != 0f && Time.time > congratsTime) { - PeltzerMain.Instance.GetFloatingMessage().Show("Congrats!\nYou've got the basics", TextPosition.CENTER_NO_TITLE); - PeltzerMain.Instance.GetFloatingMessage().ShowProgressBar(/*show*/ false); - PeltzerMain.Instance.GetFloatingMessage().PlayFinalConfetti(); - PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.tutorialCompletionSound); - PeltzerMain.Instance.attentionCaller.GlowTheSiren(); - PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(/*duration*/ 10f); - congratsTime = 0f; + /// + /// Tutorial step where the user is instructed to delete the second cherry. + /// + private class DeleteSecondCherryStep : ITutorialStep + { + private IceCreamTutorial tutorial; + private bool deletedCherry; + + public DeleteSecondCherryStep(IceCreamTutorial tutorial) + { + this.tutorial = tutorial; + } + + public void OnPrepare() + { + // Show instructions. + PeltzerMain.Instance.GetFloatingMessage().Show("Erase the\nnew cherry", TextPosition.HALF_SIDE); + PeltzerMain.Instance.GetFloatingMessage().ShowGIF("ERASE"); + PeltzerMain.Instance.attentionCaller.Recolor(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StartMeshGlowing(tutorial.clonedCherryMeshId); + PeltzerMain.Instance.restrictionManager.onlySelectableMeshIdForTutorial = tutorial.clonedCherryMeshId; + } + + // Only allow a DeleteMesh command on the cherry, which is sufficient but not necessary for this + // step. + // We want the tutorial to complete at the point the user sees the cherry disappear, which will + // be before the command is sent, as the delete tool waits until the trigger is released to issue commands. + public bool OnCommand(Command command) + { + if (deletedCherry) + { + return false; + } + + // We expect a CompositeCommand. We require that it contains at least a copy of the cherry as the minimal + // requirement to be able to continue the tutorial (rather than requiring everything, and risking some edge + // case blocking the tutorial. + if (!(command is CompositeCommand)) + { + return false; + } + CompositeCommand compositeCommand = (CompositeCommand)command; + foreach (Command compositeCommandEntry in compositeCommand.GetCommands()) + { + if (compositeCommandEntry is DeleteMeshCommand) + { + DeleteMeshCommand deleteMeshCommand = (DeleteMeshCommand)compositeCommandEntry; + if (deleteMeshCommand.MeshId == tutorial.clonedCherryMeshId) + { + deletedCherry = true; + return true; + } + } + } + return false; + } + + public bool OnValidate() + { + if (deletedCherry || !PeltzerMain.Instance.model.HasMesh(tutorial.clonedCherryMeshId) || + PeltzerMain.Instance.model.MeshIsMarkedForDeletion(tutorial.clonedCherryMeshId)) + { + return true; + } + // Make sure the cherry stays glowing. + PeltzerMain.Instance.attentionCaller.MakeSureMeshIsGlowing(tutorial.clonedCherryMeshId); + return false; + } + + public void ResetState() + { + deletedCherry = false; + } + + public void OnFinish() + { + // You did it! + PeltzerMain.Instance.peltzerController.YouDidIt(); + + PeltzerMain.Instance.attentionCaller.GreyOut(ControllerMode.delete); + // Hide the placeholder. + PeltzerMain.Instance.attentionCaller.GreyOut(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StopGlowing(AttentionCaller.Element.PELTZER_TRIGGER); + PeltzerMain.Instance.attentionCaller.StopMeshGlowing(tutorial.clonedCherryMeshId); + PeltzerMain.Instance.GetFloatingMessage().ShowHeader(""); + PeltzerMain.Instance.GetFloatingMessage().Show("That's better", TextPosition.CENTER_NO_TITLE, true); + PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); + tutorial.PlaySuccessSound(); + PeltzerMain.Instance.attentionCaller.StartGlowing(AttentionCaller.Element.SIREN, + PeltzerMain.Instance.attentionCaller.defaultSirenGlow); + PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(); + PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); + + // Reset state variables. + ResetState(); + } } - return false; - } + /// + /// End of tutorial -- display a 'finished' message for a short while then time out. + /// + private class EndTutorialStep : ITutorialStep + { + private IceCreamTutorial iceCreamTutorial; - public void ResetState() { - // Nothing to reset. - } + public EndTutorialStep(IceCreamTutorial iceCreamTutorial) + { + this.iceCreamTutorial = iceCreamTutorial; + } - public void OnFinish() { - // Play an extra-successful sound for the final step. - PeltzerMain.Instance.GetFloatingMessage().Show("Can't wait to see\nwhat you make!", TextPosition.CENTER_NO_TITLE); - // No finish actions; this step times out and is succeeded by 'ExitTutorial' cleanup. - } - } + float startTime = 0.0f; + float congratsTime; + float congratsDuration = 0.1f; + // How long we should wait on this step; very little time because the end animation is ~4 + // seconds and begins when this step is over. + float duration = 2f; + + public void OnPrepare() + { + // Show instructions. + PeltzerMain.Instance.GetFloatingMessage().IncrementProgressBar(); + PeltzerMain.Instance.GetFloatingMessage().Show("", TextPosition.CENTER); + PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); + + // Re-enable menu actions. + PeltzerMain.Instance.restrictionManager.menuActionsAllowed = true; + startTime = Time.time; + congratsTime = Time.time + congratsDuration; + } + + public bool OnCommand(Command command) + { + // No commands allowed in this step. + return false; + } + + public bool OnValidate() + { + // This step is complete after a certain time duration. + if (Time.time > startTime + duration) + { + return true; + } + + if (congratsTime != 0f && Time.time > congratsTime) + { + PeltzerMain.Instance.GetFloatingMessage().Show("Congrats!\nYou've got the basics", TextPosition.CENTER_NO_TITLE); + PeltzerMain.Instance.GetFloatingMessage().ShowProgressBar(/*show*/ false); + PeltzerMain.Instance.GetFloatingMessage().PlayFinalConfetti(); + PeltzerMain.Instance.audioLibrary.PlayClip(PeltzerMain.Instance.audioLibrary.tutorialCompletionSound); + PeltzerMain.Instance.attentionCaller.GlowTheSiren(); + PeltzerMain.Instance.attentionCaller.CascadeGlowAllLightbulbs(/*duration*/ 10f); + congratsTime = 0f; + } + + return false; + } - public IceCreamTutorial() { - AddStep(new IntroductionStep(this)); - AddStep(new ZoomAndMoveStep(this)); - AddStep(new SelectSpherePrimitiveStep(this)); - AddStep(new PlaceScoopStep(this)); - AddStep(new ScaleSphereStep(this)); - AddStep(new PlaceCherryStep(this)); - AddStep(new SwitchToPaintToolStep(this)); - AddStep(new SelectRedColorStep(this)); - AddStep(new PaintCherryStep(this)); - AddStep(new SwitchToGrabToolStep(this)); - AddStep(new MultiSelectEverythingStep(this)); - AddStep(new CopyIceCreamStep(this)); - AddStep(new SwitchToDeleteToolStep(this)); - AddStep(new DeleteSecondCherryStep(this)); - AddStep(new EndTutorialStep(this)); + public void ResetState() + { + // Nothing to reset. + } + + public void OnFinish() + { + // Play an extra-successful sound for the final step. + PeltzerMain.Instance.GetFloatingMessage().Show("Can't wait to see\nwhat you make!", TextPosition.CENTER_NO_TITLE); + // No finish actions; this step times out and is succeeded by 'ExitTutorial' cleanup. + } + } + + public IceCreamTutorial() + { + AddStep(new IntroductionStep(this)); + AddStep(new ZoomAndMoveStep(this)); + AddStep(new SelectSpherePrimitiveStep(this)); + AddStep(new PlaceScoopStep(this)); + AddStep(new ScaleSphereStep(this)); + AddStep(new PlaceCherryStep(this)); + AddStep(new SwitchToPaintToolStep(this)); + AddStep(new SelectRedColorStep(this)); + AddStep(new PaintCherryStep(this)); + AddStep(new SwitchToGrabToolStep(this)); + AddStep(new MultiSelectEverythingStep(this)); + AddStep(new CopyIceCreamStep(this)); + AddStep(new SwitchToDeleteToolStep(this)); + AddStep(new DeleteSecondCherryStep(this)); + AddStep(new EndTutorialStep(this)); + } } - } } diff --git a/Assets/Scripts/tutorial/Tutorial.cs b/Assets/Scripts/tutorial/Tutorial.cs index 0a78056f..aaaf2eca 100644 --- a/Assets/Scripts/tutorial/Tutorial.cs +++ b/Assets/Scripts/tutorial/Tutorial.cs @@ -14,30 +14,34 @@ using System.Collections.Generic; -namespace com.google.apps.peltzer.client.tutorial { - /// - /// Represents a tutorial. A tutorial is a single lesson in user education (for example, a lesson - /// that teaches the user how to place and move objects). It is made up of a list of steps, - /// which the user must go through in sequence. - /// - public abstract class Tutorial { - public List steps { get; private set; } - - protected Tutorial() { - steps = new List(); - } - +namespace com.google.apps.peltzer.client.tutorial +{ /// - /// Called to prepare the tutorial. This is called before the first step. + /// Represents a tutorial. A tutorial is a single lesson in user education (for example, a lesson + /// that teaches the user how to place and move objects). It is made up of a list of steps, + /// which the user must go through in sequence. /// - public virtual void OnPrepare() {} + public abstract class Tutorial + { + public List steps { get; private set; } - /// - /// Adds a new tutorial step. - /// - /// - protected void AddStep(ITutorialStep step) { - steps.Add(step); + protected Tutorial() + { + steps = new List(); + } + + /// + /// Called to prepare the tutorial. This is called before the first step. + /// + public virtual void OnPrepare() { } + + /// + /// Adds a new tutorial step. + /// + /// + protected void AddStep(ITutorialStep step) + { + steps.Add(step); + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/tutorial/TutorialManager.cs b/Assets/Scripts/tutorial/TutorialManager.cs index d914ec31..ca70b58f 100644 --- a/Assets/Scripts/tutorial/TutorialManager.cs +++ b/Assets/Scripts/tutorial/TutorialManager.cs @@ -21,505 +21,559 @@ using com.google.apps.peltzer.client.model.util; using com.google.apps.peltzer.client.model.render; -namespace com.google.apps.peltzer.client.tutorial { - /// - /// Manages the execution of tutorials. - /// - public class TutorialManager : MonoBehaviour { - public static string HAS_EVER_STARTED_TUTORIAL_KEY = "blocks_has_started_tutorial"; - - /// - /// Mesh ID of the camera alignment marker in the tutorial file. - /// This must be a CONE. The position of the cone will indicate where the camera should be located, - /// and the direction of the tip of the cone will indicate where it should be pointing. - /// - private const int CAMERA_ALIGNMENT_MARKER_MESH_ID = 100; - - private const float DISTANCE_FROM_USER = 1.5f; - +namespace com.google.apps.peltzer.client.tutorial +{ /// - /// Delay before first step, in seconds. + /// Manages the execution of tutorials. /// - private const float DELAY_BEFORE_FIRST_STEP = 2; - - /// - /// Delay between successive tutorial steps, in seconds. - /// - private const float DELAY_BETWEEN_STEPS = 2.5f; - - /// - /// Delay after last step, in seconds. - /// - private const float DELAY_AFTER_LAST_STEP = 1; - - // All available tutorials, ordered. - private List tutorials = new List { + public class TutorialManager : MonoBehaviour + { + public static string HAS_EVER_STARTED_TUTORIAL_KEY = "blocks_has_started_tutorial"; + + /// + /// Mesh ID of the camera alignment marker in the tutorial file. + /// This must be a CONE. The position of the cone will indicate where the camera should be located, + /// and the direction of the tip of the cone will indicate where it should be pointing. + /// + private const int CAMERA_ALIGNMENT_MARKER_MESH_ID = 100; + + private const float DISTANCE_FROM_USER = 1.5f; + + /// + /// Delay before first step, in seconds. + /// + private const float DELAY_BEFORE_FIRST_STEP = 2; + + /// + /// Delay between successive tutorial steps, in seconds. + /// + private const float DELAY_BETWEEN_STEPS = 2.5f; + + /// + /// Delay after last step, in seconds. + /// + private const float DELAY_AFTER_LAST_STEP = 1; + + // All available tutorials, ordered. + private List tutorials = new List { new IceCreamTutorial() }; - // The current tutorial being taken. - private Tutorial currentTutorial; - private int currentTutorialIndex; - - /// - /// The tutorial step we're currently running. This is null if there is no current step - /// (for example, when we're in the delay between steps). - /// - private ITutorialStep currentStep; - - // The index of the tutorial step we're currently running. - private int currentStepIndex = 0; - - /// - /// If currentStep == null, then we are counting down to advance to the next step - /// or to end the tutorial. When this reaches 0, we advance to the next step - /// (or finish the tutorial). - /// - private float countdownToAdvance; - - // Parameters to animate the scene lighting to be different for tutorials. - // Red-Blue max value to make the sky brighter (does not use a Color because Colors are capped at - // 1 and we want the tutorial sky brighter). - private const float TUTORIAL_SKY_RB_VAL = 1.06f; - - // Parameters to control the duration of in and out animations. - private const float ANIMATION_DURATION = 1.0f; - private const float ANIMATION_DURATION_IN = 1.0f; - private const float MOUNTAIN_ANIMATION_DURATION = 1.5f; - private const float ANIMATION_DURATION_OUT = 3.0f; - private const float ANIMATION_QUADRATIC = 4.0f; - - // Scale of the model when the out-animation begins. - private float startingAnimationScale = 0f; - - // Parameters to animate the ground coloring (and fog and fresnel of distant mountains). - private static readonly Color TUTORIAL_GROUND_DIFFUSE_A = - new Color(255f / 255f, 235f / 255f, 219f / 255f, 255f / 255f); - private static readonly Color TUTORIAL_GROUND_DIFFUSE_B = - new Color(233f / 255f, 205f / 255f, 200f / 255f, 255f / 255f); - private static readonly Color TUTORIAL_GROUND_FOG = - new Color(255f / 255f, 230f / 255f, 219f / 255f, 255f / 255f); - private static readonly Color TUTORIAL_GROUND_FRESNEL = - new Color(255f / 255f, 240f / 255f, 219f / 255f, 255f / 255f); - - private static readonly Color ORIGINAL_GROUND_DIFFUSE_A = - new Color(255f / 255f, 233f / 255f, 190f / 255f, 255f / 255f); - private static readonly Color ORIGINAL_GROUND_DIFFUSE_B = - new Color(233f / 255f, 205f / 255f, 176f / 255f, 255f / 255f); - private static readonly Color ORIGINAL_GROUND_FOG = - new Color(255f / 255f, 230f / 255f, 195f / 255f, 255f / 255f); - private static readonly Color ORIGINAL_GROUND_FRESNEL = - new Color(255f / 255f, 228f / 255f, 190f / 255f, 255f / 255f); - - // Height of the flat terrain (without mountains). - private const float TERRAIN_FLAT_HEIGHT = 0.0378f; - - // Parameters to adjust the final confetti effect animation. - private const float PITCH_MIN = 0.9f; - private const float PITCH_MAX = 1.3f; - private const float RADIUS_MIN = 0.15f; - private const float RADIUS_MAX = 0.3f; - private const float STAGGER_TIME = 0.0008f; - - private Lighting polyLighting; - private bool animatingIn; - private bool animatingOut; - private float timeStartedAnimating; - private bool animatingInMesh; - private float timeStartedAnimatingMesh; - private Vector3 startingAnimationOffset; - private Vector3 finalAnimationOffset; - - /// - /// Queue of meshes to be destroyed for the end tutorial animation, paired with a float that holds - /// the time they should be detonated. - /// - private Queue> meshesToBeShattered = new Queue>(); - private ParticleSystem finishEffectPrefab; - - public void Start() { - polyLighting = ObjectFinder.ComponentById("ID_Lighting"); - finishEffectPrefab = Resources.Load("Tutorial/FinishEffect"); - } - - /// - /// Returns whether or not a tutorial is currently occurring. - /// - /// - public bool TutorialOccurring() { - return currentTutorial != null; - } - - /// - /// Start a given tutorial. - /// - /// The index of the tutorial, 0-indexed - public void StartTutorial(int tutorialIndex) { - if (tutorialIndex < 0 || tutorialIndex >= tutorials.Count) { - Debug.LogError("Only " + tutorials.Count + " tutorials exist"); - return; - } - - PlayerPrefs.SetString(HAS_EVER_STARTED_TUTORIAL_KEY, "true"); - PlayerPrefs.Save(); - - PeltzerMain.Instance.CreateNewModel(); + // The current tutorial being taken. + private Tutorial currentTutorial; + private int currentTutorialIndex; + + /// + /// The tutorial step we're currently running. This is null if there is no current step + /// (for example, when we're in the delay between steps). + /// + private ITutorialStep currentStep; + + // The index of the tutorial step we're currently running. + private int currentStepIndex = 0; + + /// + /// If currentStep == null, then we are counting down to advance to the next step + /// or to end the tutorial. When this reaches 0, we advance to the next step + /// (or finish the tutorial). + /// + private float countdownToAdvance; + + // Parameters to animate the scene lighting to be different for tutorials. + // Red-Blue max value to make the sky brighter (does not use a Color because Colors are capped at + // 1 and we want the tutorial sky brighter). + private const float TUTORIAL_SKY_RB_VAL = 1.06f; + + // Parameters to control the duration of in and out animations. + private const float ANIMATION_DURATION = 1.0f; + private const float ANIMATION_DURATION_IN = 1.0f; + private const float MOUNTAIN_ANIMATION_DURATION = 1.5f; + private const float ANIMATION_DURATION_OUT = 3.0f; + private const float ANIMATION_QUADRATIC = 4.0f; + + // Scale of the model when the out-animation begins. + private float startingAnimationScale = 0f; + + // Parameters to animate the ground coloring (and fog and fresnel of distant mountains). + private static readonly Color TUTORIAL_GROUND_DIFFUSE_A = + new Color(255f / 255f, 235f / 255f, 219f / 255f, 255f / 255f); + private static readonly Color TUTORIAL_GROUND_DIFFUSE_B = + new Color(233f / 255f, 205f / 255f, 200f / 255f, 255f / 255f); + private static readonly Color TUTORIAL_GROUND_FOG = + new Color(255f / 255f, 230f / 255f, 219f / 255f, 255f / 255f); + private static readonly Color TUTORIAL_GROUND_FRESNEL = + new Color(255f / 255f, 240f / 255f, 219f / 255f, 255f / 255f); + + private static readonly Color ORIGINAL_GROUND_DIFFUSE_A = + new Color(255f / 255f, 233f / 255f, 190f / 255f, 255f / 255f); + private static readonly Color ORIGINAL_GROUND_DIFFUSE_B = + new Color(233f / 255f, 205f / 255f, 176f / 255f, 255f / 255f); + private static readonly Color ORIGINAL_GROUND_FOG = + new Color(255f / 255f, 230f / 255f, 195f / 255f, 255f / 255f); + private static readonly Color ORIGINAL_GROUND_FRESNEL = + new Color(255f / 255f, 228f / 255f, 190f / 255f, 255f / 255f); + + // Height of the flat terrain (without mountains). + private const float TERRAIN_FLAT_HEIGHT = 0.0378f; + + // Parameters to adjust the final confetti effect animation. + private const float PITCH_MIN = 0.9f; + private const float PITCH_MAX = 1.3f; + private const float RADIUS_MIN = 0.15f; + private const float RADIUS_MAX = 0.3f; + private const float STAGGER_TIME = 0.0008f; + + private Lighting polyLighting; + private bool animatingIn; + private bool animatingOut; + private float timeStartedAnimating; + private bool animatingInMesh; + private float timeStartedAnimatingMesh; + private Vector3 startingAnimationOffset; + private Vector3 finalAnimationOffset; + + /// + /// Queue of meshes to be destroyed for the end tutorial animation, paired with a float that holds + /// the time they should be detonated. + /// + private Queue> meshesToBeShattered = new Queue>(); + private ParticleSystem finishEffectPrefab; + + public void Start() + { + polyLighting = ObjectFinder.ComponentById("ID_Lighting"); + finishEffectPrefab = Resources.Load("Tutorial/FinishEffect"); + } - animatingIn = true; - timeStartedAnimating = Time.time; + /// + /// Returns whether or not a tutorial is currently occurring. + /// + /// + public bool TutorialOccurring() + { + return currentTutorial != null; + } - currentTutorialIndex = tutorialIndex; - currentTutorial = tutorials[currentTutorialIndex]; - currentStep = null; + /// + /// Start a given tutorial. + /// + /// The index of the tutorial, 0-indexed + public void StartTutorial(int tutorialIndex) + { + if (tutorialIndex < 0 || tutorialIndex >= tutorials.Count) + { + Debug.LogError("Only " + tutorials.Count + " tutorials exist"); + return; + } - // Prepare tutorial. - currentTutorial.OnPrepare(); + PlayerPrefs.SetString(HAS_EVER_STARTED_TUTORIAL_KEY, "true"); + PlayerPrefs.Save(); - // Install a command validator in the model so the tutorial can validate commands. - PeltzerMain.Instance.model.OnValidateCommand += ValidateCommand; - PeltzerMain.Instance.controllerMain.ControllerActionHandler += OnControllerEvent; + PeltzerMain.Instance.CreateNewModel(); - // TODO(bug): implement other tutorials. - currentStepIndex = 0; - countdownToAdvance = DELAY_BEFORE_FIRST_STEP; + animatingIn = true; + timeStartedAnimating = Time.time; - // Don't show move/zoom tooltips during the tutorial. - PeltzerMain.Instance.restrictionManager.tooltipsAllowed = false; - PeltzerMain.Instance.paletteController.DisableGripTooltips(); - PeltzerMain.Instance.peltzerController.DisableGripTooltips(); - PeltzerMain.Instance.paletteController.DisableSnapTooltips(); + currentTutorialIndex = tutorialIndex; + currentTutorial = tutorials[currentTutorialIndex]; + currentStep = null; - // Play a start tutorial sound effect. - AudioLibrary audioLibrary = PeltzerMain.Instance.audioLibrary; - audioLibrary.PlayClip(audioLibrary.tutorialIntroSound); + // Prepare tutorial. + currentTutorial.OnPrepare(); - } + // Install a command validator in the model so the tutorial can validate commands. + PeltzerMain.Instance.model.OnValidateCommand += ValidateCommand; + PeltzerMain.Instance.controllerMain.ControllerActionHandler += OnControllerEvent; - /// - /// Indicates if the given command is allowed or not in the current tutorial state. - /// In particular, if there is no tutorial currently active, this returns true. - /// - /// The command to validate - /// True if the command is allowed, false if not. - public bool ValidateCommand(Command command) { - // If no tutorial is currently active, all commands are valid. - if (currentTutorial == null) { - return true; - } - // Otherwise, a command is only valid if there is an active step and that step approves it. - return currentStep != null && currentStep.OnCommand(command); - } + // TODO(bug): implement other tutorials. + currentStepIndex = 0; + countdownToAdvance = DELAY_BEFORE_FIRST_STEP; - /// - /// Exits the tutorial. Can only be called if a tutorial is in progress. - /// - public void ExitTutorial(bool isForceExit = false) { - PeltzerMain.Instance.GetFloatingMessage().ShowProgressBar(/*show*/ false); - - AssertOrThrow.NotNull(currentTutorial, "Can't exit tutorial: not in a tutorial."); - float curPct = 1f; - if (animatingIn) { - curPct = (Time.time - timeStartedAnimating) / ANIMATION_DURATION_IN; - animatingIn = false; - } - - if (isForceExit) { - PeltzerMain.Instance.GetFloatingMessage().Show("Come back any time!", TextPosition.CENTER); - PeltzerMain.Instance.GetFloatingMessage().ShowHeader(""); - PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); - } - - animatingOut = true; - IceCreamFinishEffect(); - startingAnimationScale = PeltzerMain.Instance.worldSpace.scale; - startingAnimationOffset = PeltzerMain.Instance.worldSpace.offset; - finalAnimationOffset = new Vector3(startingAnimationOffset.x, -2f, startingAnimationOffset.z); - - // Reset the state of the current step, in case the user wants to complete the tutorial again later. - if (isForceExit && currentStep != null) { - currentStep.ResetState(); - } - - timeStartedAnimating = Time.time; - currentStep = null; - currentTutorial = null; - PeltzerMain.Instance.model.OnValidateCommand -= ValidateCommand; - PeltzerMain.Instance.attentionCaller.ResetAll(); - - PeltzerMain.Instance.GetSelector().DeselectAll(); - PeltzerMain.Instance.attentionCaller.StopGlowingAll(); - - if (!PeltzerMain.Instance.HasEverShownFeaturedTooltip - && !PeltzerMain.Instance.applicationButtonToolTips.IsActive()) { - PeltzerMain.Instance.applicationButtonToolTips.TurnOn("ViewFeatured"); - PeltzerMain.Instance.polyMenuMain.SwitchToFeaturedSection(); - PeltzerMain.Instance.HasEverShownFeaturedTooltip = true; - - } - } + // Don't show move/zoom tooltips during the tutorial. + PeltzerMain.Instance.restrictionManager.tooltipsAllowed = false; + PeltzerMain.Instance.paletteController.DisableGripTooltips(); + PeltzerMain.Instance.peltzerController.DisableGripTooltips(); + PeltzerMain.Instance.paletteController.DisableSnapTooltips(); - /// - /// Make the ice cream explode into sprinkles upon completion. - /// - public void IceCreamFinishEffect() { - float startTime = Time.time; - - // Manually reverse the order of the meshes. - ICollection meshCollection = PeltzerMain.Instance.model.GetAllMeshes(); - List meshes = new List(meshCollection.Count); - - foreach (MMesh mesh in meshCollection) { - meshes.Add(mesh); - } - for (int i = meshes.Count - 1; i >= 0; i--) { - // Push each mesh onto the particle system effect queue. - startTime += STAGGER_TIME; - meshesToBeShattered.Enqueue(new KeyValuePair(meshes[i], startTime)); - } - } + // Play a start tutorial sound effect. + AudioLibrary audioLibrary = PeltzerMain.Instance.audioLibrary; + audioLibrary.PlayClip(audioLibrary.tutorialIntroSound); - /// - /// Process the finish effect queue; when the time has come for the mesh to be shattered, dequeue - /// it and instantiate a relevant particle system effect. - /// - public void ProcessFinishEffectQueue() { - if (meshesToBeShattered.Peek().Value <= Time.time) { - KeyValuePair pair = meshesToBeShattered.Dequeue(); - MMesh mesh = pair.Key; - - // Only play an effect for a third of the meshes, to reduce the visual & performance overload, per bug - if (meshesToBeShattered.Count % 3 == 0) { - // Instantiate a shatter effect. - ParticleSystem shatterEffect = Instantiate(finishEffectPrefab); - // Remove the shatter effect from the scene when it is over. - Destroy(shatterEffect, shatterEffect.main.duration); - - // Play a confetti sound effect. - AudioLibrary audioLibrary = PeltzerMain.Instance.audioLibrary; - float pitch = Random.Range(PITCH_MIN, PITCH_MAX); - audioLibrary.PlayClip(audioLibrary.confettiSound, pitch); - - shatterEffect.transform.position = PeltzerMain.Instance.worldSpace.ModelToWorld(mesh.offset); - ParticleSystem.ShapeModule shapeModule = shatterEffect.shape; - shapeModule.radius = Random.Range(RADIUS_MIN, RADIUS_MAX); - int materialId = mesh.GetFace(0).properties.materialId; - if (materialId == MaterialRegistry.PINK_WIREFRAME_ID - || materialId == MaterialRegistry.GREEN_WIREFRAME_ID) { - materialId = MaterialRegistry.RED_ID; - } - shatterEffect.GetComponent().material = - MaterialRegistry.GetMaterialWithAlbedoById(materialId); } - // Hide the mesh- safe because we will soon call CreateNewModel() when the tutorial is finished exitting, - // deleting any remaining meshes. - PeltzerMain.Instance.model.HideMeshForTestOrTutorial(mesh.id); - } - } + /// + /// Indicates if the given command is allowed or not in the current tutorial state. + /// In particular, if there is no tutorial currently active, this returns true. + /// + /// The command to validate + /// True if the command is allowed, false if not. + public bool ValidateCommand(Command command) + { + // If no tutorial is currently active, all commands are valid. + if (currentTutorial == null) + { + return true; + } + // Otherwise, a command is only valid if there is an active step and that step approves it. + return currentStep != null && currentStep.OnCommand(command); + } - private void AdvanceToNextStep() { - if (currentStepIndex >= currentTutorial.steps.Count) { - // End of tutorial. - ExitTutorial(); - } else { - // Advance to next step. - currentStep = currentTutorial.steps[currentStepIndex]; - currentStepIndex++; - // Prepare the step for display. - currentStep.OnPrepare(); - } - } + /// + /// Exits the tutorial. Can only be called if a tutorial is in progress. + /// + public void ExitTutorial(bool isForceExit = false) + { + PeltzerMain.Instance.GetFloatingMessage().ShowProgressBar(/*show*/ false); + + AssertOrThrow.NotNull(currentTutorial, "Can't exit tutorial: not in a tutorial."); + float curPct = 1f; + if (animatingIn) + { + curPct = (Time.time - timeStartedAnimating) / ANIMATION_DURATION_IN; + animatingIn = false; + } + + if (isForceExit) + { + PeltzerMain.Instance.GetFloatingMessage().Show("Come back any time!", TextPosition.CENTER); + PeltzerMain.Instance.GetFloatingMessage().ShowHeader(""); + PeltzerMain.Instance.GetFloatingMessage().HideAllGIFs(); + } + + animatingOut = true; + IceCreamFinishEffect(); + startingAnimationScale = PeltzerMain.Instance.worldSpace.scale; + startingAnimationOffset = PeltzerMain.Instance.worldSpace.offset; + finalAnimationOffset = new Vector3(startingAnimationOffset.x, -2f, startingAnimationOffset.z); + + // Reset the state of the current step, in case the user wants to complete the tutorial again later. + if (isForceExit && currentStep != null) + { + currentStep.ResetState(); + } + + timeStartedAnimating = Time.time; + currentStep = null; + currentTutorial = null; + PeltzerMain.Instance.model.OnValidateCommand -= ValidateCommand; + PeltzerMain.Instance.attentionCaller.ResetAll(); + + PeltzerMain.Instance.GetSelector().DeselectAll(); + PeltzerMain.Instance.attentionCaller.StopGlowingAll(); + + if (!PeltzerMain.Instance.HasEverShownFeaturedTooltip + && !PeltzerMain.Instance.applicationButtonToolTips.IsActive()) + { + PeltzerMain.Instance.applicationButtonToolTips.TurnOn("ViewFeatured"); + PeltzerMain.Instance.polyMenuMain.SwitchToFeaturedSection(); + PeltzerMain.Instance.HasEverShownFeaturedTooltip = true; + + } + } - /// - /// Exits the tutorial if there is a peltzer controller trigger pull while the user - /// is hovering over the tutorial exit button. - /// - /// - /// - private void OnControllerEvent(object sender, ControllerEventArgs args) { + /// + /// Make the ice cream explode into sprinkles upon completion. + /// + public void IceCreamFinishEffect() + { + float startTime = Time.time; + + // Manually reverse the order of the meshes. + ICollection meshCollection = PeltzerMain.Instance.model.GetAllMeshes(); + List meshes = new List(meshCollection.Count); + + foreach (MMesh mesh in meshCollection) + { + meshes.Add(mesh); + } + for (int i = meshes.Count - 1; i >= 0; i--) + { + // Push each mesh onto the particle system effect queue. + startTime += STAGGER_TIME; + meshesToBeShattered.Enqueue(new KeyValuePair(meshes[i], startTime)); + } + } - } + /// + /// Process the finish effect queue; when the time has come for the mesh to be shattered, dequeue + /// it and instantiate a relevant particle system effect. + /// + public void ProcessFinishEffectQueue() + { + if (meshesToBeShattered.Peek().Value <= Time.time) + { + KeyValuePair pair = meshesToBeShattered.Dequeue(); + MMesh mesh = pair.Key; + + // Only play an effect for a third of the meshes, to reduce the visual & performance overload, per bug + if (meshesToBeShattered.Count % 3 == 0) + { + // Instantiate a shatter effect. + ParticleSystem shatterEffect = Instantiate(finishEffectPrefab); + // Remove the shatter effect from the scene when it is over. + Destroy(shatterEffect, shatterEffect.main.duration); + + // Play a confetti sound effect. + AudioLibrary audioLibrary = PeltzerMain.Instance.audioLibrary; + float pitch = Random.Range(PITCH_MIN, PITCH_MAX); + audioLibrary.PlayClip(audioLibrary.confettiSound, pitch); + + shatterEffect.transform.position = PeltzerMain.Instance.worldSpace.ModelToWorld(mesh.offset); + ParticleSystem.ShapeModule shapeModule = shatterEffect.shape; + shapeModule.radius = Random.Range(RADIUS_MIN, RADIUS_MAX); + int materialId = mesh.GetFace(0).properties.materialId; + if (materialId == MaterialRegistry.PINK_WIREFRAME_ID + || materialId == MaterialRegistry.GREEN_WIREFRAME_ID) + { + materialId = MaterialRegistry.RED_ID; + } + shatterEffect.GetComponent().material = + MaterialRegistry.GetMaterialWithAlbedoById(materialId); + } + + // Hide the mesh- safe because we will soon call CreateNewModel() when the tutorial is finished exitting, + // deleting any remaining meshes. + PeltzerMain.Instance.model.HideMeshForTestOrTutorial(mesh.id); + } + } - private bool IsGrabEvent(ControllerEventArgs args) { - return args.ControllerType == ControllerType.PELTZER - && args.ButtonId == ButtonId.Trigger - && args.Action == ButtonAction.DOWN; - } + private void AdvanceToNextStep() + { + if (currentStepIndex >= currentTutorial.steps.Count) + { + // End of tutorial. + ExitTutorial(); + } + else + { + // Advance to next step. + currentStep = currentTutorial.steps[currentStepIndex]; + currentStepIndex++; + // Prepare the step for display. + currentStep.OnPrepare(); + } + } - // Fade the terrain colors to those of the tutorial environment. - private void AnimateTerrain(GameObject terrain, float pct) { - if (animatingIn) { - terrain.GetComponent().material.SetVector("_DiffuseColorA", - Color.Lerp(ORIGINAL_GROUND_DIFFUSE_A, TUTORIAL_GROUND_DIFFUSE_A, pct)); - terrain.GetComponent().material.SetVector("_DiffuseColorB", - Color.Lerp(ORIGINAL_GROUND_DIFFUSE_B, TUTORIAL_GROUND_DIFFUSE_B, pct)); - terrain.GetComponent().material.SetVector("_FogColor", - Color.Lerp(ORIGINAL_GROUND_FOG, TUTORIAL_GROUND_FOG, pct)); - terrain.GetComponent().material.SetVector("_FresnelColor", - Color.Lerp(ORIGINAL_GROUND_FRESNEL, TUTORIAL_GROUND_FRESNEL, pct)); - } else { - terrain.GetComponent().material.SetVector("_DiffuseColorA", - Color.Lerp(TUTORIAL_GROUND_DIFFUSE_A, ORIGINAL_GROUND_DIFFUSE_A, pct)); - terrain.GetComponent().material.SetVector("_DiffuseColorB", - Color.Lerp(TUTORIAL_GROUND_DIFFUSE_B, ORIGINAL_GROUND_DIFFUSE_B, pct)); - terrain.GetComponent().material.SetVector("_FogColor", - Color.Lerp(TUTORIAL_GROUND_FOG, ORIGINAL_GROUND_FOG, pct)); - terrain.GetComponent().material.SetVector("_FresnelColor", - Color.Lerp(TUTORIAL_GROUND_FRESNEL, ORIGINAL_GROUND_FRESNEL, pct)); - } - } + /// + /// Exits the tutorial if there is a peltzer controller trigger pull while the user + /// is hovering over the tutorial exit button. + /// + /// + /// + private void OnControllerEvent(object sender, ControllerEventArgs args) + { - private void Update() { - if (animatingIn) { - float pct = (Time.time - timeStartedAnimating) / ANIMATION_DURATION_IN; - if (pct >= 1) { - pct = 1; - animatingIn = false; - } else { - float r = Mathf.Lerp(1f, TUTORIAL_SKY_RB_VAL, pct); - float b = Mathf.Lerp(1f, TUTORIAL_SKY_RB_VAL, pct); - RenderSettings.skybox.SetVector("_Tint", new Vector4(r, 1f, b, 1f)); - - // Fade ground in. - GameObject terrain = ObjectFinder.ObjectById("ID_TerrainLift").transform.Find("Terrain2-Best").gameObject; - GameObject terrainWithoutMountains = ObjectFinder.ObjectById("ID_TerrainNoMountains").gameObject; - AnimateTerrain(terrain, pct); - AnimateTerrain(terrainWithoutMountains, pct); - - // Shrink mountains with a Bezier curve ease. - float mountainPct = - Math3d.CubicBezierEasing(0f, 0f, 0.2f, 1f, (Time.time - timeStartedAnimating) / ANIMATION_DURATION_IN); - float y = Mathf.Lerp(1f, TERRAIN_FLAT_HEIGHT, mountainPct); - ObjectFinder.ObjectById("ID_TerrainLift").transform.localScale = new Vector3(1f, y, 1f); - - // Don't do anything else while animating in. - return; } - } else if (meshesToBeShattered.Count > 0) { - // Process any meshes that are queued to be shattered in the tutorial ending animation. - ProcessFinishEffectQueue(); - } else if (animatingOut) { - float pct = (Time.time - timeStartedAnimating) / ANIMATION_DURATION_OUT; - if (pct >= 1) { - pct = 1; - animatingOut = false; - - // Remove all restrictions so the user is back to the fully functional mode. - PeltzerMain.Instance.restrictionManager.AllowAll(); - // Once the exit animations are done start a new model. - PeltzerMain.Instance.CreateNewModel(); - PeltzerMain.Instance.GetFloatingMessage().FadeOutBillboard(); - } else { - // Return the sky to the correct color. - float r = Mathf.Lerp(TUTORIAL_SKY_RB_VAL, 1f, pct); - float b = Mathf.Lerp(TUTORIAL_SKY_RB_VAL, 1f, pct); - RenderSettings.skybox.SetVector("_Tint", new Vector4(r, 1f, b, 1f)); - - // Fade ground out. - GameObject terrain = ObjectFinder.ObjectById("ID_TerrainLift").transform.Find("Terrain2-Best").gameObject; - GameObject terrainWithoutMountains = ObjectFinder.ObjectById("ID_TerrainNoMountains").gameObject; - AnimateTerrain(terrain, pct); - AnimateTerrain(terrainWithoutMountains, pct); - - // Raise mountains with a Bezier curve ease. - float mountainPct = - Math3d.CubicBezierEasing(0f, 0f, 0.2f, 1f, (Time.time - timeStartedAnimating) / ANIMATION_DURATION_OUT); - float y = Mathf.Lerp(TERRAIN_FLAT_HEIGHT, 1f, mountainPct); - ObjectFinder.ObjectById("ID_TerrainLift").transform.localScale = new Vector3(1f, y, 1f); + + private bool IsGrabEvent(ControllerEventArgs args) + { + return args.ControllerType == ControllerType.PELTZER + && args.ButtonId == ButtonId.Trigger + && args.Action == ButtonAction.DOWN; } - } - if (animatingInMesh) { - float pct = Math3d.CubicBezierEasing(0f, 0f, 0.2f, 1f, (Time.time - timeStartedAnimatingMesh) / ANIMATION_DURATION_IN); - if (pct >= 1) { - pct = 1; - animatingInMesh = false; + // Fade the terrain colors to those of the tutorial environment. + private void AnimateTerrain(GameObject terrain, float pct) + { + if (animatingIn) + { + terrain.GetComponent().material.SetVector("_DiffuseColorA", + Color.Lerp(ORIGINAL_GROUND_DIFFUSE_A, TUTORIAL_GROUND_DIFFUSE_A, pct)); + terrain.GetComponent().material.SetVector("_DiffuseColorB", + Color.Lerp(ORIGINAL_GROUND_DIFFUSE_B, TUTORIAL_GROUND_DIFFUSE_B, pct)); + terrain.GetComponent().material.SetVector("_FogColor", + Color.Lerp(ORIGINAL_GROUND_FOG, TUTORIAL_GROUND_FOG, pct)); + terrain.GetComponent().material.SetVector("_FresnelColor", + Color.Lerp(ORIGINAL_GROUND_FRESNEL, TUTORIAL_GROUND_FRESNEL, pct)); + } + else + { + terrain.GetComponent().material.SetVector("_DiffuseColorA", + Color.Lerp(TUTORIAL_GROUND_DIFFUSE_A, ORIGINAL_GROUND_DIFFUSE_A, pct)); + terrain.GetComponent().material.SetVector("_DiffuseColorB", + Color.Lerp(TUTORIAL_GROUND_DIFFUSE_B, ORIGINAL_GROUND_DIFFUSE_B, pct)); + terrain.GetComponent().material.SetVector("_FogColor", + Color.Lerp(TUTORIAL_GROUND_FOG, ORIGINAL_GROUND_FOG, pct)); + terrain.GetComponent().material.SetVector("_FresnelColor", + Color.Lerp(TUTORIAL_GROUND_FRESNEL, ORIGINAL_GROUND_FRESNEL, pct)); + } } - PeltzerMain.Instance.worldSpace.scale = Mathf.Lerp(0.5f, 1, pct); - } - - if (currentTutorial == null) { - // No tutorial is currently active, so there's nothing to do. - return; - } - - if (currentStep == null) { - // We are counting down to the next step or to the end of the tutorial. - if ((countdownToAdvance -= Time.deltaTime) <= 0) { - // Time to advance. - AdvanceToNextStep(); + + private void Update() + { + if (animatingIn) + { + float pct = (Time.time - timeStartedAnimating) / ANIMATION_DURATION_IN; + if (pct >= 1) + { + pct = 1; + animatingIn = false; + } + else + { + float r = Mathf.Lerp(1f, TUTORIAL_SKY_RB_VAL, pct); + float b = Mathf.Lerp(1f, TUTORIAL_SKY_RB_VAL, pct); + RenderSettings.skybox.SetVector("_Tint", new Vector4(r, 1f, b, 1f)); + + // Fade ground in. + GameObject terrain = ObjectFinder.ObjectById("ID_TerrainLift").transform.Find("Terrain2-Best").gameObject; + GameObject terrainWithoutMountains = ObjectFinder.ObjectById("ID_TerrainNoMountains").gameObject; + AnimateTerrain(terrain, pct); + AnimateTerrain(terrainWithoutMountains, pct); + + // Shrink mountains with a Bezier curve ease. + float mountainPct = + Math3d.CubicBezierEasing(0f, 0f, 0.2f, 1f, (Time.time - timeStartedAnimating) / ANIMATION_DURATION_IN); + float y = Mathf.Lerp(1f, TERRAIN_FLAT_HEIGHT, mountainPct); + ObjectFinder.ObjectById("ID_TerrainLift").transform.localScale = new Vector3(1f, y, 1f); + + // Don't do anything else while animating in. + return; + } + } + else if (meshesToBeShattered.Count > 0) + { + // Process any meshes that are queued to be shattered in the tutorial ending animation. + ProcessFinishEffectQueue(); + } + else if (animatingOut) + { + float pct = (Time.time - timeStartedAnimating) / ANIMATION_DURATION_OUT; + if (pct >= 1) + { + pct = 1; + animatingOut = false; + + // Remove all restrictions so the user is back to the fully functional mode. + PeltzerMain.Instance.restrictionManager.AllowAll(); + // Once the exit animations are done start a new model. + PeltzerMain.Instance.CreateNewModel(); + PeltzerMain.Instance.GetFloatingMessage().FadeOutBillboard(); + } + else + { + // Return the sky to the correct color. + float r = Mathf.Lerp(TUTORIAL_SKY_RB_VAL, 1f, pct); + float b = Mathf.Lerp(TUTORIAL_SKY_RB_VAL, 1f, pct); + RenderSettings.skybox.SetVector("_Tint", new Vector4(r, 1f, b, 1f)); + + // Fade ground out. + GameObject terrain = ObjectFinder.ObjectById("ID_TerrainLift").transform.Find("Terrain2-Best").gameObject; + GameObject terrainWithoutMountains = ObjectFinder.ObjectById("ID_TerrainNoMountains").gameObject; + AnimateTerrain(terrain, pct); + AnimateTerrain(terrainWithoutMountains, pct); + + // Raise mountains with a Bezier curve ease. + float mountainPct = + Math3d.CubicBezierEasing(0f, 0f, 0.2f, 1f, (Time.time - timeStartedAnimating) / ANIMATION_DURATION_OUT); + float y = Mathf.Lerp(TERRAIN_FLAT_HEIGHT, 1f, mountainPct); + ObjectFinder.ObjectById("ID_TerrainLift").transform.localScale = new Vector3(1f, y, 1f); + } + } + + if (animatingInMesh) + { + float pct = Math3d.CubicBezierEasing(0f, 0f, 0.2f, 1f, (Time.time - timeStartedAnimatingMesh) / ANIMATION_DURATION_IN); + if (pct >= 1) + { + pct = 1; + animatingInMesh = false; + } + PeltzerMain.Instance.worldSpace.scale = Mathf.Lerp(0.5f, 1, pct); + } + + if (currentTutorial == null) + { + // No tutorial is currently active, so there's nothing to do. + return; + } + + if (currentStep == null) + { + // We are counting down to the next step or to the end of the tutorial. + if ((countdownToAdvance -= Time.deltaTime) <= 0) + { + // Time to advance. + AdvanceToNextStep(); + } + } + else if (currentStep.OnValidate()) + { + // User completed step. Start counting down to the next step. + currentStep.OnFinish(); + currentStep = null; + countdownToAdvance = (currentStepIndex >= currentTutorial.steps.Count) ? + DELAY_AFTER_LAST_STEP : DELAY_BETWEEN_STEPS; + } } - } else if (currentStep.OnValidate()) { - // User completed step. Start counting down to the next step. - currentStep.OnFinish(); - currentStep = null; - countdownToAdvance = (currentStepIndex >= currentTutorial.steps.Count) ? - DELAY_AFTER_LAST_STEP : DELAY_BETWEEN_STEPS; - } - } - /// - /// Loads a tutorial file name from the resources folder and rotates/translates the world so that model - /// appears in a static position in front of the billboard. - /// - /// The resources path of the tutorial file name to load. - public void LoadAndAlignTutorialModel(string path, Vector3 anchorDirection, Vector3 anchorPosition) { - PeltzerMain.Instance.LoadPeltzerFileFromResources(path, /*resetAttentionCaller*/ false, /*resetRestrictions*/ false); - animatingInMesh = true; - // Play a sound effect for the model animating in. - AudioLibrary audioLibrary = PeltzerMain.Instance.audioLibrary; - audioLibrary.PlayClip(audioLibrary.tutorialMeshAnimateInSound); + /// + /// Loads a tutorial file name from the resources folder and rotates/translates the world so that model + /// appears in a static position in front of the billboard. + /// + /// The resources path of the tutorial file name to load. + public void LoadAndAlignTutorialModel(string path, Vector3 anchorDirection, Vector3 anchorPosition) + { + PeltzerMain.Instance.LoadPeltzerFileFromResources(path, /*resetAttentionCaller*/ false, /*resetRestrictions*/ false); + animatingInMesh = true; + // Play a sound effect for the model animating in. + AudioLibrary audioLibrary = PeltzerMain.Instance.audioLibrary; + audioLibrary.PlayClip(audioLibrary.tutorialMeshAnimateInSound); - timeStartedAnimatingMesh = Time.time; + timeStartedAnimatingMesh = Time.time; - Quaternion rotationalOffset = Quaternion.AngleAxis(45f, Vector3.up); + Quaternion rotationalOffset = Quaternion.AngleAxis(45f, Vector3.up); - // Look for the camera marker mesh. - MMesh marker = PeltzerMain.Instance.model.GetMesh(CAMERA_ALIGNMENT_MARKER_MESH_ID); - Vector3 markerPos = marker.offset; + // Look for the camera marker mesh. + MMesh marker = PeltzerMain.Instance.model.GetMesh(CAMERA_ALIGNMENT_MARKER_MESH_ID); + Vector3 markerPos = marker.offset; - // Figure out where the tip of the cone is pointing. This will give us the direction the camera has to point. - Vector3 tipPos = marker.VertexPositionInModelCoords(FindTipOfCone(marker)); + // Figure out where the tip of the cone is pointing. This will give us the direction the camera has to point. + Vector3 tipPos = marker.VertexPositionInModelCoords(FindTipOfCone(marker)); - // The "forward" vector we want is the vector that goes from the marker position to the tip position. - Vector3 desiredForward = tipPos - markerPos; + // The "forward" vector we want is the vector that goes from the marker position to the tip position. + Vector3 desiredForward = tipPos - markerPos; - Quaternion fullRotation = Quaternion.FromToRotation(desiredForward, anchorDirection); + Quaternion fullRotation = Quaternion.FromToRotation(desiredForward, anchorDirection); - // Set the world rotation such that desiredForward is rotated into cameraForward. - PeltzerMain.Instance.worldSpace.rotation = Quaternion.Euler(0f, fullRotation.eulerAngles.y, 0f); + // Set the world rotation such that desiredForward is rotated into cameraForward. + PeltzerMain.Instance.worldSpace.rotation = Quaternion.Euler(0f, fullRotation.eulerAngles.y, 0f); - // Delete the marker. - PeltzerMain.Instance.model.DeleteMesh(CAMERA_ALIGNMENT_MARKER_MESH_ID); + // Delete the marker. + PeltzerMain.Instance.model.DeleteMesh(CAMERA_ALIGNMENT_MARKER_MESH_ID); - // Also adjust the world's translation such that the camera coincides with the marker. - Vector3 markerWorldPos = PeltzerMain.Instance.worldSpace.ModelToWorld(markerPos); - PeltzerMain.Instance.worldSpace.offset = anchorPosition - markerWorldPos; - PeltzerMain.Instance.worldSpace.scale = 0.3f; - } + // Also adjust the world's translation such that the camera coincides with the marker. + Vector3 markerWorldPos = PeltzerMain.Instance.worldSpace.ModelToWorld(markerPos); + PeltzerMain.Instance.worldSpace.offset = anchorPosition - markerWorldPos; + PeltzerMain.Instance.worldSpace.scale = 0.3f; + } - /// - /// Find the tip of the given cone mesh. - /// The tip is defined as the vertex with the highest degree (vertex that belongs to most faces). - /// - /// The cone whose tip is to be found. - /// The vertex ID of the tip of the cone. - private static int FindTipOfCone(MMesh cone) { - // The tip of the cone is the vertex with highest degree (belongs to the most faces). - Dictionary degreeOf = new Dictionary(); - int winner = -1; - foreach (Face face in cone.GetFaces()) { - foreach (int vertexId in face.vertexIds) { - int currentValue; - degreeOf[vertexId] = degreeOf.TryGetValue(vertexId, out currentValue) ? - currentValue + 1 : 1; - if (winner < 0 || degreeOf[vertexId] > degreeOf[winner]) { - winner = vertexId; - } + /// + /// Find the tip of the given cone mesh. + /// The tip is defined as the vertex with the highest degree (vertex that belongs to most faces). + /// + /// The cone whose tip is to be found. + /// The vertex ID of the tip of the cone. + private static int FindTipOfCone(MMesh cone) + { + // The tip of the cone is the vertex with highest degree (belongs to the most faces). + Dictionary degreeOf = new Dictionary(); + int winner = -1; + foreach (Face face in cone.GetFaces()) + { + foreach (int vertexId in face.vertexIds) + { + int currentValue; + degreeOf[vertexId] = degreeOf.TryGetValue(vertexId, out currentValue) ? + currentValue + 1 : 1; + if (winner < 0 || degreeOf[vertexId] > degreeOf[winner]) + { + winner = vertexId; + } + } + } + AssertOrThrow.True(winner >= 0, "Could not find tip of cone in mesh."); + return winner; } - } - AssertOrThrow.True(winner >= 0, "Could not find tip of cone in mesh."); - return winner; } - } } diff --git a/Assets/Scripts/video/MoveableVideoViewer.cs b/Assets/Scripts/video/MoveableVideoViewer.cs index 010abc5e..25a001f7 100644 --- a/Assets/Scripts/video/MoveableVideoViewer.cs +++ b/Assets/Scripts/video/MoveableVideoViewer.cs @@ -18,45 +18,50 @@ using com.google.apps.peltzer.client.model.main; using com.google.apps.peltzer.client.tools; -namespace com.google.apps.peltzer.video { - /// - /// The video viewer is a mesh with a texture that allows movies (videos) to be played. This script allows the - /// viewer to be moved, and thrown with the grab tool, or 'deleted' with the delete tool, without actually - /// being a part of the Model. Note that we never actually delete the video viewer: exactly one viewer exists in - /// the scene at all times, and is hidden rather than deleted. The video viewer cannot be scaled, to avoid - /// distorting the movie texture. - /// - /// Operations on the video viewer are included in the undo/redo stack. - /// - public class MoveableVideoViewer : MoveableObject { - public override void Setup() { - base.Setup(); - - mesh = gameObject.GetComponent().mesh; - material = gameObject.GetComponent().material; - - WorldSpace worldSpace = PeltzerMain.Instance.worldSpace; - positionModelSpace = worldSpace.WorldToModel(transform.position); - rotationModelSpace = worldSpace.WorldOrientationToModel(transform.rotation); - RecalculateVerticesAndNormal(); - } +namespace com.google.apps.peltzer.video +{ + /// + /// The video viewer is a mesh with a texture that allows movies (videos) to be played. This script allows the + /// viewer to be moved, and thrown with the grab tool, or 'deleted' with the delete tool, without actually + /// being a part of the Model. Note that we never actually delete the video viewer: exactly one viewer exists in + /// the scene at all times, and is hidden rather than deleted. The video viewer cannot be scaled, to avoid + /// distorting the movie texture. + /// + /// Operations on the video viewer are included in the undo/redo stack. + /// + public class MoveableVideoViewer : MoveableObject + { + public override void Setup() + { + base.Setup(); - internal override void Delete() { - base.Delete(); + mesh = gameObject.GetComponent().mesh; + material = gameObject.GetComponent().material; - PeltzerMain.Instance.GetModel().ApplyCommand(new HideVideoViewerCommand()); - } + WorldSpace worldSpace = PeltzerMain.Instance.worldSpace; + positionModelSpace = worldSpace.WorldToModel(transform.position); + rotationModelSpace = worldSpace.WorldOrientationToModel(transform.rotation); + RecalculateVerticesAndNormal(); + } + + internal override void Delete() + { + base.Delete(); + + PeltzerMain.Instance.GetModel().ApplyCommand(new HideVideoViewerCommand()); + } - internal override void Release() { - base.Release(); + internal override void Release() + { + base.Release(); - // Force an update to get the latest position and rotation. - UpdatePosition(); + // Force an update to get the latest position and rotation. + UpdatePosition(); - // Move the viewer via a command. - Vector3 positionDelta = positionModelSpace - positionAtStartOfMove; - Quaternion rotDelta = Quaternion.Inverse(rotationAtStartOfMove) * rotationModelSpace; - PeltzerMain.Instance.GetModel().ApplyCommand(new MoveVideoViewerCommand(positionDelta, rotDelta)); + // Move the viewer via a command. + Vector3 positionDelta = positionModelSpace - positionAtStartOfMove; + Quaternion rotDelta = Quaternion.Inverse(rotationAtStartOfMove) * rotationModelSpace; + PeltzerMain.Instance.GetModel().ApplyCommand(new MoveVideoViewerCommand(positionDelta, rotDelta)); + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/zandria/ZandriaCreationHandler.cs b/Assets/Scripts/zandria/ZandriaCreationHandler.cs index 1b93b12b..7441988f 100644 --- a/Assets/Scripts/zandria/ZandriaCreationHandler.cs +++ b/Assets/Scripts/zandria/ZandriaCreationHandler.cs @@ -20,82 +20,92 @@ using com.google.apps.peltzer.client.model.export; using com.google.apps.peltzer.client.tools; -namespace com.google.apps.peltzer.client.zandria { - public class ZandriaCreationHandler : MonoBehaviour { - // This is in Unity units where 1.0 = 1m. - private const float MENU_TILE_SIZE = 0.05f; +namespace com.google.apps.peltzer.client.zandria +{ + public class ZandriaCreationHandler : MonoBehaviour + { + // This is in Unity units where 1.0 = 1m. + private const float MENU_TILE_SIZE = 0.05f; - public List originalMeshes { get; private set; } - public List previewMeshes { get; private set; } - public List detailSizedMeshes { get; set; } - public PeltzerFile peltzerFile { get; private set; } - public string creatorName { get; private set; } - public string creationDate { get; private set; } - public string creationTitle { get; private set; } - public string creationAssetId { get; private set; } - public string creationLocalId { get; private set; } - public bool isActiveOnMenu { get; set; } - public bool hasPublishedRotation { get; set; } - public float recommendedRotation { get; private set; } + public List originalMeshes { get; private set; } + public List previewMeshes { get; private set; } + public List detailSizedMeshes { get; set; } + public PeltzerFile peltzerFile { get; private set; } + public string creatorName { get; private set; } + public string creationDate { get; private set; } + public string creationTitle { get; private set; } + public string creationAssetId { get; private set; } + public string creationLocalId { get; private set; } + public bool isActiveOnMenu { get; set; } + public bool hasPublishedRotation { get; set; } + public float recommendedRotation { get; private set; } - public void Setup(ObjectStoreEntry objectStoreEntry) { - previewMeshes = new List(); - originalMeshes = new List(); - detailSizedMeshes = new List(); + public void Setup(ObjectStoreEntry objectStoreEntry) + { + previewMeshes = new List(); + originalMeshes = new List(); + detailSizedMeshes = new List(); - creatorName = objectStoreEntry.author; - creationDate = objectStoreEntry.createdDate.ToString(); - creationTitle = objectStoreEntry.title; - creationAssetId = objectStoreEntry.id; - creationLocalId = objectStoreEntry.localId; - // If the model was published and the camera forward is available, rotate the model about the - // y-axis so it faces the camera forward when positioned on the Poly menu. - if (objectStoreEntry.cameraForward != null && objectStoreEntry.cameraForward != Vector3.zero) { - Vector3 cameraForward = objectStoreEntry.cameraForward; - Quaternion publishedRotationQuaternion = Quaternion.LookRotation(cameraForward); - recommendedRotation = publishedRotationQuaternion.eulerAngles.y; - hasPublishedRotation = true; - } else { - hasPublishedRotation = false; - recommendedRotation = 0f; - } - } + creatorName = objectStoreEntry.author; + creationDate = objectStoreEntry.createdDate.ToString(); + creationTitle = objectStoreEntry.title; + creationAssetId = objectStoreEntry.id; + creationLocalId = objectStoreEntry.localId; + // If the model was published and the camera forward is available, rotate the model about the + // y-axis so it faces the camera forward when positioned on the Poly menu. + if (objectStoreEntry.cameraForward != null && objectStoreEntry.cameraForward != Vector3.zero) + { + Vector3 cameraForward = objectStoreEntry.cameraForward; + Quaternion publishedRotationQuaternion = Quaternion.LookRotation(cameraForward); + recommendedRotation = publishedRotationQuaternion.eulerAngles.y; + hasPublishedRotation = true; + } + else + { + hasPublishedRotation = false; + recommendedRotation = 0f; + } + } - /// - /// Takes the raw data for a PeltzerFile and converts it to MMeshes scaled to fit on the menu. - /// - /// The raw file data. - /// Callback function on successful retrieval. - /// Whether the file was valid. - public bool GetMMeshesFromPeltzerFile(byte[] rawFileData, System.Action, float> callback) { - PeltzerFile peltzerFile; - bool validFile = PeltzerFileHandler.PeltzerFileFromBytes(rawFileData, out peltzerFile); + /// + /// Takes the raw data for a PeltzerFile and converts it to MMeshes scaled to fit on the menu. + /// + /// The raw file data. + /// Callback function on successful retrieval. + /// Whether the file was valid. + public bool GetMMeshesFromPeltzerFile(byte[] rawFileData, System.Action, float> callback) + { + PeltzerFile peltzerFile; + bool validFile = PeltzerFileHandler.PeltzerFileFromBytes(rawFileData, out peltzerFile); - if (validFile) { - // Keep a reference to the peltzerFile so that it can be loaded into the model. - this.peltzerFile = peltzerFile; + if (validFile) + { + // Keep a reference to the peltzerFile so that it can be loaded into the model. + this.peltzerFile = peltzerFile; - // Keep a reference to the original meshes so that they can be loaded into a scene at full scale. - originalMeshes = peltzerFile.meshes; - // Keep a reference to the meshes that have been scaled to be previews on the PolyMenu so they can - // be loaded into the scene at this size without re-scaling. - previewMeshes = Scaler.ScaleMeshes(originalMeshes, MENU_TILE_SIZE); + // Keep a reference to the original meshes so that they can be loaded into a scene at full scale. + originalMeshes = peltzerFile.meshes; + // Keep a reference to the meshes that have been scaled to be previews on the PolyMenu so they can + // be loaded into the scene at this size without re-scaling. + previewMeshes = Scaler.ScaleMeshes(originalMeshes, MENU_TILE_SIZE); - // If there was not a published rotation, recommend the rotation the model was saved with (if available). - if (!hasPublishedRotation) { - recommendedRotation = peltzerFile.metadata.recommendedRotation; - } + // If there was not a published rotation, recommend the rotation the model was saved with (if available). + if (!hasPublishedRotation) + { + recommendedRotation = peltzerFile.metadata.recommendedRotation; + } - // Returns the scaled MMeshes with a recommended display rotation. - callback(previewMeshes, recommendedRotation); + // Returns the scaled MMeshes with a recommended display rotation. + callback(previewMeshes, recommendedRotation); - return true; - } - else { - Debug.LogError("Invalid file with asset id " + creationAssetId + " and local id " + creationLocalId); - } + return true; + } + else + { + Debug.LogError("Invalid file with asset id " + creationAssetId + " and local id " + creationLocalId); + } - return false; + return false; + } } - } } \ No newline at end of file diff --git a/Assets/Scripts/zandria/ZandriaCreationsManager.cs b/Assets/Scripts/zandria/ZandriaCreationsManager.cs index e8570fa2..6d74e8e0 100644 --- a/Assets/Scripts/zandria/ZandriaCreationsManager.cs +++ b/Assets/Scripts/zandria/ZandriaCreationsManager.cs @@ -31,1045 +31,1187 @@ using System.IO; using com.google.apps.peltzer.client.entitlement; -namespace com.google.apps.peltzer.client.zandria { - public class Creation { - public int index; - /// - /// The entry for this creation. The entry will change if an existing entry fails to load. - /// - public Entry entry; - /// - /// The preview displayed on the PolyMenu. This is instantiated from a creation prefab. - /// - public GameObject preview; - /// - /// The thumbnail game object. We keep a reference to the game object and not just the sprite so we can easily - /// activate and deactivate it. - /// - public GameObject thumbnail; - /// - /// The thumbnail to show when the creation fails to load. - /// - public GameObject errorThumbnail; - /// - /// The actual sprite for the thumbnail. - /// - public Sprite thumbnailSprite; - /// - /// The handler script for the creation that handles converting queries into usuable information. - /// - public ZandriaCreationHandler handler; - /// - /// Whether this is a locally-available creation, rather than a cloud-saved creation. - /// - public bool isLocal; - /// - /// Whether this is a creation that was saved in this session. - /// - public bool isSave; - - public Creation(int index, Entry entry, GameObject creationPrefab, bool isLocal, bool isSave) { - this.index = index; - this.entry = entry; - preview = creationPrefab; - thumbnail = creationPrefab.transform.Find("CreationPreview").gameObject.transform.Find("Thumbnail").gameObject; - errorThumbnail = - creationPrefab.transform.Find("CreationPreview").gameObject.transform.Find("Error_Thumbnail").gameObject; - errorThumbnail.SetActive(false); - preview.GetComponent().creation = this; - handler = preview.GetComponent(); - this.isLocal = isLocal; - this.isSave = isSave; - } - - internal void SetThumbnailSprite(Sprite thumbnailSprite) { - this.thumbnailSprite = thumbnailSprite; - thumbnail.GetComponent().sprite = thumbnailSprite; - } - } - - public class Entry { - /// - /// The actual entry that is used to query the store for the model. - /// - public ObjectStoreEntry queryEntry; - /// - /// A reference to the current load status of this entry. The status is used to indicate whether we have ever - /// tried to load this entry so we can find a "fresh" entry to load when we need a new creation. - /// - public ZandriaCreationsManager.LoadStatus loadStatus; +namespace com.google.apps.peltzer.client.zandria +{ + public class Creation + { + public int index; + /// + /// The entry for this creation. The entry will change if an existing entry fails to load. + /// + public Entry entry; + /// + /// The preview displayed on the PolyMenu. This is instantiated from a creation prefab. + /// + public GameObject preview; + /// + /// The thumbnail game object. We keep a reference to the game object and not just the sprite so we can easily + /// activate and deactivate it. + /// + public GameObject thumbnail; + /// + /// The thumbnail to show when the creation fails to load. + /// + public GameObject errorThumbnail; + /// + /// The actual sprite for the thumbnail. + /// + public Sprite thumbnailSprite; + /// + /// The handler script for the creation that handles converting queries into usuable information. + /// + public ZandriaCreationHandler handler; + /// + /// Whether this is a locally-available creation, rather than a cloud-saved creation. + /// + public bool isLocal; + /// + /// Whether this is a creation that was saved in this session. + /// + public bool isSave; + + public Creation(int index, Entry entry, GameObject creationPrefab, bool isLocal, bool isSave) + { + this.index = index; + this.entry = entry; + preview = creationPrefab; + thumbnail = creationPrefab.transform.Find("CreationPreview").gameObject.transform.Find("Thumbnail").gameObject; + errorThumbnail = + creationPrefab.transform.Find("CreationPreview").gameObject.transform.Find("Error_Thumbnail").gameObject; + errorThumbnail.SetActive(false); + preview.GetComponent().creation = this; + handler = preview.GetComponent(); + this.isLocal = isLocal; + this.isSave = isSave; + } - public Entry(ObjectStoreEntry queryEntry) { - this.queryEntry = queryEntry; - // On creation set loadStatus to be None. This will indicate that the entry has never been loaded. - loadStatus = ZandriaCreationsManager.LoadStatus.NONE; + internal void SetThumbnailSprite(Sprite thumbnailSprite) + { + this.thumbnailSprite = thumbnailSprite; + thumbnail.GetComponent().sprite = thumbnailSprite; + } } - } - public class Load { - /// - /// A reference to the list of creation entry metadata for entries waiting to be loaded. This list contains - /// the metadata for every creation we can load and is the starting point for loading creations onto the - /// PolyMenu. As we attempt to load entries they are removed from this list and attached to a creation. - /// - public List entries; - /// - /// The creations for this load. These are in chronological order. - /// - public List creations; - /// - /// The prefab for a creation that has the correct scripts and size to be attached to the PolyMenu. - /// - public GameObject creationPrefab; - /// - /// A reference to the load requests we should make during the next Update loop. The index refers to the index of - /// a creation in creations that needs to be loaded. - /// - /// Requests are only appended to the list when: An initial call to StartLoad() is called where we make - /// NUMBER_OF_CREATIONS_PER_PAGE load requests, an existing load request fails and needs to replace itself, there - /// aren't enough loaded previews for a next page request but there are still entries whose status is - /// LoadStatus.NONE. - /// - public List pendingModelLoadRequestIndices; - public List pendingThumbnailLoadRequestIndices; - /// - /// A reference to the number of root load requests made for a given type. A root load request is an intial - /// request made at the start of a load or because of a page request. A load request which is generated as a - /// resulting load failure does not count as a root load. totalRootLoads tells us how many pages we are currently - /// trying to actively populate. - /// - public int totalRootLoadRequests; - /// - /// The total number of entries queried for this load. - /// - public int totalNumberOfEntries; - /// - /// The number of pages we are actively trying to load, we might not actually have enough models to fill all these - /// pages. - /// - public int activePageCount; - /// - /// The total number of pages for this load. This is either the max number of pages allowed or the number of pages - /// that can be filled given the number of creations for this load. - /// - public int numberOfPages; - - public Load(GameObject creationPrefab) { - entries = new List(); - totalNumberOfEntries = 0; - this.creationPrefab = creationPrefab; - creations = new List(); - pendingModelLoadRequestIndices = new List(); - pendingThumbnailLoadRequestIndices = new List(); - totalRootLoadRequests = 0; - numberOfPages = 0; - activePageCount = - ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE * ZandriaCreationsManager.NUMBER_OF_PAGES_AT_START; + public class Entry + { + /// + /// The actual entry that is used to query the store for the model. + /// + public ObjectStoreEntry queryEntry; + /// + /// A reference to the current load status of this entry. The status is used to indicate whether we have ever + /// tried to load this entry so we can find a "fresh" entry to load when we need a new creation. + /// + public ZandriaCreationsManager.LoadStatus loadStatus; + + public Entry(ObjectStoreEntry queryEntry) + { + this.queryEntry = queryEntry; + // On creation set loadStatus to be None. This will indicate that the entry has never been loaded. + loadStatus = ZandriaCreationsManager.LoadStatus.NONE; + } } - /// - /// Takes an entry add forces it to the front of the menu. This is typically used when a user saves and we want - /// to maintain the chronological ordering of the menu. - /// - /// The entry to be parsed into a creation and loaded onto the menu. - public void AddEntryToStartOfMenu(Entry entry, bool isLocal, bool isSave) { - // Update the total number of entries for this load. - totalNumberOfEntries += 1; - - // Setup the creation. - // Create a copy of the prefab for a creation that has the correct size and scripts to be attached to the menu. - GameObject zandriaCreationHolder = GameObject.Instantiate(creationPrefab); - // Hide the creation until it's attached to the menu. - zandriaCreationHolder.SetActive(false); - // Create a creation from the prefab and the next entry to be loaded. - creations.Insert(0, (new Creation(0, entry, zandriaCreationHolder, isLocal, isSave))); - // The following collections index into creations. Given that we've prepended to creations, we must increment - // each index into creations. - for (int i = 0; i < pendingModelLoadRequestIndices.Count; i++) { - pendingModelLoadRequestIndices[i] = pendingModelLoadRequestIndices[i] + 1; - } - for (int i = 0; i < pendingThumbnailLoadRequestIndices.Count; i++) { - pendingThumbnailLoadRequestIndices[i] = pendingThumbnailLoadRequestIndices[i] + 1; - } - - int maxCreations = - ZandriaCreationsManager.MAX_NUMBER_OF_PAGES * ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE; - - // We are going to load this creation no matter what to the front of the list. But that might put the number of - // creations over the limit and we have to cut one from the end of the list. - if (creations.Count() > maxCreations) { - int removedIndex = creations.Count() - 1; - Creation removedCreation = creations.Last(); - - // Remove the load from pending loads, this will silently fail if its not in there. - pendingModelLoadRequestIndices.Remove(removedIndex); - pendingThumbnailLoadRequestIndices.Remove(removedIndex); - - // The worst case is when the creation is in the middle of being loaded. If this is the case destroying the - // gameObject won't be threadsafe. - // TODO (bug): Destroy previews when a creation is removed in a threadsafe way by marking creations for - // deletion and then deleting them on the main thread once they finish loading. - if (removedCreation.entry.loadStatus == ZandriaCreationsManager.LoadStatus.LOADING_MODEL) { - // Set the actual preview inactive. - removedCreation.preview.SetActive(false); - // Remove any markers in the handler that this preview should be active once it's done loading. - removedCreation.handler.isActiveOnMenu = false; - } else { - // It is not loading, we can destroy it in a threadsafe way. - GameObject.Destroy(removedCreation.preview); + public class Load + { + /// + /// A reference to the list of creation entry metadata for entries waiting to be loaded. This list contains + /// the metadata for every creation we can load and is the starting point for loading creations onto the + /// PolyMenu. As we attempt to load entries they are removed from this list and attached to a creation. + /// + public List entries; + /// + /// The creations for this load. These are in chronological order. + /// + public List creations; + /// + /// The prefab for a creation that has the correct scripts and size to be attached to the PolyMenu. + /// + public GameObject creationPrefab; + /// + /// A reference to the load requests we should make during the next Update loop. The index refers to the index of + /// a creation in creations that needs to be loaded. + /// + /// Requests are only appended to the list when: An initial call to StartLoad() is called where we make + /// NUMBER_OF_CREATIONS_PER_PAGE load requests, an existing load request fails and needs to replace itself, there + /// aren't enough loaded previews for a next page request but there are still entries whose status is + /// LoadStatus.NONE. + /// + public List pendingModelLoadRequestIndices; + public List pendingThumbnailLoadRequestIndices; + /// + /// A reference to the number of root load requests made for a given type. A root load request is an intial + /// request made at the start of a load or because of a page request. A load request which is generated as a + /// resulting load failure does not count as a root load. totalRootLoads tells us how many pages we are currently + /// trying to actively populate. + /// + public int totalRootLoadRequests; + /// + /// The total number of entries queried for this load. + /// + public int totalNumberOfEntries; + /// + /// The number of pages we are actively trying to load, we might not actually have enough models to fill all these + /// pages. + /// + public int activePageCount; + /// + /// The total number of pages for this load. This is either the max number of pages allowed or the number of pages + /// that can be filled given the number of creations for this load. + /// + public int numberOfPages; + + public Load(GameObject creationPrefab) + { + entries = new List(); + totalNumberOfEntries = 0; + this.creationPrefab = creationPrefab; + creations = new List(); + pendingModelLoadRequestIndices = new List(); + pendingThumbnailLoadRequestIndices = new List(); + totalRootLoadRequests = 0; + numberOfPages = 0; + activePageCount = + ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE * ZandriaCreationsManager.NUMBER_OF_PAGES_AT_START; } - // We only ever add one to the front so we can just remove one from the end. - creations.RemoveAt(creations.Count() - 1); - totalRootLoadRequests -= 1; - } - - // Mark the creation to be loaded. - pendingThumbnailLoadRequestIndices.Add(0); - // Increment totalRootLoadRequests. - totalRootLoadRequests += 1; + /// + /// Takes an entry add forces it to the front of the menu. This is typically used when a user saves and we want + /// to maintain the chronological ordering of the menu. + /// + /// The entry to be parsed into a creation and loaded onto the menu. + public void AddEntryToStartOfMenu(Entry entry, bool isLocal, bool isSave) + { + // Update the total number of entries for this load. + totalNumberOfEntries += 1; + + // Setup the creation. + // Create a copy of the prefab for a creation that has the correct size and scripts to be attached to the menu. + GameObject zandriaCreationHolder = GameObject.Instantiate(creationPrefab); + // Hide the creation until it's attached to the menu. + zandriaCreationHolder.SetActive(false); + // Create a creation from the prefab and the next entry to be loaded. + creations.Insert(0, (new Creation(0, entry, zandriaCreationHolder, isLocal, isSave))); + // The following collections index into creations. Given that we've prepended to creations, we must increment + // each index into creations. + for (int i = 0; i < pendingModelLoadRequestIndices.Count; i++) + { + pendingModelLoadRequestIndices[i] = pendingModelLoadRequestIndices[i] + 1; + } + for (int i = 0; i < pendingThumbnailLoadRequestIndices.Count; i++) + { + pendingThumbnailLoadRequestIndices[i] = pendingThumbnailLoadRequestIndices[i] + 1; + } - int totalPossiblePages = - (int)Mathf.Ceil((float)totalNumberOfEntries / ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE); - numberOfPages = Math.Min(ZandriaCreationsManager.MAX_NUMBER_OF_PAGES, totalPossiblePages); - } + int maxCreations = + ZandriaCreationsManager.MAX_NUMBER_OF_PAGES * ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE; + + // We are going to load this creation no matter what to the front of the list. But that might put the number of + // creations over the limit and we have to cut one from the end of the list. + if (creations.Count() > maxCreations) + { + int removedIndex = creations.Count() - 1; + Creation removedCreation = creations.Last(); + + // Remove the load from pending loads, this will silently fail if its not in there. + pendingModelLoadRequestIndices.Remove(removedIndex); + pendingThumbnailLoadRequestIndices.Remove(removedIndex); + + // The worst case is when the creation is in the middle of being loaded. If this is the case destroying the + // gameObject won't be threadsafe. + // TODO (bug): Destroy previews when a creation is removed in a threadsafe way by marking creations for + // deletion and then deleting them on the main thread once they finish loading. + if (removedCreation.entry.loadStatus == ZandriaCreationsManager.LoadStatus.LOADING_MODEL) + { + // Set the actual preview inactive. + removedCreation.preview.SetActive(false); + // Remove any markers in the handler that this preview should be active once it's done loading. + removedCreation.handler.isActiveOnMenu = false; + } + else + { + // It is not loading, we can destroy it in a threadsafe way. + GameObject.Destroy(removedCreation.preview); + } + + // We only ever add one to the front so we can just remove one from the end. + creations.RemoveAt(creations.Count() - 1); + totalRootLoadRequests -= 1; + } - /// - /// Takes a list of entries and adds them behind any existing entries in the menu. This is typically used for bulk - /// calls to Zandria where the entries are already returned in chronological order. Most of the time there won't - /// be any existing creations when this method is called, but it's designed to act like there is to handle the case - /// where the bulk call has started, the user quickly saves a model and the call to load that model onto the menu - /// is faster than the call to get all the other models. - /// - /// The entries to be parsed into creations and loaded onto the menu. - public void AddEntriesToEndOfMenu(List entries, bool isLocal, bool isSave) { - // Append all of the given entries to the existing entries. - this.entries.AddRange(entries); - totalNumberOfEntries += entries.Count(); - - // Determine new number of max creations. This is either still the max allowed if the menu was already full or - // the new entry count. - int maxCreations = - Mathf.Min(ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE * ZandriaCreationsManager.MAX_NUMBER_OF_PAGES, - totalNumberOfEntries); - - // Determine how many creations we are going to add from the list of entries. - int numCreationsToAdd = maxCreations - creations.Count(); - int startIndex = creations.Count(); - - // Instatiate a creation for the number of creations we want to load onto the menu. - for (int i = 0; i < numCreationsToAdd; i++) { - // Create a copy of the prefab for a creation that has the correct size and scripts to be attached to the menu. - GameObject zandriaCreationHolder = GameObject.Instantiate(creationPrefab); - // Hide the creation until it's attached to the menu. - zandriaCreationHolder.SetActive(false); - // Create a creation from the prefab and the next entry to be loaded. - creations.Add(new Creation(startIndex + i, entries.First(), zandriaCreationHolder, isLocal, isSave)); - - // Remove the entry from the list of pending entries now that it's associated with a creation. - entries.RemoveAt(0); - } - - // Determine how many creations we want to currently be trying to load onto the menu. This isn't necessarily the - // max number of creations the menu can hold, but the max number given the number of pages we are actively trying - // to load. - int maxActiveCreations = - Mathf.Min(activePageCount * ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE, creations.Count()); - - // The number of load requests to make is equal to the difference between the number of requests we have made and - // the max number of requests we can make. - int numLoadRequests = maxActiveCreations - totalRootLoadRequests; - pendingThumbnailLoadRequestIndices.AddRange(Enumerable.Range(startIndex, numLoadRequests)); - totalRootLoadRequests += numLoadRequests; - - int totalPossiblePages = - (int)Mathf.Ceil((float)totalNumberOfEntries / ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE); - numberOfPages = Math.Min(ZandriaCreationsManager.MAX_NUMBER_OF_PAGES, totalPossiblePages); - } - } - - /// - /// Loads and holds references to all the Zandria creations on the in VR Zandria Menu. - /// - public class ZandriaCreationsManager : MonoBehaviour { - // The loading status of an entry. - // NONE: The entry exists but we have never tried to load it. - // LOADING_THUMBNAIL: A load has been started for this entry's thumbnail. - // LOADING_MODEL: A load has been started for this entry's model. - // FAILED: At some point in the loading pipeline this load failed and the pipeline was terminated. - // SUCCESSFUL: This load is finished and a preview of the creation now exists in previewsByType and can be attached - // to the PolyMenu. - public enum LoadStatus { NONE, LOADING_THUMBNAIL, LOADING_MODEL, FAILED, SUCCESSFUL } - public const int NUMBER_OF_CREATIONS_PER_PAGE = 9; - public const int MAX_NUMBER_OF_PAGES = 10; - public const int NUMBER_OF_PAGES_AT_START = 2; - // The PPU for imported thumbnails from Zandria that will be displayed on the menu. Chosen by eyeballing it. - // More positive numbers will give smaller thumbnails, and vice-versa. - private const int THUMBNAIL_IMPORT_PIXELS_PER_UNIT = 300; - - // The number of different types of creations. Currently we are support: Your models, featured, liked. - private const int NUMBER_OF_CREATION_TYPES = 3; - - // We implement polling for the "Featured" and "Liked" sections in order to show any - // new models that get featured or liked by the user while Blocks is running. - private const float POLLING_INTERVAL_SECONDS = 8; - - // WARNING: All dictionaries in ZandriaCreationsManager are private because they are not threadsafe. They must be - // accessed from within ZandriaCreationsManager and they must be locked before access. - // WARNING AGAIN: DON'T EVEN THINK ABOUT TOUCHING THESE ON A BACKGROUND THREAD OR IN ANYWAY THAT IS NOT THREADSAFE. - private Dictionary loadsByType; - // A reference to load requests that have been made but don't exist in loadsByType until the initial query to get - // all the entries are complete. We'll use this to manage the state of the PolyMenu until the initial query is - // complete. - private HashSet pendingLoadsByType; - - private GameObject creationPrefab; - private readonly object mutex = new object(); - private WorldSpace identityWorldSpace; - - // When we last polled for updates to the menu. - private float timeLastPolled; - - public AssetsServiceClient assetsServiceClient; - - public void Setup() { - assetsServiceClient = gameObject.AddComponent(); - identityWorldSpace = new WorldSpace(PeltzerMain.DEFAULT_BOUNDS); - - lock (mutex) { - // Load an instance of the ZandriaCreationHolder prefab. This will be used to attach each creation to. - creationPrefab = (GameObject)Resources.Load("Prefabs/ZandriaCreationHolder"); - - loadsByType = new Dictionary(NUMBER_OF_CREATION_TYPES); - pendingLoadsByType = new HashSet(); - - StartLoad(PolyMenuMain.CreationType.FEATURED); - LoadOfflineModels(); - } - } + // Mark the creation to be loaded. + pendingThumbnailLoadRequestIndices.Add(0); + // Increment totalRootLoadRequests. + totalRootLoadRequests += 1; - void Update() { - lock (mutex) { - // Queue thumbnails for loading. - foreach (KeyValuePair pair in loadsByType) { - for (int j = 0; j < pair.Value.pendingThumbnailLoadRequestIndices.Count(); j++) { - int loadIndex = pair.Value.pendingThumbnailLoadRequestIndices[j]; - // Find the creation we are trying to load. - if (loadIndex >= pair.Value.creations.Count) { continue; } // Preventing bug - Creation creation = pair.Value.creations[loadIndex]; - - // Update the progress of the load. - creation.entry.loadStatus = LoadStatus.LOADING_THUMBNAIL; - // Execute the load. - StartCoroutine(LoadThumbnailForCreation(creation, pair.Key, pair.Value, loadIndex)); - } - - // Clear pendingThumbnailLoadRequestIndices. We have made a load request for every pending request. - pair.Value.pendingThumbnailLoadRequestIndices.Clear(); + int totalPossiblePages = + (int)Mathf.Ceil((float)totalNumberOfEntries / ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE); + numberOfPages = Math.Min(ZandriaCreationsManager.MAX_NUMBER_OF_PAGES, totalPossiblePages); } - foreach (KeyValuePair pair in loadsByType) { - for (int j = 0; j < pair.Value.pendingModelLoadRequestIndices.Count(); j++) { - int loadIndex = pair.Value.pendingModelLoadRequestIndices[j]; - // Find the creation we are trying to load. - if (loadIndex >= pair.Value.creations.Count) { continue; } // Preventing bug - Creation creation = pair.Value.creations[loadIndex]; - - // Update the progress of the load. - creation.entry.loadStatus = LoadStatus.LOADING_MODEL; - // Execute the load. - LoadModelForCreation(creation, pair.Key); - } - - // Clear pendingModelLoadRequestIndices. We have made a load request for every pending request. - pair.Value.pendingModelLoadRequestIndices.Clear(); - } - } - - // Poll for new featured or liked models periodically if the menu is open. - // Note: we don't poll the "Your models" section because (1) it's harder to optimize (it's not ordered - // by modified time) and (2) that flow is already covered in an ad-hoc way: we update the poly menu - // manually when the user saves a model. - if (PeltzerMain.Instance.polyMenuMain.PolyMenuIsActive() && Time.time - timeLastPolled > POLLING_INTERVAL_SECONDS) { - if (loadsByType.ContainsKey(PolyMenuMain.CreationType.FEATURED)) { - Poll(PolyMenuMain.CreationType.FEATURED); - } - if (loadsByType.ContainsKey(PolyMenuMain.CreationType.LIKED) && OAuth2Identity.Instance.LoggedIn) { - Poll(PolyMenuMain.CreationType.LIKED); + /// + /// Takes a list of entries and adds them behind any existing entries in the menu. This is typically used for bulk + /// calls to Zandria where the entries are already returned in chronological order. Most of the time there won't + /// be any existing creations when this method is called, but it's designed to act like there is to handle the case + /// where the bulk call has started, the user quickly saves a model and the call to load that model onto the menu + /// is faster than the call to get all the other models. + /// + /// The entries to be parsed into creations and loaded onto the menu. + public void AddEntriesToEndOfMenu(List entries, bool isLocal, bool isSave) + { + // Append all of the given entries to the existing entries. + this.entries.AddRange(entries); + totalNumberOfEntries += entries.Count(); + + // Determine new number of max creations. This is either still the max allowed if the menu was already full or + // the new entry count. + int maxCreations = + Mathf.Min(ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE * ZandriaCreationsManager.MAX_NUMBER_OF_PAGES, + totalNumberOfEntries); + + // Determine how many creations we are going to add from the list of entries. + int numCreationsToAdd = maxCreations - creations.Count(); + int startIndex = creations.Count(); + + // Instatiate a creation for the number of creations we want to load onto the menu. + for (int i = 0; i < numCreationsToAdd; i++) + { + // Create a copy of the prefab for a creation that has the correct size and scripts to be attached to the menu. + GameObject zandriaCreationHolder = GameObject.Instantiate(creationPrefab); + // Hide the creation until it's attached to the menu. + zandriaCreationHolder.SetActive(false); + // Create a creation from the prefab and the next entry to be loaded. + creations.Add(new Creation(startIndex + i, entries.First(), zandriaCreationHolder, isLocal, isSave)); + + // Remove the entry from the list of pending entries now that it's associated with a creation. + entries.RemoveAt(0); + } + + // Determine how many creations we want to currently be trying to load onto the menu. This isn't necessarily the + // max number of creations the menu can hold, but the max number given the number of pages we are actively trying + // to load. + int maxActiveCreations = + Mathf.Min(activePageCount * ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE, creations.Count()); + + // The number of load requests to make is equal to the difference between the number of requests we have made and + // the max number of requests we can make. + int numLoadRequests = maxActiveCreations - totalRootLoadRequests; + pendingThumbnailLoadRequestIndices.AddRange(Enumerable.Range(startIndex, numLoadRequests)); + totalRootLoadRequests += numLoadRequests; + + int totalPossiblePages = + (int)Mathf.Ceil((float)totalNumberOfEntries / ZandriaCreationsManager.NUMBER_OF_CREATIONS_PER_PAGE); + numberOfPages = Math.Min(ZandriaCreationsManager.MAX_NUMBER_OF_PAGES, totalPossiblePages); } - timeLastPolled = Time.time; - } } /// - /// Starts a load for a given creation type by making an API query to get the metadata for all such creations. - /// Then, establishes all the data structures that enable continuous loading and pagination - /// for all the creations of this type. - /// A sibling method, StartSingleCreationLoad, exists below. + /// Loads and holds references to all the Zandria creations on the in VR Zandria Menu. /// - /// The enum type of the load. - public void StartLoad(PolyMenuMain.CreationType type) { - lock (mutex) { - pendingLoadsByType.Add(type); - } - - // Start a coroutine which will create a UnityWebRequest and wait for it to send and return with results. - GetAssetsServiceSearchResults(type, - delegate (ObjectStoreSearchResult objectStoreResults) { - if (objectStoreResults.results.Length == 0) { return; } - - // We've successfully called back with results from the query. Parse them into actual entries. - List entries = new List(); - for (int i = 0; i < objectStoreResults.results.Length; i++) { - entries.Add(new Entry(objectStoreResults.results[i])); + public class ZandriaCreationsManager : MonoBehaviour + { + // The loading status of an entry. + // NONE: The entry exists but we have never tried to load it. + // LOADING_THUMBNAIL: A load has been started for this entry's thumbnail. + // LOADING_MODEL: A load has been started for this entry's model. + // FAILED: At some point in the loading pipeline this load failed and the pipeline was terminated. + // SUCCESSFUL: This load is finished and a preview of the creation now exists in previewsByType and can be attached + // to the PolyMenu. + public enum LoadStatus { NONE, LOADING_THUMBNAIL, LOADING_MODEL, FAILED, SUCCESSFUL } + public const int NUMBER_OF_CREATIONS_PER_PAGE = 9; + public const int MAX_NUMBER_OF_PAGES = 10; + public const int NUMBER_OF_PAGES_AT_START = 2; + // The PPU for imported thumbnails from Zandria that will be displayed on the menu. Chosen by eyeballing it. + // More positive numbers will give smaller thumbnails, and vice-versa. + private const int THUMBNAIL_IMPORT_PIXELS_PER_UNIT = 300; + + // The number of different types of creations. Currently we are support: Your models, featured, liked. + private const int NUMBER_OF_CREATION_TYPES = 3; + + // We implement polling for the "Featured" and "Liked" sections in order to show any + // new models that get featured or liked by the user while Blocks is running. + private const float POLLING_INTERVAL_SECONDS = 8; + + // WARNING: All dictionaries in ZandriaCreationsManager are private because they are not threadsafe. They must be + // accessed from within ZandriaCreationsManager and they must be locked before access. + // WARNING AGAIN: DON'T EVEN THINK ABOUT TOUCHING THESE ON A BACKGROUND THREAD OR IN ANYWAY THAT IS NOT THREADSAFE. + private Dictionary loadsByType; + // A reference to load requests that have been made but don't exist in loadsByType until the initial query to get + // all the entries are complete. We'll use this to manage the state of the PolyMenu until the initial query is + // complete. + private HashSet pendingLoadsByType; + + private GameObject creationPrefab; + private readonly object mutex = new object(); + private WorldSpace identityWorldSpace; + + // When we last polled for updates to the menu. + private float timeLastPolled; + + public AssetsServiceClient assetsServiceClient; + + public void Setup() + { + assetsServiceClient = gameObject.AddComponent(); + identityWorldSpace = new WorldSpace(PeltzerMain.DEFAULT_BOUNDS); + + lock (mutex) + { + // Load an instance of the ZandriaCreationHolder prefab. This will be used to attach each creation to. + creationPrefab = (GameObject)Resources.Load("Prefabs/ZandriaCreationHolder"); + + loadsByType = new Dictionary(NUMBER_OF_CREATION_TYPES); + pendingLoadsByType = new HashSet(); + + StartLoad(PolyMenuMain.CreationType.FEATURED); + LoadOfflineModels(); + } } - lock (mutex) { - Load load; - if (!loadsByType.TryGetValue(type, out load)) { - load = new Load(creationPrefab); - loadsByType.Add(type, load); - } + void Update() + { + lock (mutex) + { + // Queue thumbnails for loading. + foreach (KeyValuePair pair in loadsByType) + { + for (int j = 0; j < pair.Value.pendingThumbnailLoadRequestIndices.Count(); j++) + { + int loadIndex = pair.Value.pendingThumbnailLoadRequestIndices[j]; + // Find the creation we are trying to load. + if (loadIndex >= pair.Value.creations.Count) { continue; } // Preventing bug + Creation creation = pair.Value.creations[loadIndex]; + + // Update the progress of the load. + creation.entry.loadStatus = LoadStatus.LOADING_THUMBNAIL; + // Execute the load. + StartCoroutine(LoadThumbnailForCreation(creation, pair.Key, pair.Value, loadIndex)); + } + + // Clear pendingThumbnailLoadRequestIndices. We have made a load request for every pending request. + pair.Value.pendingThumbnailLoadRequestIndices.Clear(); + } + + foreach (KeyValuePair pair in loadsByType) + { + for (int j = 0; j < pair.Value.pendingModelLoadRequestIndices.Count(); j++) + { + int loadIndex = pair.Value.pendingModelLoadRequestIndices[j]; + // Find the creation we are trying to load. + if (loadIndex >= pair.Value.creations.Count) { continue; } // Preventing bug + Creation creation = pair.Value.creations[loadIndex]; + + // Update the progress of the load. + creation.entry.loadStatus = LoadStatus.LOADING_MODEL; + // Execute the load. + LoadModelForCreation(creation, pair.Key); + } + + // Clear pendingModelLoadRequestIndices. We have made a load request for every pending request. + pair.Value.pendingModelLoadRequestIndices.Clear(); + } + } - load.AddEntriesToEndOfMenu(entries, /* isLocal */ false, /* isSave */ false); + // Poll for new featured or liked models periodically if the menu is open. + // Note: we don't poll the "Your models" section because (1) it's harder to optimize (it's not ordered + // by modified time) and (2) that flow is already covered in an ad-hoc way: we update the poly menu + // manually when the user saves a model. + if (PeltzerMain.Instance.polyMenuMain.PolyMenuIsActive() && Time.time - timeLastPolled > POLLING_INTERVAL_SECONDS) + { + if (loadsByType.ContainsKey(PolyMenuMain.CreationType.FEATURED)) + { + Poll(PolyMenuMain.CreationType.FEATURED); + } + if (loadsByType.ContainsKey(PolyMenuMain.CreationType.LIKED) && OAuth2Identity.Instance.LoggedIn) + { + Poll(PolyMenuMain.CreationType.LIKED); + } + timeLastPolled = Time.time; + } + } - // The load has completed and is now managed in loadsByType so we can remove it from pendingLoads. - pendingLoadsByType.Remove(type); + /// + /// Starts a load for a given creation type by making an API query to get the metadata for all such creations. + /// Then, establishes all the data structures that enable continuous loading and pagination + /// for all the creations of this type. + /// A sibling method, StartSingleCreationLoad, exists below. + /// + /// The enum type of the load. + public void StartLoad(PolyMenuMain.CreationType type) + { + lock (mutex) + { + pendingLoadsByType.Add(type); + } - // Refresh the PolyMenu now that there are creations available. - PeltzerMain.Instance.GetPolyMenuMain().RefreshPolyMenu(); - } - }, - delegate () { - // The query was not successful. - lock (mutex) { - pendingLoadsByType.Remove(type); - PeltzerMain.Instance.GetPolyMenuMain().RefreshPolyMenu(); + // Start a coroutine which will create a UnityWebRequest and wait for it to send and return with results. + GetAssetsServiceSearchResults(type, + delegate (ObjectStoreSearchResult objectStoreResults) + { + if (objectStoreResults.results.Length == 0) { return; } + + // We've successfully called back with results from the query. Parse them into actual entries. + List entries = new List(); + for (int i = 0; i < objectStoreResults.results.Length; i++) + { + entries.Add(new Entry(objectStoreResults.results[i])); + } + + lock (mutex) + { + Load load; + if (!loadsByType.TryGetValue(type, out load)) + { + load = new Load(creationPrefab); + loadsByType.Add(type, load); + } + + load.AddEntriesToEndOfMenu(entries, /* isLocal */ false, /* isSave */ false); + + // The load has completed and is now managed in loadsByType so we can remove it from pendingLoads. + pendingLoadsByType.Remove(type); + + // Refresh the PolyMenu now that there are creations available. + PeltzerMain.Instance.GetPolyMenuMain().RefreshPolyMenu(); + } + }, + delegate () + { + // The query was not successful. + lock (mutex) + { + pendingLoadsByType.Remove(type); + PeltzerMain.Instance.GetPolyMenuMain().RefreshPolyMenu(); + } + }); } - }); - } - public void Poll(PolyMenuMain.CreationType type) { - lock (mutex) { - if (pendingLoadsByType.Contains(type)) { return; } - } - - // Start a coroutine which will create a UnityWebRequest and wait for it to send and return with results. - GetAssetsServiceSearchResults(type, - delegate (ObjectStoreSearchResult objectStoreResults) { - if (objectStoreResults.results.Length == 0) { return; } - - // Success! Load the new models onto the front of the menu. - lock (mutex) { - Load load; - if (!loadsByType.TryGetValue(type, out load)) { - load = new Load(creationPrefab); - loadsByType.Add(type, load); - } - - for (int i = 0; i < objectStoreResults.results.Length; i++) { - load.AddEntryToStartOfMenu(new Entry(objectStoreResults.results[i]), - /* isLocal */ false, /* isSave */ false); - - // The load has completed and is now managed in loadsByType so we can remove it from pendingLoads. - pendingLoadsByType.Remove(type); - } - } - // Refresh the PolyMenu now that there are creations available. - PeltzerMain.Instance.GetPolyMenuMain().RefreshPolyMenu(); - }, - delegate () { - // The query was not successful. Do nothing. - }); - } + public void Poll(PolyMenuMain.CreationType type) + { + lock (mutex) + { + if (pendingLoadsByType.Contains(type)) { return; } + } - /// - /// Load all of the models in the users OfflineModels directory to the PolyMenu. - /// - public void LoadOfflineModels() { - // Most recent first. - try { - DirectoryInfo offlineModelsDirectory = new DirectoryInfo(PeltzerMain.Instance.offlineModelsPath); - if (!offlineModelsDirectory.Exists) return; - List directories = offlineModelsDirectory.GetDirectories().ToList(); - - // Parse them in reverse order such that we add the newest entries to the start of the menu - // after we add older entries to the start of the menu. - for (int i = directories.Count() - 1; i >= 0; i--) { - DirectoryInfo directory = directories[i]; - ObjectStoreEntry entry; - - if (GetObjectStoreEntryFromLocalDirectory(directory, out entry)) { - StartSingleCreationLoad(PolyMenuMain.CreationType.YOUR, entry, /* isLocal */ true, /* isSave */ false); - } + // Start a coroutine which will create a UnityWebRequest and wait for it to send and return with results. + GetAssetsServiceSearchResults(type, + delegate (ObjectStoreSearchResult objectStoreResults) + { + if (objectStoreResults.results.Length == 0) { return; } + + // Success! Load the new models onto the front of the menu. + lock (mutex) + { + Load load; + if (!loadsByType.TryGetValue(type, out load)) + { + load = new Load(creationPrefab); + loadsByType.Add(type, load); + } + + for (int i = 0; i < objectStoreResults.results.Length; i++) + { + load.AddEntryToStartOfMenu(new Entry(objectStoreResults.results[i]), + /* isLocal */ false, /* isSave */ false); + + // The load has completed and is now managed in loadsByType so we can remove it from pendingLoads. + pendingLoadsByType.Remove(type); + } + } + // Refresh the PolyMenu now that there are creations available. + PeltzerMain.Instance.GetPolyMenuMain().RefreshPolyMenu(); + }, + delegate () + { + // The query was not successful. Do nothing. + }); } - } catch (Exception e) { - // We failed to get offline models, the app can continue, but we'll log the issue. - Debug.Log("Failed to get offline models: " + e); - } - } - - /// - /// Delete the given offline model directory. - /// - public void DeleteOfflineModel(string directoryName) { - try { - DirectoryInfo directory = new DirectoryInfo( - Path.Combine(PeltzerMain.Instance.offlineModelsPath, directoryName)); - if (!directory.Exists) return; - directory.Delete(/* recursive */ true); - } catch (Exception) { - // No big harm in a failure. - } - } - public bool GetObjectStoreEntryFromLocalDirectory(DirectoryInfo directory, out ObjectStoreEntry objectStoreEntry) { - objectStoreEntry = new ObjectStoreEntry(); - if (!directory.Exists) return false; + /// + /// Load all of the models in the users OfflineModels directory to the PolyMenu. + /// + public void LoadOfflineModels() + { + // Most recent first. + try + { + DirectoryInfo offlineModelsDirectory = new DirectoryInfo(PeltzerMain.Instance.offlineModelsPath); + if (!offlineModelsDirectory.Exists) return; + List directories = offlineModelsDirectory.GetDirectories().ToList(); + + // Parse them in reverse order such that we add the newest entries to the start of the menu + // after we add older entries to the start of the menu. + for (int i = directories.Count() - 1; i >= 0; i--) + { + DirectoryInfo directory = directories[i]; + ObjectStoreEntry entry; + + if (GetObjectStoreEntryFromLocalDirectory(directory, out entry)) + { + StartSingleCreationLoad(PolyMenuMain.CreationType.YOUR, entry, /* isLocal */ true, /* isSave */ false); + } + } + } + catch (Exception e) + { + // We failed to get offline models, the app can continue, but we'll log the issue. + Debug.Log("Failed to get offline models: " + e); + } + } - try { - FileInfo[] thumbnailFiles = directory.GetFiles("thumbnail.png"); - if (thumbnailFiles.Count() == 0) { - Debug.Log("No thumbnail file found in offline directory " + directory.FullName); - return false; + /// + /// Delete the given offline model directory. + /// + public void DeleteOfflineModel(string directoryName) + { + try + { + DirectoryInfo directory = new DirectoryInfo( + Path.Combine(PeltzerMain.Instance.offlineModelsPath, directoryName)); + if (!directory.Exists) return; + directory.Delete(/* recursive */ true); + } + catch (Exception) + { + // No big harm in a failure. + } } - FileInfo[] blocksFiles = directory.GetFiles("*.blocks"); - if (blocksFiles.Count() == 0) { - Debug.Log("No .blocks file found in offline directory " + directory.FullName); - return false; + public bool GetObjectStoreEntryFromLocalDirectory(DirectoryInfo directory, out ObjectStoreEntry objectStoreEntry) + { + objectStoreEntry = new ObjectStoreEntry(); + if (!directory.Exists) return false; + + try + { + FileInfo[] thumbnailFiles = directory.GetFiles("thumbnail.png"); + if (thumbnailFiles.Count() == 0) + { + Debug.Log("No thumbnail file found in offline directory " + directory.FullName); + return false; + } + + FileInfo[] blocksFiles = directory.GetFiles("*.blocks"); + if (blocksFiles.Count() == 0) + { + Debug.Log("No .blocks file found in offline directory " + directory.FullName); + return false; + } + + objectStoreEntry.localThumbnailFile = thumbnailFiles[0].FullName; + objectStoreEntry.localPeltzerFile = blocksFiles[0].FullName; + objectStoreEntry.localId = directory.Name; + + return true; + } + catch (Exception e) + { + // We failed to get offline models, the app can continue, but we'll log the issue. + Debug.Log("Failed to get offline models from a directory: " + e); + return false; + } } - objectStoreEntry.localThumbnailFile = thumbnailFiles[0].FullName; - objectStoreEntry.localPeltzerFile = blocksFiles[0].FullName; - objectStoreEntry.localId = directory.Name; + /// + /// As above, but for a single creation specified by an object store entry. + /// + public void StartSingleCreationLoad(PolyMenuMain.CreationType type, ObjectStoreEntry objectStoreEntry, + bool isLocal, bool isSave) + { + Entry entry = new Entry(objectStoreEntry); + lock (mutex) + { + pendingLoadsByType.Add(type); + Load load; + // We only hit this case if the load was pending at the start of this call, both the single creation and bulk + // creation load coroutines were operating at the same time but the single creation call completed first. + // In this case the single creation call needs to create the entry in loadByType. + if (!loadsByType.TryGetValue(type, out load)) + { + load = new Load(creationPrefab); + loadsByType.Add(type, load); + } + + load.AddEntryToStartOfMenu(entry, isLocal, isSave); + + // The load has completed and is now managed in loadsByType so we can remove it from pendingLoads. + pendingLoadsByType.Remove(type); + + // Refresh the PolyMenu now that there are creations available. + PeltzerMain.Instance.GetPolyMenuMain().RefreshPolyMenu(); + } + } - return true; - } catch (Exception e) { - // We failed to get offline models, the app can continue, but we'll log the issue. - Debug.Log("Failed to get offline models from a directory: " + e); - return false; - } - } + /// + /// Removes a given creation of the specified type. + /// + public void RemoveSingleCreationAndRefreshMenu(PolyMenuMain.CreationType creationType, string entryIdToRemove) + { + lock (mutex) + { + Load load = loadsByType[creationType]; + List creations = load.creations; + for (int i = 0; i < creations.Count; i++) + { + Creation creation = creations[i]; + if ((creation.isLocal && creation.entry.queryEntry.localId == entryIdToRemove) || + !creation.isLocal && creation.entry.queryEntry.id == entryIdToRemove) + { + creations.RemoveAt(i); + PeltzerMain.Instance.GetPolyMenuMain().RefreshPolyMenu(); + break; + } + } + } + return; + } - /// - /// As above, but for a single creation specified by an object store entry. - /// - public void StartSingleCreationLoad(PolyMenuMain.CreationType type, ObjectStoreEntry objectStoreEntry, - bool isLocal, bool isSave) { - Entry entry = new Entry(objectStoreEntry); - lock (mutex) { - pendingLoadsByType.Add(type); - Load load; - // We only hit this case if the load was pending at the start of this call, both the single creation and bulk - // creation load coroutines were operating at the same time but the single creation call completed first. - // In this case the single creation call needs to create the entry in loadByType. - if (!loadsByType.TryGetValue(type, out load)) { - load = new Load(creationPrefab); - loadsByType.Add(type, load); + /// + /// Updates a single cloud-saved creation on the 'your models' section. + /// + public void UpdateSingleCloudCreationOnYourModels(string asset) + { + PeltzerMain.Instance.DoPolyMenuBackgroundWork(new ParseAssetBackgroundWork(asset, + delegate (ObjectStoreEntry objectStoreEntry) + { + UpdateSingleCreationOnYourModels(objectStoreEntry, /* isLocal */ false, /* isSave */ true); + }, /* hackUrls */ true)); } - load.AddEntryToStartOfMenu(entry, isLocal, isSave); + /// + /// Updates a single locally-saved creation on the 'your models' section. + /// + public void UpdateSingleLocalCreationOnYourModels(DirectoryInfo directory) + { + ObjectStoreEntry objectStoreEntry; + if (GetObjectStoreEntryFromLocalDirectory(directory, out objectStoreEntry)) + { + UpdateSingleCreationOnYourModels(objectStoreEntry, /* isLocal */ true, /* isSave */ true); + } + } - // The load has completed and is now managed in loadsByType so we can remove it from pendingLoads. - pendingLoadsByType.Remove(type); + /// + /// Updates a single creation on the 'your models' section. + /// + private void UpdateSingleCreationOnYourModels(ObjectStoreEntry objectStoreEntry, bool isLocal, bool isSave) + { + // We've successfully called back with results from the query. Parse them into an actual entry. + Entry entry = new Entry(objectStoreEntry); + + lock (mutex) + { + // We update an asset by deleting it then loading it to the front of the menu. + Load load = loadsByType[PolyMenuMain.CreationType.YOUR]; + List creations = load.creations; + for (int i = 0; i < creations.Count; i++) + { + Creation creation = creations[i]; + if ((!creation.isLocal && creations[i].entry.queryEntry.id == entry.queryEntry.id) || + (creation.isLocal && creations[i].entry.queryEntry.localId == entry.queryEntry.localId)) + { + creations.RemoveAt(i); + break; + } + } + load.AddEntryToStartOfMenu(entry, isLocal, isSave); + + // Refresh the PolyMenu now that there are creations available. + PeltzerMain.Instance.GetPolyMenuMain().RefreshPolyMenu(); + } + } - // Refresh the PolyMenu now that there are creations available. - PeltzerMain.Instance.GetPolyMenuMain().RefreshPolyMenu(); - } - } + /// + /// Clears a given load. Typically used when a user signs out. We set all the previews visibly inactive but don't + /// destroy them. This would cause some serious problems with our threading if a preview we destroyed in the + /// middle of being loaded. + /// + /// TODO (bug): Destroy previews when a load is cleared in a threadsafe way by marking creations for + /// deletion and then deleting them on the main thread once they finish loading. + /// + /// The type of load. + public void ClearLoad(PolyMenuMain.CreationType type) + { + lock (mutex) + { + Load load; + if (loadsByType.TryGetValue(type, out load)) + { + // Hide every non-local preview. + for (int i = load.creations.Count() - 1; i >= 0; i--) + { + Creation creation = load.creations[i]; + + // Set the actual preview inactive. + creation.preview.SetActive(false); + // Remove any markers in the handler that this preview should be active once it's done loading. + creation.handler.isActiveOnMenu = false; + } + } + pendingLoadsByType.Remove(type); + loadsByType.Remove(type); + + PeltzerMain.Instance.GetPolyMenuMain().RefreshPolyMenu(); + } + } - /// - /// Removes a given creation of the specified type. - /// - public void RemoveSingleCreationAndRefreshMenu(PolyMenuMain.CreationType creationType, string entryIdToRemove) { - lock (mutex) { - Load load = loadsByType[creationType]; - List creations = load.creations; - for (int i = 0; i < creations.Count; i++) { - Creation creation = creations[i]; - if ((creation.isLocal && creation.entry.queryEntry.localId == entryIdToRemove) || - !creation.isLocal && creation.entry.queryEntry.id == entryIdToRemove) { - creations.RemoveAt(i); - PeltzerMain.Instance.GetPolyMenuMain().RefreshPolyMenu(); - break; - } + /// + /// Makes a query to get creations metadata for the given type. + /// + /// The type for the query. + /// Callback function on successful query. + public void GetAssetsServiceSearchResults(PolyMenuMain.CreationType type, + Action successCallback, System.Action failureCallback) + { + switch (type) + { + case PolyMenuMain.CreationType.FEATURED: + assetsServiceClient.GetFeaturedModels(successCallback, failureCallback); + break; + case PolyMenuMain.CreationType.YOUR: + assetsServiceClient.GetYourModels(successCallback, failureCallback); + break; + case PolyMenuMain.CreationType.LIKED: + assetsServiceClient.GetLikedModels(successCallback, failureCallback); + break; + } } - } - return; - } - /// - /// Updates a single cloud-saved creation on the 'your models' section. - /// - public void UpdateSingleCloudCreationOnYourModels(string asset) { - PeltzerMain.Instance.DoPolyMenuBackgroundWork(new ParseAssetBackgroundWork(asset, - delegate (ObjectStoreEntry objectStoreEntry) { - UpdateSingleCreationOnYourModels(objectStoreEntry, /* isLocal */ false, /* isSave */ true); - }, /* hackUrls */ true)); - } + /// + /// Makes a query to get creations metadata for the given asset. + /// + /// An assets service asset id. + /// Callback function on successful query. + public void GetAssetFromAssetsService(string assetId, Action callback) + { + assetsServiceClient.GetAsset(assetId, callback); + } - /// - /// Updates a single locally-saved creation on the 'your models' section. - /// - public void UpdateSingleLocalCreationOnYourModels(DirectoryInfo directory) { - ObjectStoreEntry objectStoreEntry; - if (GetObjectStoreEntryFromLocalDirectory(directory, out objectStoreEntry)) { - UpdateSingleCreationOnYourModels(objectStoreEntry, /* isLocal */ true, /* isSave */ true); - } - } + /// + /// Takes the metadata for an creation entry and makes the call to get the model and then starts + /// background work to create the GameObject preview of the creation that can be loaded to the PolyMenu. + /// + /// The creation to be loaded. + /// The type of entry it is. + public void LoadModelForCreation(Creation creation, PolyMenuMain.CreationType type) + { + ObjectStoreEntry entry = creation.entry.queryEntry; + if (entry == null || (entry.localPeltzerFile == null && + (entry.assets == null || (entry.assets.peltzer == null && entry.assets.peltzer_package == null)))) + { + OnLoadFailure(creation, type); + return; + } - /// - /// Updates a single creation on the 'your models' section. - /// - private void UpdateSingleCreationOnYourModels(ObjectStoreEntry objectStoreEntry, bool isLocal, bool isSave) { - // We've successfully called back with results from the query. Parse them into an actual entry. - Entry entry = new Entry(objectStoreEntry); - - lock (mutex) { - // We update an asset by deleting it then loading it to the front of the menu. - Load load = loadsByType[PolyMenuMain.CreationType.YOUR]; - List creations = load.creations; - for (int i = 0; i < creations.Count; i++) { - Creation creation = creations[i]; - if ((!creation.isLocal && creations[i].entry.queryEntry.id == entry.queryEntry.id) || - (creation.isLocal && creations[i].entry.queryEntry.localId == entry.queryEntry.localId)) { - creations.RemoveAt(i); - break; - } + // Setup the handler script from the prefab which is able to load the actual model. + creation.handler.Setup(entry); + + // Get the raw file data for the entry. + ObjectStoreClient.GetRawFileData(entry, delegate (byte[] rawFileData) + { + // On failure replace this load attempt with another by generating a pending load request. + if (rawFileData == null) + { + OnLoadFailure(creation, type); + } + + // On successful return of the raw byte data for the creation start background work and create the preview + // for the creation. + PeltzerMain.Instance.DoPolyMenuBackgroundWork(new LoadCreationWork(identityWorldSpace, creation.handler, this, + creation, rawFileData, entry, type)); + }); } - load.AddEntryToStartOfMenu(entry, isLocal, isSave); - // Refresh the PolyMenu now that there are creations available. - PeltzerMain.Instance.GetPolyMenuMain().RefreshPolyMenu(); - } - } + /// + /// Takes the metadata for an creation entry and makes the call to get the thumbnail and then + /// assigns it to the gameObject as a placeholder until the model can be loaded. + /// + /// The creation to be loaded. + /// The type of entry it is. + public IEnumerator LoadThumbnailForCreation(Creation creation, PolyMenuMain.CreationType type, Load load, + int indexInCreations) + { + ObjectStoreEntry entry = creation.entry.queryEntry; + if (entry == null) + { + OnLoadFailure(creation, type); + yield break; + } + // No thumbnail, just go ahead and load the model. + if (entry.thumbnail == null && entry.localThumbnailFile == null) + { + load.pendingModelLoadRequestIndices.Add(indexInCreations); + yield break; + } + // We have a thumbnail, fetch it before loading the model. + GetThumbnailTexture(entry, delegate (Texture2D tex) + { + Sprite thumbnailSprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), + new Vector2(0.5f, 0.5f), THUMBNAIL_IMPORT_PIXELS_PER_UNIT); + creation.SetThumbnailSprite(thumbnailSprite); + load.pendingModelLoadRequestIndices.Add(indexInCreations); + }); + } - /// - /// Clears a given load. Typically used when a user signs out. We set all the previews visibly inactive but don't - /// destroy them. This would cause some serious problems with our threading if a preview we destroyed in the - /// middle of being loaded. - /// - /// TODO (bug): Destroy previews when a load is cleared in a threadsafe way by marking creations for - /// deletion and then deleting them on the main thread once they finish loading. - /// - /// The type of load. - public void ClearLoad(PolyMenuMain.CreationType type) { - lock (mutex) { - Load load; - if (loadsByType.TryGetValue(type, out load)) { - // Hide every non-local preview. - for (int i = load.creations.Count() - 1; i >= 0; i--) { - Creation creation = load.creations[i]; - - // Set the actual preview inactive. - creation.preview.SetActive(false); - // Remove any markers in the handler that this preview should be active once it's done loading. - creation.handler.isActiveOnMenu = false; - } + private void GetThumbnailTexture(ObjectStoreEntry entry, + System.Action thumbnailTextureCallback, bool isRecursion = false) + { + if (entry.localThumbnailFile != null) + { + Texture2D tex = new Texture2D(192, 192); + tex.LoadImage(File.ReadAllBytes(entry.localThumbnailFile)); + thumbnailTextureCallback(tex); + } + else + { + UnityWebRequest request = assetsServiceClient.GetRequest(entry.thumbnail, "image/png"); + PeltzerMain.Instance.webRequestManager.EnqueueRequest( + () => { return request; }, + (bool success, int responseCode, byte[] responseBytes) => StartCoroutine( + ProcessGetThumbnailTexture(success, responseCode, responseBytes, request, entry, + thumbnailTextureCallback, isRecursion))); + } } - pendingLoadsByType.Remove(type); - loadsByType.Remove(type); - PeltzerMain.Instance.GetPolyMenuMain().RefreshPolyMenu(); - } - } + // Deals with the response of a GetThumbnailTexture request, retrying it if an auth token was stale. + private IEnumerator ProcessGetThumbnailTexture(bool success, int responseCode, + byte[] responseBytes, UnityWebRequest request, ObjectStoreEntry entry, + System.Action thumbnailTextureCallback, bool isRecursion = false) + { + if (!success || responseCode == 401 || responseBytes.Length == 0) + { + if (isRecursion) + { + Debug.Log(AssetsServiceClient.GetDebugString(request, "Error when fetching a thumbnail for " + entry.id)); + yield break; + } + yield return OAuth2Identity.Instance.Reauthorize(); + GetThumbnailTexture(entry, thumbnailTextureCallback, /* isRecursion */ true); + } + else + { + Texture2D tex = new Texture2D(192, 192); + tex.LoadImage(responseBytes); + thumbnailTextureCallback(tex); + } + } - /// - /// Makes a query to get creations metadata for the given type. - /// - /// The type for the query. - /// Callback function on successful query. - public void GetAssetsServiceSearchResults(PolyMenuMain.CreationType type, - Action successCallback, System.Action failureCallback) { - switch (type) { - case PolyMenuMain.CreationType.FEATURED: - assetsServiceClient.GetFeaturedModels(successCallback, failureCallback); - break; - case PolyMenuMain.CreationType.YOUR: - assetsServiceClient.GetYourModels(successCallback, failureCallback); - break; - case PolyMenuMain.CreationType.LIKED: - assetsServiceClient.GetLikedModels(successCallback, failureCallback); - break; - } - } + /// + /// Updates the entry's load status safely once it's successfully loaded from an external script. + /// + /// The creation that was loaded successfully. + /// The type of entry. + public void OnLoadSuccess(Creation creation, PolyMenuMain.CreationType type, + MeshWithMaterialRenderer mwmRenderer) + { + lock (mutex) + { + // Add the preview to the list of fully loaded and ready to go previews. + creation.thumbnail.SetActive(false); + // Update the status of the load to be finished and successful. + creation.entry.loadStatus = LoadStatus.SUCCESSFUL; + } - /// - /// Makes a query to get creations metadata for the given asset. - /// - /// An assets service asset id. - /// Callback function on successful query. - public void GetAssetFromAssetsService(string assetId, Action callback) { - assetsServiceClient.GetAsset(assetId, callback); - } + // If this creation was generated on the menu because the user just saved in app we want to reuse + // the preview as the savePreview. + if (creation.isSave) + { + PeltzerMain.Instance.savePreview.SetupPreview(mwmRenderer); + // Make sure that when the user opens the menu again it opens to the model they just saved. + PeltzerMain.Instance.polyMenuMain.SwitchToYourModelsSection(); + } - /// - /// Takes the metadata for an creation entry and makes the call to get the model and then starts - /// background work to create the GameObject preview of the creation that can be loaded to the PolyMenu. - /// - /// The creation to be loaded. - /// The type of entry it is. - public void LoadModelForCreation(Creation creation, PolyMenuMain.CreationType type) { - ObjectStoreEntry entry = creation.entry.queryEntry; - if (entry == null || (entry.localPeltzerFile == null && - (entry.assets == null || (entry.assets.peltzer == null && entry.assets.peltzer_package == null)))) { - OnLoadFailure(creation, type); - return; - } - - // Setup the handler script from the prefab which is able to load the actual model. - creation.handler.Setup(entry); - - // Get the raw file data for the entry. - ObjectStoreClient.GetRawFileData(entry, delegate(byte[] rawFileData) { - // On failure replace this load attempt with another by generating a pending load request. - if (rawFileData == null) { - OnLoadFailure(creation, type); + if (type == PolyMenuMain.CreationType.FEATURED) + { + PeltzerMain.Instance.menuHint.AddPreview(mwmRenderer); + } } - // On successful return of the raw byte data for the creation start background work and create the preview - // for the creation. - PeltzerMain.Instance.DoPolyMenuBackgroundWork(new LoadCreationWork(identityWorldSpace, creation.handler, this, - creation, rawFileData, entry, type)); - }); - } - - /// - /// Takes the metadata for an creation entry and makes the call to get the thumbnail and then - /// assigns it to the gameObject as a placeholder until the model can be loaded. - /// - /// The creation to be loaded. - /// The type of entry it is. - public IEnumerator LoadThumbnailForCreation(Creation creation, PolyMenuMain.CreationType type, Load load, - int indexInCreations) { - ObjectStoreEntry entry = creation.entry.queryEntry; - if (entry == null) { - OnLoadFailure(creation, type); - yield break; - } - // No thumbnail, just go ahead and load the model. - if (entry.thumbnail == null && entry.localThumbnailFile == null) { - load.pendingModelLoadRequestIndices.Add(indexInCreations); - yield break; - } - // We have a thumbnail, fetch it before loading the model. - GetThumbnailTexture(entry, delegate (Texture2D tex) { - Sprite thumbnailSprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), - new Vector2(0.5f, 0.5f), THUMBNAIL_IMPORT_PIXELS_PER_UNIT); - creation.SetThumbnailSprite(thumbnailSprite); - load.pendingModelLoadRequestIndices.Add(indexInCreations); - }); - } + /// + /// Updates the entry's load status safely if it fails to load from an external script or internally since there + /// are multiple places a load can fail. + /// + /// The creation that was not successfully loaded. + /// The type of entry. + public void OnLoadFailure(Creation creation, PolyMenuMain.CreationType type) + { + lock (mutex) + { + // Update the status of the load. + creation.entry.loadStatus = LoadStatus.FAILED; + creation.errorThumbnail.SetActive(true); + creation.thumbnail.SetActive(false); + } + } - private void GetThumbnailTexture(ObjectStoreEntry entry, - System.Action thumbnailTextureCallback, bool isRecursion = false) { - if (entry.localThumbnailFile != null) { - Texture2D tex = new Texture2D(192, 192); - tex.LoadImage(File.ReadAllBytes(entry.localThumbnailFile)); - thumbnailTextureCallback(tex); - } else { - UnityWebRequest request = assetsServiceClient.GetRequest(entry.thumbnail, "image/png"); - PeltzerMain.Instance.webRequestManager.EnqueueRequest( - () => { return request; }, - (bool success, int responseCode, byte[] responseBytes) => StartCoroutine( - ProcessGetThumbnailTexture(success, responseCode, responseBytes, request, entry, - thumbnailTextureCallback, isRecursion))); - } - } + /// + /// Gets a range of valid previews of a given type to be loaded a page of the PolyMenu. The page number defines + /// the range. This will not return the total number of desired previews if 1) There aren't enough previews + /// loaded. 2) upToAndIncluding exceeds the total number of possible previews (it's the last page). + /// + /// The type of preview being requested. + /// The start index of the range of previews. + /// The exclusive end index of the range of previews. + /// As many valid previews as possible. + public List GetPreviews(PolyMenuMain.CreationType type, int from, int upToNotIncluding) + { + List previews = new List(upToNotIncluding - from); + + lock (mutex) + { + if (loadsByType.ContainsKey(type)) + { + // Check to see if this preview request is for previews on our last loaded (loading) page. If it is we want to + // send load requests for the next page so we are always one page ahead of the user. + if (upToNotIncluding >= loadsByType[type].totalRootLoadRequests) + { + int startIndexToLoad = loadsByType[type].totalRootLoadRequests; + + // Either load a full page, or load the remaining creations on this current page. + int numberOfLoads = + Mathf.Min(NUMBER_OF_CREATIONS_PER_PAGE, loadsByType[type].creations.Count() - startIndexToLoad); + + // This is a request to load the next page so these requests count as root requests. + loadsByType[type].totalRootLoadRequests = + loadsByType[type].totalRootLoadRequests + numberOfLoads; + + // bug: Somehow numberOfLoads is throwing an ArgumentOutOfRangeException because it is being set to + // be less than 0. This shouldn't be happening but we can't repro to figure out what is wrong. Adding a + // check so that it doesn't throw an error. + if (numberOfLoads >= 0) + { + loadsByType[type].pendingThumbnailLoadRequestIndices.AddRange( + Enumerable.Range(startIndexToLoad, numberOfLoads)); + } + } + + for (int i = from; i < upToNotIncluding && i < loadsByType[type].creations.Count(); i++) + { + previews.Add(loadsByType[type].creations[i].preview); + } + } + } - // Deals with the response of a GetThumbnailTexture request, retrying it if an auth token was stale. - private IEnumerator ProcessGetThumbnailTexture(bool success, int responseCode, - byte[] responseBytes, UnityWebRequest request, ObjectStoreEntry entry, - System.Action thumbnailTextureCallback, bool isRecursion = false) { - if (!success || responseCode == 401 || responseBytes.Length == 0) { - if (isRecursion) { - Debug.Log(AssetsServiceClient.GetDebugString(request, "Error when fetching a thumbnail for " + entry.id)); - yield break; + return previews; } - yield return OAuth2Identity.Instance.Reauthorize(); - GetThumbnailTexture(entry, thumbnailTextureCallback, /* isRecursion */ true); - } else { - Texture2D tex = new Texture2D(192, 192); - tex.LoadImage(responseBytes); - thumbnailTextureCallback(tex); - } - } - - /// - /// Updates the entry's load status safely once it's successfully loaded from an external script. - /// - /// The creation that was loaded successfully. - /// The type of entry. - public void OnLoadSuccess(Creation creation, PolyMenuMain.CreationType type, - MeshWithMaterialRenderer mwmRenderer) { - lock (mutex) { - // Add the preview to the list of fully loaded and ready to go previews. - creation.thumbnail.SetActive(false); - // Update the status of the load to be finished and successful. - creation.entry.loadStatus = LoadStatus.SUCCESSFUL; - } - - // If this creation was generated on the menu because the user just saved in app we want to reuse - // the preview as the savePreview. - if (creation.isSave) { - PeltzerMain.Instance.savePreview.SetupPreview(mwmRenderer); - // Make sure that when the user opens the menu again it opens to the model they just saved. - PeltzerMain.Instance.polyMenuMain.SwitchToYourModelsSection(); - } - - if (type == PolyMenuMain.CreationType.FEATURED) { - PeltzerMain.Instance.menuHint.AddPreview(mwmRenderer); - } - } - /// - /// Updates the entry's load status safely if it fails to load from an external script or internally since there - /// are multiple places a load can fail. - /// - /// The creation that was not successfully loaded. - /// The type of entry. - public void OnLoadFailure(Creation creation, PolyMenuMain.CreationType type) { - lock (mutex) { - // Update the status of the load. - creation.entry.loadStatus = LoadStatus.FAILED; - creation.errorThumbnail.SetActive(true); - creation.thumbnail.SetActive(false); - } - } + /// + /// Finds the number of pages for a given creation type. This is either the number of pages there are, or the max + /// allowed number of pages. + /// + /// The creation type. + /// The number of pages. + public int GetNumberOfPages(PolyMenuMain.CreationType type) + { + bool containsType; + + lock (mutex) + { + containsType = loadsByType.ContainsKey(type); + } - /// - /// Gets a range of valid previews of a given type to be loaded a page of the PolyMenu. The page number defines - /// the range. This will not return the total number of desired previews if 1) There aren't enough previews - /// loaded. 2) upToAndIncluding exceeds the total number of possible previews (it's the last page). - /// - /// The type of preview being requested. - /// The start index of the range of previews. - /// The exclusive end index of the range of previews. - /// As many valid previews as possible. - public List GetPreviews(PolyMenuMain.CreationType type, int from, int upToNotIncluding) { - List previews = new List(upToNotIncluding - from); - - lock (mutex) { - if (loadsByType.ContainsKey(type)) { - // Check to see if this preview request is for previews on our last loaded (loading) page. If it is we want to - // send load requests for the next page so we are always one page ahead of the user. - if (upToNotIncluding >= loadsByType[type].totalRootLoadRequests) { - int startIndexToLoad = loadsByType[type].totalRootLoadRequests; - - // Either load a full page, or load the remaining creations on this current page. - int numberOfLoads = - Mathf.Min(NUMBER_OF_CREATIONS_PER_PAGE, loadsByType[type].creations.Count() - startIndexToLoad); - - // This is a request to load the next page so these requests count as root requests. - loadsByType[type].totalRootLoadRequests = - loadsByType[type].totalRootLoadRequests + numberOfLoads; - - // bug: Somehow numberOfLoads is throwing an ArgumentOutOfRangeException because it is being set to - // be less than 0. This shouldn't be happening but we can't repro to figure out what is wrong. Adding a - // check so that it doesn't throw an error. - if (numberOfLoads >= 0) { - loadsByType[type].pendingThumbnailLoadRequestIndices.AddRange( - Enumerable.Range(startIndexToLoad, numberOfLoads)); + if (!containsType) + { + return 1; } - } + else + { + int numPages; + + lock (mutex) + { + numPages = loadsByType[type].numberOfPages; + } - for (int i = from; i < upToNotIncluding && i < loadsByType[type].creations.Count(); i++) { - previews.Add(loadsByType[type].creations[i].preview); - } + return Math.Max(1, numPages); + } } - } - return previews; - } + /// + /// Whether a load is pending and we don't know if it is valid or invalid or if we are sure a load is valid because + /// it has non-zero entries. + /// + /// The load type. + /// Whether the load is pending or valid. + public bool HasPendingOrValidLoad(PolyMenuMain.CreationType type) + { + Load load; + bool hasPendingOrValidLoad; + + lock (mutex) + { + hasPendingOrValidLoad = pendingLoadsByType.Contains(type) + || (loadsByType.TryGetValue(type, out load) && load.totalNumberOfEntries > 0); + } - /// - /// Finds the number of pages for a given creation type. This is either the number of pages there are, or the max - /// allowed number of pages. - /// - /// The creation type. - /// The number of pages. - public int GetNumberOfPages(PolyMenuMain.CreationType type) { - bool containsType; - - lock (mutex) { - containsType = loadsByType.ContainsKey(type); - } - - if (!containsType) { - return 1; - } else { - int numPages; - - lock (mutex) { - numPages = loadsByType[type].numberOfPages; + return hasPendingOrValidLoad; } - return Math.Max(1, numPages); - } - } - - /// - /// Whether a load is pending and we don't know if it is valid or invalid or if we are sure a load is valid because - /// it has non-zero entries. - /// - /// The load type. - /// Whether the load is pending or valid. - public bool HasPendingOrValidLoad(PolyMenuMain.CreationType type) { - Load load; - bool hasPendingOrValidLoad; - - lock (mutex) { - hasPendingOrValidLoad = pendingLoadsByType.Contains(type) - || (loadsByType.TryGetValue(type, out load) && load.totalNumberOfEntries > 0); - } - - return hasPendingOrValidLoad; - } + /// + /// Whether or not the creations manager has a load for a given creation type. + /// + /// The type of creation. + /// Whether or not a load exists. + public bool IsLoadingType(PolyMenuMain.CreationType type) + { + bool containsType; + + lock (mutex) + { + containsType = loadsByType.ContainsKey(type); + } - /// - /// Whether or not the creations manager has a load for a given creation type. - /// - /// The type of creation. - /// Whether or not a load exists. - public bool IsLoadingType(PolyMenuMain.CreationType type) { - bool containsType; + return containsType; + } - lock (mutex) { - containsType = loadsByType.ContainsKey(type); - } + /// + /// Whether or not a load exists for this type and if the load has valid entries to load. + /// + /// The creation type being loaded. + /// Whether the load exists and has more than 0 entries to load. + public bool HasValidLoad(PolyMenuMain.CreationType type) + { + bool hasValidLoad; + + lock (mutex) + { + if (loadsByType.ContainsKey(type)) + { + hasValidLoad = loadsByType[type].totalNumberOfEntries > 0; + } + else + { + hasValidLoad = false; + } + } - return containsType; + return hasValidLoad; + } } /// - /// Whether or not a load exists for this type and if the load has valid entries to load. + /// Class for creating BackgroundWork executed in PeltzerMain. This minimizes how much Zandria creation + /// loading we do on the MainThread by getting file data on a background thread and then creating the + /// creation's preview on the MainThread in PostWork() when the background work is complete. /// - /// The creation type being loaded. - /// Whether the load exists and has more than 0 entries to load. - public bool HasValidLoad(PolyMenuMain.CreationType type) { - bool hasValidLoad; - - lock (mutex) { - if (loadsByType.ContainsKey(type)) { - hasValidLoad = loadsByType[type].totalNumberOfEntries > 0; - } else { - hasValidLoad = false; + public class LoadCreationWork : BackgroundWork + { + private readonly ZandriaCreationsManager creationsManager; + private readonly ZandriaCreationHandler creationHandler; + private readonly byte[] rawFileData; + private Creation creation; + private List meshes; + private WorldSpace identityWorldSpace; + private bool isValidCreation; + private PolyMenuMain.CreationType type; + private float recommendedRotation; + + public LoadCreationWork(WorldSpace identityWorldSpace, ZandriaCreationHandler creationHandler, + ZandriaCreationsManager creationsManager, Creation creation, byte[] rawFileData, + ObjectStoreEntry entry, PolyMenuMain.CreationType type) + { + this.creationHandler = creationHandler; + this.creationsManager = creationsManager; + this.creation = creation; + this.rawFileData = rawFileData; + this.identityWorldSpace = identityWorldSpace; + this.type = type; } - } - - return hasValidLoad; - } - } - - /// - /// Class for creating BackgroundWork executed in PeltzerMain. This minimizes how much Zandria creation - /// loading we do on the MainThread by getting file data on a background thread and then creating the - /// creation's preview on the MainThread in PostWork() when the background work is complete. - /// - public class LoadCreationWork : BackgroundWork { - private readonly ZandriaCreationsManager creationsManager; - private readonly ZandriaCreationHandler creationHandler; - private readonly byte[] rawFileData; - private Creation creation; - private List meshes; - private WorldSpace identityWorldSpace; - private bool isValidCreation; - private PolyMenuMain.CreationType type; - private float recommendedRotation; - - public LoadCreationWork(WorldSpace identityWorldSpace, ZandriaCreationHandler creationHandler, - ZandriaCreationsManager creationsManager, Creation creation, byte[] rawFileData, - ObjectStoreEntry entry, PolyMenuMain.CreationType type) { - this.creationHandler = creationHandler; - this.creationsManager = creationsManager; - this.creation = creation; - this.rawFileData = rawFileData; - this.identityWorldSpace = identityWorldSpace; - this.type = type; - } - public void BackgroundWork() { - // Get the actual MMeshes from the peltzer file. - isValidCreation = creationHandler.GetMMeshesFromPeltzerFile(rawFileData, - delegate (List meshes, float recommendedRotation) { - this.meshes = meshes; - this.recommendedRotation = recommendedRotation; - }); - } - - public void PostWork() { - if (!isValidCreation) { - creationsManager.OnLoadFailure(creation, type); - return; - } - - if (!Features.showPolyMenuModelPreviews) { - creation.entry.loadStatus = ZandriaCreationsManager.LoadStatus.SUCCESSFUL; - - if (creation.isSave) { - MeshHelper.GameObjectFromMMeshesForMenu(identityWorldSpace, meshes, delegate (GameObject meshPreview) { - MeshWithMaterialRenderer mwmRenderer = meshPreview.GetComponent(); - - // Set it to draw to a new layer, such that we don't see shadows for this preview. - mwmRenderer.Layer = MeshWithMaterialRenderer.NO_SHADOWS_LAYER; - - // Reset the transform so that we only use the parent transform. - mwmRenderer.ResetTransform(); - meshPreview.SetActive(false); - - PeltzerMain.Instance.savePreview.SetupPreview(mwmRenderer); - // Make sure that when the user opens the menu again it opens to the model they just saved. - PeltzerMain.Instance.polyMenuMain.SwitchToYourModelsSection(); - }); - } else if (type == PolyMenuMain.CreationType.FEATURED && PeltzerMain.Instance.menuHint.IsPopulating()) { - MeshHelper.GameObjectFromMMeshesForMenu(identityWorldSpace, meshes, delegate (GameObject meshPreview) { - MeshWithMaterialRenderer mwmRenderer = meshPreview.GetComponent(); - - // Set it to draw to a new layer, such that we don't see shadows for this preview. - mwmRenderer.Layer = MeshWithMaterialRenderer.NO_SHADOWS_LAYER; + public void BackgroundWork() + { + // Get the actual MMeshes from the peltzer file. + isValidCreation = creationHandler.GetMMeshesFromPeltzerFile(rawFileData, + delegate (List meshes, float recommendedRotation) + { + this.meshes = meshes; + this.recommendedRotation = recommendedRotation; + }); + } - // Reset the transform so that we only use the parent transform. - mwmRenderer.ResetTransform(); - meshPreview.SetActive(false); + public void PostWork() + { + if (!isValidCreation) + { + creationsManager.OnLoadFailure(creation, type); + return; + } - PeltzerMain.Instance.menuHint.AddPreview(mwmRenderer); - }); - } + if (!Features.showPolyMenuModelPreviews) + { + creation.entry.loadStatus = ZandriaCreationsManager.LoadStatus.SUCCESSFUL; + + if (creation.isSave) + { + MeshHelper.GameObjectFromMMeshesForMenu(identityWorldSpace, meshes, delegate (GameObject meshPreview) + { + MeshWithMaterialRenderer mwmRenderer = meshPreview.GetComponent(); + + // Set it to draw to a new layer, such that we don't see shadows for this preview. + mwmRenderer.Layer = MeshWithMaterialRenderer.NO_SHADOWS_LAYER; + + // Reset the transform so that we only use the parent transform. + mwmRenderer.ResetTransform(); + meshPreview.SetActive(false); + + PeltzerMain.Instance.savePreview.SetupPreview(mwmRenderer); + // Make sure that when the user opens the menu again it opens to the model they just saved. + PeltzerMain.Instance.polyMenuMain.SwitchToYourModelsSection(); + }); + } + else if (type == PolyMenuMain.CreationType.FEATURED && PeltzerMain.Instance.menuHint.IsPopulating()) + { + MeshHelper.GameObjectFromMMeshesForMenu(identityWorldSpace, meshes, delegate (GameObject meshPreview) + { + MeshWithMaterialRenderer mwmRenderer = meshPreview.GetComponent(); + + // Set it to draw to a new layer, such that we don't see shadows for this preview. + mwmRenderer.Layer = MeshWithMaterialRenderer.NO_SHADOWS_LAYER; + + // Reset the transform so that we only use the parent transform. + mwmRenderer.ResetTransform(); + meshPreview.SetActive(false); + + PeltzerMain.Instance.menuHint.AddPreview(mwmRenderer); + }); + } + + return; + } - return; - } - - // Get a preview from the MMeshes found on the background thread. - MeshHelper.GameObjectFromMMeshesForMenu(identityWorldSpace, meshes, delegate (GameObject meshPreview) { - MeshWithMaterialRenderer mwmRenderer = meshPreview.GetComponent(); - - // Set it to draw to a new layer, such that we don't see shadows for this preview. - mwmRenderer.Layer = MeshWithMaterialRenderer.NO_SHADOWS_LAYER; - - // Reset the transform so that we only use the parent transform. - mwmRenderer.ResetTransform(); - - // We have successfully loaded the creation as a preview so we attach it to the menu. - if (meshPreview != null) { - // Parent the preview to the ZandriaCreationHolder. - meshPreview.transform.parent = creation.preview.transform.Find("CreationPreview"); - meshPreview.transform.localPosition = Vector3.zero; - Quaternion newRotation = new Quaternion(); - newRotation.eulerAngles = new Vector3(0, recommendedRotation, 0); - meshPreview.transform.localRotation = newRotation; - - // Hide the preview from the scene. The PolyMenuMain will be responsible for showing the appropriate - // previews. - creation.preview.SetActive(creationHandler.isActiveOnMenu); - - // We've successfully loaded a creation. Pass everything back to the creationsManager to keep track of. - creationsManager.OnLoadSuccess(creation, type, mwmRenderer); - } else { - // Make a new load request if the preview returned null. - creationsManager.OnLoadFailure(creation, type); + // Get a preview from the MMeshes found on the background thread. + MeshHelper.GameObjectFromMMeshesForMenu(identityWorldSpace, meshes, delegate (GameObject meshPreview) + { + MeshWithMaterialRenderer mwmRenderer = meshPreview.GetComponent(); + + // Set it to draw to a new layer, such that we don't see shadows for this preview. + mwmRenderer.Layer = MeshWithMaterialRenderer.NO_SHADOWS_LAYER; + + // Reset the transform so that we only use the parent transform. + mwmRenderer.ResetTransform(); + + // We have successfully loaded the creation as a preview so we attach it to the menu. + if (meshPreview != null) + { + // Parent the preview to the ZandriaCreationHolder. + meshPreview.transform.parent = creation.preview.transform.Find("CreationPreview"); + meshPreview.transform.localPosition = Vector3.zero; + Quaternion newRotation = new Quaternion(); + newRotation.eulerAngles = new Vector3(0, recommendedRotation, 0); + meshPreview.transform.localRotation = newRotation; + + // Hide the preview from the scene. The PolyMenuMain will be responsible for showing the appropriate + // previews. + creation.preview.SetActive(creationHandler.isActiveOnMenu); + + // We've successfully loaded a creation. Pass everything back to the creationsManager to keep track of. + creationsManager.OnLoadSuccess(creation, type, mwmRenderer); + } + else + { + // Make a new load request if the preview returned null. + creationsManager.OnLoadFailure(creation, type); + } + }); } - }); } - } } diff --git a/Assets/SimplePixelizer/SimplePixelizer.cs b/Assets/SimplePixelizer/SimplePixelizer.cs index 97c4cb97..10294e26 100644 --- a/Assets/SimplePixelizer/SimplePixelizer.cs +++ b/Assets/SimplePixelizer/SimplePixelizer.cs @@ -20,54 +20,61 @@ [ExecuteInEditMode] [AddComponentMenu("Image Effects/SimplePixelizer")] -public class SimplePixelizer : MonoBehaviour { - - public bool colorBlending = false; - public int pixelize = 8; - - //Fixed Resolution - //Enabling fixed resolution will ignore the pixelize variable. - //It won't ignore colorBlending - public bool useFixedResolution = false; - public int fixedHeight = 640; - public int fixedWidth = 480; - - //Check if image effects are supported - protected void Start() { - if (!SystemInfo.supportsImageEffects) { - enabled = false; - return; - } - } - - //Downgrade the image - void OnRenderImage (RenderTexture source, RenderTexture destination) { - //Create the buffer - RenderTexture buffer = null; - - //Set the resolution of the buffer - if(useFixedResolution) { - buffer = RenderTexture.GetTemporary(fixedWidth, fixedHeight, 0); - } - else { - buffer = RenderTexture.GetTemporary(source.width/pixelize, source.height/pixelize, 0); - } - - //Change filter mode of buffer to create the pixel effect - buffer.filterMode = FilterMode.Point; - - //Change filter mode of source to disable color blending/merging - if(!colorBlending) { - source.filterMode = FilterMode.Point; - } - - //Copy source to buffer to create the final image - Graphics.Blit(source, buffer); - - //Copy buffer to destination so it renders on screen - Graphics.Blit(buffer, destination); - - //Release buffer - RenderTexture.ReleaseTemporary(buffer); - } +public class SimplePixelizer : MonoBehaviour +{ + + public bool colorBlending = false; + public int pixelize = 8; + + //Fixed Resolution + //Enabling fixed resolution will ignore the pixelize variable. + //It won't ignore colorBlending + public bool useFixedResolution = false; + public int fixedHeight = 640; + public int fixedWidth = 480; + + //Check if image effects are supported + protected void Start() + { + if (!SystemInfo.supportsImageEffects) + { + enabled = false; + return; + } + } + + //Downgrade the image + void OnRenderImage(RenderTexture source, RenderTexture destination) + { + //Create the buffer + RenderTexture buffer = null; + + //Set the resolution of the buffer + if (useFixedResolution) + { + buffer = RenderTexture.GetTemporary(fixedWidth, fixedHeight, 0); + } + else + { + buffer = RenderTexture.GetTemporary(source.width / pixelize, source.height / pixelize, 0); + } + + //Change filter mode of buffer to create the pixel effect + buffer.filterMode = FilterMode.Point; + + //Change filter mode of source to disable color blending/merging + if (!colorBlending) + { + source.filterMode = FilterMode.Point; + } + + //Copy source to buffer to create the final image + Graphics.Blit(source, buffer); + + //Copy buffer to destination so it renders on screen + Graphics.Blit(buffer, destination); + + //Release buffer + RenderTexture.ReleaseTemporary(buffer); + } } \ No newline at end of file diff --git a/Assets/ToolMaterialManager.cs b/Assets/ToolMaterialManager.cs index cfe6d995..7ebd6ee4 100644 --- a/Assets/ToolMaterialManager.cs +++ b/Assets/ToolMaterialManager.cs @@ -18,91 +18,110 @@ using com.google.apps.peltzer.client.model.main; using UnityEngine; -public class ToolMaterialManager : MonoBehaviour { - private const float ANIMATION_DURATION = .3f; - // GOs using MeshRenderer - public GameObject[] materialObjects; - private const float DISABLE_MAX = 0.9f; +public class ToolMaterialManager : MonoBehaviour +{ + private const float ANIMATION_DURATION = .3f; + // GOs using MeshRenderer + public GameObject[] materialObjects; + private const float DISABLE_MAX = 0.9f; - public ControllerMode controllerMode; - private float animationStartTime; + public ControllerMode controllerMode; + private float animationStartTime; - public bool isDisabled; + public bool isDisabled; - private bool animatingToDisable; - private bool animatingToEnable; + private bool animatingToDisable; + private bool animatingToEnable; - private float curPct; + private float curPct; - private RestrictionManager restrictionManager; - // Use this for initialization - void Start () { - this.restrictionManager = PeltzerMain.Instance.restrictionManager; - // Instance materials on all meshes - foreach (GameObject go in materialObjects) { - Renderer renderer = go.GetComponent(); - renderer.material = new Material(renderer.material); - } + private RestrictionManager restrictionManager; + // Use this for initialization + void Start() + { + this.restrictionManager = PeltzerMain.Instance.restrictionManager; + // Instance materials on all meshes + foreach (GameObject go in materialObjects) + { + Renderer renderer = go.GetComponent(); + renderer.material = new Material(renderer.material); + } - curPct = 0f; - animatingToDisable = false; - animatingToEnable = false; - } - - // Update is called once per frame - void Update () { - if (!animatingToDisable && !animatingToEnable) return; - curPct = Mathf.Min((Time.time - animationStartTime) / ANIMATION_DURATION, 1f); - if (animatingToDisable) { - SetOverridePercent(curPct); - if (curPct >= DISABLE_MAX) { + curPct = 0f; animatingToDisable = false; - } - } - if (animatingToEnable) { - SetOverridePercent(1f - curPct); - if (curPct >= 1f) animatingToEnable = false; + animatingToEnable = false; } - } - public void ChangeToEnable() { - if (curPct >= 1f) { - curPct = 0f; + // Update is called once per frame + void Update() + { + if (!animatingToDisable && !animatingToEnable) return; + curPct = Mathf.Min((Time.time - animationStartTime) / ANIMATION_DURATION, 1f); + if (animatingToDisable) + { + SetOverridePercent(curPct); + if (curPct >= DISABLE_MAX) + { + animatingToDisable = false; + } + } + if (animatingToEnable) + { + SetOverridePercent(1f - curPct); + if (curPct >= 1f) animatingToEnable = false; + } } - if (!gameObject.activeInHierarchy) { - SetOverridePercent(0); - animatingToEnable = false; - animatingToDisable = false; - } else { - animatingToEnable = true; - animatingToDisable = false; - animationStartTime = Time.time - ANIMATION_DURATION * curPct; - isDisabled = false; - } - } + public void ChangeToEnable() + { + if (curPct >= 1f) + { + curPct = 0f; + } - public void ChangeToDisable() { - if (curPct >= 1f) { - curPct = 0f; + if (!gameObject.activeInHierarchy) + { + SetOverridePercent(0); + animatingToEnable = false; + animatingToDisable = false; + } + else + { + animatingToEnable = true; + animatingToDisable = false; + animationStartTime = Time.time - ANIMATION_DURATION * curPct; + isDisabled = false; + } } - if (!gameObject.activeInHierarchy) { - SetOverridePercent(DISABLE_MAX); - animatingToEnable = false; - animatingToDisable = false; - } else { - animatingToEnable = false; - animatingToDisable = true; - animationStartTime = Time.time - ANIMATION_DURATION * curPct; - isDisabled = true; + public void ChangeToDisable() + { + if (curPct >= 1f) + { + curPct = 0f; + } + + if (!gameObject.activeInHierarchy) + { + SetOverridePercent(DISABLE_MAX); + animatingToEnable = false; + animatingToDisable = false; + } + else + { + animatingToEnable = false; + animatingToDisable = true; + animationStartTime = Time.time - ANIMATION_DURATION * curPct; + isDisabled = true; + } } - } - void SetOverridePercent(float percent) { - foreach (GameObject go in materialObjects) { - Renderer renderer = go.GetComponent(); - renderer.material.SetFloat("_OverrideAmount", percent); + void SetOverridePercent(float percent) + { + foreach (GameObject go in materialObjects) + { + Renderer renderer = go.GetComponent(); + renderer.material.SetFloat("_OverrideAmount", percent); + } } - } } diff --git a/Assets/Vistas/shaders/Editor/PolyboxShaderGUI.cs b/Assets/Vistas/shaders/Editor/PolyboxShaderGUI.cs index eb9c9c9a..0cac69db 100644 --- a/Assets/Vistas/shaders/Editor/PolyboxShaderGUI.cs +++ b/Assets/Vistas/shaders/Editor/PolyboxShaderGUI.cs @@ -17,414 +17,414 @@ namespace UnityEditor { -internal class PolyboxShaderGUI : ShaderGUI -{ - private enum WorkflowMode - { - Specular, - Metallic, - Dielectric - } - - public enum BlendMode - { - Opaque, - Cutout, - Fade, // Old school alpha-blending mode, fresnel does not affect amount of transparency - Transparent // Physically plausible transparency mode, implemented as alpha pre-multiply - } - - private static class Styles - { - public static GUIStyle optionsButton = "PaneOptions"; - public static GUIContent uvSetLabel = new GUIContent("UV Set"); - public static GUIContent[] uvSetOptions = new GUIContent[] { new GUIContent("UV channel 0"), new GUIContent("UV channel 1") }; - - public static string emptyTootip = ""; - public static GUIContent albedoText = new GUIContent("Albedo", "Albedo (RGB) and Transparency (A)"); - public static GUIContent alphaCutoffText = new GUIContent("Alpha Cutoff", "Threshold for alpha cutoff"); - public static GUIContent specularMapText = new GUIContent("Specular", "Specular (RGB) and Smoothness (A)"); - public static GUIContent metallicMapText = new GUIContent("Metallic", "Metallic (R) and Smoothness (A)"); - public static GUIContent smoothnessText = new GUIContent("Smoothness", ""); - public static GUIContent normalMapText = new GUIContent("Normal Map", "Normal Map"); - public static GUIContent heightMapText = new GUIContent("Height Map", "Height Map (G)"); - public static GUIContent occlusionText = new GUIContent("Occlusion", "Occlusion (G)"); - public static GUIContent emissionText = new GUIContent("Emission", "Emission (RGB)"); - public static GUIContent detailMaskText = new GUIContent("Detail Mask", "Mask for Secondary Maps (A)"); - public static GUIContent detailAlbedoText = new GUIContent("Detail Albedo x2", "Albedo (RGB) multiplied by 2"); - public static GUIContent detailNormalMapText = new GUIContent("Normal Map", "Normal Map"); - - public static GUIContent heightFogStart = new GUIContent("Height Fog Start", "Start range for Height Fog measured in meters on Z axis"); - public static GUIContent heightFogEnd = new GUIContent("Height Fog End", "End range for Height for measured in meters on the Z axis"); - public static GUIContent heightFogColor = new GUIContent("Height Fog Color", "Height Fog Overide Color, if Alhpa is 0 then the Global Fog Color is used"); - - public static string whiteSpaceString = " "; - public static string primaryMapsText = "Main Maps"; - public static string secondaryMapsText = "Secondary Maps"; - public static string renderingMode = "Rendering Mode"; - public static GUIContent emissiveWarning = new GUIContent ("Emissive value is animated but the material has not been configured to support emissive. Please make sure the material itself has some amount of emissive."); - public static GUIContent emissiveColorWarning = new GUIContent ("Ensure emissive color is non-black for emission to have effect."); - public static readonly string[] blendNames = Enum.GetNames (typeof (BlendMode)); - } - - MaterialProperty blendMode = null; - MaterialProperty albedoMap = null; - MaterialProperty albedoColor = null; - MaterialProperty alphaCutoff = null; - MaterialProperty specularMap = null; - MaterialProperty specularColor = null; - MaterialProperty metallicMap = null; - MaterialProperty metallic = null; - MaterialProperty smoothness = null; - MaterialProperty bumpScale = null; - MaterialProperty bumpMap = null; - MaterialProperty occlusionStrength = null; - MaterialProperty occlusionMap = null; - MaterialProperty heigtMapScale = null; - MaterialProperty heightMap = null; - MaterialProperty emissionColorForRendering = null; - MaterialProperty emissionMap = null; - MaterialProperty detailMask = null; - MaterialProperty detailAlbedoMap = null; - MaterialProperty detailNormalMapScale = null; - MaterialProperty detailNormalMap = null; - MaterialProperty uvSetSecondary = null; - - MaterialProperty heightFogStart = null; - MaterialProperty heightFogEnd = null; - MaterialProperty heightFogColor = null; - - MaterialEditor m_MaterialEditor; - WorkflowMode m_WorkflowMode = WorkflowMode.Specular; - ColorPickerHDRConfig m_ColorPickerHDRConfig = new ColorPickerHDRConfig(0f, 99f, 1/99f, 3f); - - bool m_FirstTimeApply = true; - - public void FindProperties (MaterialProperty[] props) - { - blendMode = FindProperty ("_Mode", props); - albedoMap = FindProperty ("_MainTex", props); - albedoColor = FindProperty ("_Color", props); - alphaCutoff = FindProperty ("_Cutoff", props); - specularMap = FindProperty ("_SpecGlossMap", props, false); - specularColor = FindProperty ("_SpecColor", props, false); - metallicMap = FindProperty ("_MetallicGlossMap", props, false); - metallic = FindProperty ("_Metallic", props, false); - if (specularMap != null && specularColor != null) - m_WorkflowMode = WorkflowMode.Specular; - else if (metallicMap != null && metallic != null) - m_WorkflowMode = WorkflowMode.Metallic; - else - m_WorkflowMode = WorkflowMode.Dielectric; - smoothness = FindProperty ("_Glossiness", props); - bumpScale = FindProperty ("_BumpScale", props); - bumpMap = FindProperty ("_BumpMap", props); - heigtMapScale = FindProperty ("_Parallax", props); - heightMap = FindProperty("_ParallaxMap", props); - occlusionStrength = FindProperty ("_OcclusionStrength", props); - occlusionMap = FindProperty ("_OcclusionMap", props); - emissionColorForRendering = FindProperty ("_EmissionColor", props); - emissionMap = FindProperty ("_EmissionMap", props); - detailMask = FindProperty ("_DetailMask", props); - detailAlbedoMap = FindProperty ("_DetailAlbedoMap", props); - detailNormalMapScale = FindProperty ("_DetailNormalMapScale", props); - detailNormalMap = FindProperty ("_DetailNormalMap", props); - uvSetSecondary = FindProperty ("_UVSec", props); - - heightFogStart = FindProperty ("_HeightFogStart", props); - heightFogEnd = FindProperty ("_HeightFogEnd", props); - heightFogColor = FindProperty ("_HeightFogColor", props); - } - - public override void OnGUI (MaterialEditor materialEditor, MaterialProperty[] props) - { - FindProperties (props); // MaterialProperties can be animated so we do not cache them but fetch them every event to ensure animated values are updated correctly - m_MaterialEditor = materialEditor; - Material material = materialEditor.target as Material; - - ShaderPropertiesGUI (material); - - // Make sure that needed keywords are set up if we're switching some existing - // material to a standard shader. - if (m_FirstTimeApply) - { - SetMaterialKeywords (material, m_WorkflowMode); - m_FirstTimeApply = false; - } - } - - public void ShaderPropertiesGUI (Material material) - { - // Use default labelWidth - EditorGUIUtility.labelWidth = 0f; - - // Detect any changes to the material - EditorGUI.BeginChangeCheck(); - { - BlendModePopup(); - - // Primary properties - GUILayout.Label (Styles.primaryMapsText, EditorStyles.boldLabel); - DoAlbedoArea(material); - DoSpecularMetallicArea(); - m_MaterialEditor.TexturePropertySingleLine(Styles.normalMapText, bumpMap, bumpMap.textureValue != null ? bumpScale : null); - m_MaterialEditor.TexturePropertySingleLine(Styles.heightMapText, heightMap, heightMap.textureValue != null ? heigtMapScale : null); - m_MaterialEditor.TexturePropertySingleLine(Styles.occlusionText, occlusionMap, occlusionMap.textureValue != null ? occlusionStrength : null); - DoEmissionArea(material); - m_MaterialEditor.TexturePropertySingleLine(Styles.detailMaskText, detailMask); - EditorGUI.BeginChangeCheck(); - m_MaterialEditor.TextureScaleOffsetProperty(albedoMap); - if (EditorGUI.EndChangeCheck()) - emissionMap.textureScaleAndOffset = albedoMap.textureScaleAndOffset; // Apply the main texture scale and offset to the emission texture as well, for Enlighten's sake - - EditorGUILayout.Space(); - - m_MaterialEditor.ShaderProperty(heightFogStart, Styles.heightFogStart.text); - m_MaterialEditor.ShaderProperty(heightFogEnd, Styles.heightFogEnd.text); - //m_MaterialEditor.ShaderProperty (heightFogColor, Styles.heightFogColor.text); - m_MaterialEditor.ColorProperty (heightFogColor, Styles.heightFogColor.text); - - - // Secondary properties - GUILayout.Label(Styles.secondaryMapsText, EditorStyles.boldLabel); - m_MaterialEditor.TexturePropertySingleLine(Styles.detailAlbedoText, detailAlbedoMap); - m_MaterialEditor.TexturePropertySingleLine(Styles.detailNormalMapText, detailNormalMap, detailNormalMapScale); - m_MaterialEditor.TextureScaleOffsetProperty(detailAlbedoMap); - m_MaterialEditor.ShaderProperty(uvSetSecondary, Styles.uvSetLabel.text); - } - if (EditorGUI.EndChangeCheck()) - { - foreach (var obj in blendMode.targets) - MaterialChanged((Material)obj, m_WorkflowMode); - } - } - - internal void DetermineWorkflow(MaterialProperty[] props) - { - if (FindProperty("_SpecGlossMap", props, false) != null && FindProperty("_SpecColor", props, false) != null) - m_WorkflowMode = WorkflowMode.Specular; - else if (FindProperty("_MetallicGlossMap", props, false) != null && FindProperty("_Metallic", props, false) != null) - m_WorkflowMode = WorkflowMode.Metallic; - else - m_WorkflowMode = WorkflowMode.Dielectric; - } - - public override void AssignNewShaderToMaterial (Material material, Shader oldShader, Shader newShader) - { - // _Emission property is lost after assigning Standard shader to the material - // thus transfer it before assigning the new shader - if (material.HasProperty("_Emission")) + internal class PolyboxShaderGUI : ShaderGUI + { + private enum WorkflowMode + { + Specular, + Metallic, + Dielectric + } + + public enum BlendMode + { + Opaque, + Cutout, + Fade, // Old school alpha-blending mode, fresnel does not affect amount of transparency + Transparent // Physically plausible transparency mode, implemented as alpha pre-multiply + } + + private static class Styles + { + public static GUIStyle optionsButton = "PaneOptions"; + public static GUIContent uvSetLabel = new GUIContent("UV Set"); + public static GUIContent[] uvSetOptions = new GUIContent[] { new GUIContent("UV channel 0"), new GUIContent("UV channel 1") }; + + public static string emptyTootip = ""; + public static GUIContent albedoText = new GUIContent("Albedo", "Albedo (RGB) and Transparency (A)"); + public static GUIContent alphaCutoffText = new GUIContent("Alpha Cutoff", "Threshold for alpha cutoff"); + public static GUIContent specularMapText = new GUIContent("Specular", "Specular (RGB) and Smoothness (A)"); + public static GUIContent metallicMapText = new GUIContent("Metallic", "Metallic (R) and Smoothness (A)"); + public static GUIContent smoothnessText = new GUIContent("Smoothness", ""); + public static GUIContent normalMapText = new GUIContent("Normal Map", "Normal Map"); + public static GUIContent heightMapText = new GUIContent("Height Map", "Height Map (G)"); + public static GUIContent occlusionText = new GUIContent("Occlusion", "Occlusion (G)"); + public static GUIContent emissionText = new GUIContent("Emission", "Emission (RGB)"); + public static GUIContent detailMaskText = new GUIContent("Detail Mask", "Mask for Secondary Maps (A)"); + public static GUIContent detailAlbedoText = new GUIContent("Detail Albedo x2", "Albedo (RGB) multiplied by 2"); + public static GUIContent detailNormalMapText = new GUIContent("Normal Map", "Normal Map"); + + public static GUIContent heightFogStart = new GUIContent("Height Fog Start", "Start range for Height Fog measured in meters on Z axis"); + public static GUIContent heightFogEnd = new GUIContent("Height Fog End", "End range for Height for measured in meters on the Z axis"); + public static GUIContent heightFogColor = new GUIContent("Height Fog Color", "Height Fog Overide Color, if Alhpa is 0 then the Global Fog Color is used"); + + public static string whiteSpaceString = " "; + public static string primaryMapsText = "Main Maps"; + public static string secondaryMapsText = "Secondary Maps"; + public static string renderingMode = "Rendering Mode"; + public static GUIContent emissiveWarning = new GUIContent("Emissive value is animated but the material has not been configured to support emissive. Please make sure the material itself has some amount of emissive."); + public static GUIContent emissiveColorWarning = new GUIContent("Ensure emissive color is non-black for emission to have effect."); + public static readonly string[] blendNames = Enum.GetNames(typeof(BlendMode)); + } + + MaterialProperty blendMode = null; + MaterialProperty albedoMap = null; + MaterialProperty albedoColor = null; + MaterialProperty alphaCutoff = null; + MaterialProperty specularMap = null; + MaterialProperty specularColor = null; + MaterialProperty metallicMap = null; + MaterialProperty metallic = null; + MaterialProperty smoothness = null; + MaterialProperty bumpScale = null; + MaterialProperty bumpMap = null; + MaterialProperty occlusionStrength = null; + MaterialProperty occlusionMap = null; + MaterialProperty heigtMapScale = null; + MaterialProperty heightMap = null; + MaterialProperty emissionColorForRendering = null; + MaterialProperty emissionMap = null; + MaterialProperty detailMask = null; + MaterialProperty detailAlbedoMap = null; + MaterialProperty detailNormalMapScale = null; + MaterialProperty detailNormalMap = null; + MaterialProperty uvSetSecondary = null; + + MaterialProperty heightFogStart = null; + MaterialProperty heightFogEnd = null; + MaterialProperty heightFogColor = null; + + MaterialEditor m_MaterialEditor; + WorkflowMode m_WorkflowMode = WorkflowMode.Specular; + ColorPickerHDRConfig m_ColorPickerHDRConfig = new ColorPickerHDRConfig(0f, 99f, 1 / 99f, 3f); + + bool m_FirstTimeApply = true; + + public void FindProperties(MaterialProperty[] props) + { + blendMode = FindProperty("_Mode", props); + albedoMap = FindProperty("_MainTex", props); + albedoColor = FindProperty("_Color", props); + alphaCutoff = FindProperty("_Cutoff", props); + specularMap = FindProperty("_SpecGlossMap", props, false); + specularColor = FindProperty("_SpecColor", props, false); + metallicMap = FindProperty("_MetallicGlossMap", props, false); + metallic = FindProperty("_Metallic", props, false); + if (specularMap != null && specularColor != null) + m_WorkflowMode = WorkflowMode.Specular; + else if (metallicMap != null && metallic != null) + m_WorkflowMode = WorkflowMode.Metallic; + else + m_WorkflowMode = WorkflowMode.Dielectric; + smoothness = FindProperty("_Glossiness", props); + bumpScale = FindProperty("_BumpScale", props); + bumpMap = FindProperty("_BumpMap", props); + heigtMapScale = FindProperty("_Parallax", props); + heightMap = FindProperty("_ParallaxMap", props); + occlusionStrength = FindProperty("_OcclusionStrength", props); + occlusionMap = FindProperty("_OcclusionMap", props); + emissionColorForRendering = FindProperty("_EmissionColor", props); + emissionMap = FindProperty("_EmissionMap", props); + detailMask = FindProperty("_DetailMask", props); + detailAlbedoMap = FindProperty("_DetailAlbedoMap", props); + detailNormalMapScale = FindProperty("_DetailNormalMapScale", props); + detailNormalMap = FindProperty("_DetailNormalMap", props); + uvSetSecondary = FindProperty("_UVSec", props); + + heightFogStart = FindProperty("_HeightFogStart", props); + heightFogEnd = FindProperty("_HeightFogEnd", props); + heightFogColor = FindProperty("_HeightFogColor", props); + } + + public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] props) + { + FindProperties(props); // MaterialProperties can be animated so we do not cache them but fetch them every event to ensure animated values are updated correctly + m_MaterialEditor = materialEditor; + Material material = materialEditor.target as Material; + + ShaderPropertiesGUI(material); + + // Make sure that needed keywords are set up if we're switching some existing + // material to a standard shader. + if (m_FirstTimeApply) + { + SetMaterialKeywords(material, m_WorkflowMode); + m_FirstTimeApply = false; + } + } + + public void ShaderPropertiesGUI(Material material) + { + // Use default labelWidth + EditorGUIUtility.labelWidth = 0f; + + // Detect any changes to the material + EditorGUI.BeginChangeCheck(); + { + BlendModePopup(); + + // Primary properties + GUILayout.Label(Styles.primaryMapsText, EditorStyles.boldLabel); + DoAlbedoArea(material); + DoSpecularMetallicArea(); + m_MaterialEditor.TexturePropertySingleLine(Styles.normalMapText, bumpMap, bumpMap.textureValue != null ? bumpScale : null); + m_MaterialEditor.TexturePropertySingleLine(Styles.heightMapText, heightMap, heightMap.textureValue != null ? heigtMapScale : null); + m_MaterialEditor.TexturePropertySingleLine(Styles.occlusionText, occlusionMap, occlusionMap.textureValue != null ? occlusionStrength : null); + DoEmissionArea(material); + m_MaterialEditor.TexturePropertySingleLine(Styles.detailMaskText, detailMask); + EditorGUI.BeginChangeCheck(); + m_MaterialEditor.TextureScaleOffsetProperty(albedoMap); + if (EditorGUI.EndChangeCheck()) + emissionMap.textureScaleAndOffset = albedoMap.textureScaleAndOffset; // Apply the main texture scale and offset to the emission texture as well, for Enlighten's sake + + EditorGUILayout.Space(); + + m_MaterialEditor.ShaderProperty(heightFogStart, Styles.heightFogStart.text); + m_MaterialEditor.ShaderProperty(heightFogEnd, Styles.heightFogEnd.text); + //m_MaterialEditor.ShaderProperty (heightFogColor, Styles.heightFogColor.text); + m_MaterialEditor.ColorProperty(heightFogColor, Styles.heightFogColor.text); + + + // Secondary properties + GUILayout.Label(Styles.secondaryMapsText, EditorStyles.boldLabel); + m_MaterialEditor.TexturePropertySingleLine(Styles.detailAlbedoText, detailAlbedoMap); + m_MaterialEditor.TexturePropertySingleLine(Styles.detailNormalMapText, detailNormalMap, detailNormalMapScale); + m_MaterialEditor.TextureScaleOffsetProperty(detailAlbedoMap); + m_MaterialEditor.ShaderProperty(uvSetSecondary, Styles.uvSetLabel.text); + } + if (EditorGUI.EndChangeCheck()) + { + foreach (var obj in blendMode.targets) + MaterialChanged((Material)obj, m_WorkflowMode); + } + } + + internal void DetermineWorkflow(MaterialProperty[] props) { - material.SetColor("_EmissionColor", material.GetColor("_Emission")); + if (FindProperty("_SpecGlossMap", props, false) != null && FindProperty("_SpecColor", props, false) != null) + m_WorkflowMode = WorkflowMode.Specular; + else if (FindProperty("_MetallicGlossMap", props, false) != null && FindProperty("_Metallic", props, false) != null) + m_WorkflowMode = WorkflowMode.Metallic; + else + m_WorkflowMode = WorkflowMode.Dielectric; } - base.AssignNewShaderToMaterial(material, oldShader, newShader); - - if (oldShader == null || !oldShader.name.Contains("Legacy Shaders/")) - return; - - BlendMode blendMode = BlendMode.Opaque; - if (oldShader.name.Contains("/Transparent/Cutout/")) - { - blendMode = BlendMode.Cutout; - } - else if (oldShader.name.Contains("/Transparent/")) - { - // NOTE: legacy shaders did not provide physically based transparency - // therefore Fade mode - blendMode = BlendMode.Fade; - } - material.SetFloat("_Mode", (float)blendMode); - - DetermineWorkflow( MaterialEditor.GetMaterialProperties (new Material[] { material }) ); - MaterialChanged(material, m_WorkflowMode); - } - - void BlendModePopup() - { - EditorGUI.showMixedValue = blendMode.hasMixedValue; - var mode = (BlendMode)blendMode.floatValue; - - EditorGUI.BeginChangeCheck(); - mode = (BlendMode)EditorGUILayout.Popup(Styles.renderingMode, (int)mode, Styles.blendNames); - if (EditorGUI.EndChangeCheck()) - { - m_MaterialEditor.RegisterPropertyChangeUndo("Rendering Mode"); - blendMode.floatValue = (float)mode; - } - - EditorGUI.showMixedValue = false; - } - - void DoAlbedoArea(Material material) - { - m_MaterialEditor.TexturePropertySingleLine(Styles.albedoText, albedoMap, albedoColor); - if (((BlendMode)material.GetFloat("_Mode") == BlendMode.Cutout)) - { - m_MaterialEditor.ShaderProperty(alphaCutoff, Styles.alphaCutoffText.text, MaterialEditor.kMiniTextureFieldLabelIndentLevel+1); - } - } - - void DoEmissionArea(Material material) - { - float brightness = emissionColorForRendering.colorValue.maxColorComponent; - bool showHelpBox = !HasValidEmissiveKeyword(material); - bool showEmissionColorAndGIControls = brightness > 0.0f; - - bool hadEmissionTexture = emissionMap.textureValue != null; - - // Texture and HDR color controls - m_MaterialEditor.TexturePropertyWithHDRColor(Styles.emissionText, emissionMap, emissionColorForRendering, m_ColorPickerHDRConfig, false); - - // If texture was assigned and color was black set color to white - if (emissionMap.textureValue != null && !hadEmissionTexture && brightness <= 0f) - emissionColorForRendering.colorValue = Color.white; - - // Dynamic Lightmapping mode - if (showEmissionColorAndGIControls) - { - bool shouldEmissionBeEnabled = ShouldEmissionBeEnabled(emissionColorForRendering.colorValue); - EditorGUI.BeginDisabledGroup(!shouldEmissionBeEnabled); - - m_MaterialEditor.LightmapEmissionProperty (MaterialEditor.kMiniTextureFieldLabelIndentLevel + 1); - - EditorGUI.EndDisabledGroup(); - } - - if (showHelpBox) - { - EditorGUILayout.HelpBox(Styles.emissiveWarning.text, MessageType.Warning); - } - } - - void DoSpecularMetallicArea() - { - if (m_WorkflowMode == WorkflowMode.Specular) - { - if (specularMap.textureValue == null) - m_MaterialEditor.TexturePropertyTwoLines(Styles.specularMapText, specularMap, specularColor, Styles.smoothnessText, smoothness); - else - m_MaterialEditor.TexturePropertySingleLine(Styles.specularMapText, specularMap); - - } - else if (m_WorkflowMode == WorkflowMode.Metallic) - { - if (metallicMap.textureValue == null) - m_MaterialEditor.TexturePropertyTwoLines(Styles.metallicMapText, metallicMap, metallic, Styles.smoothnessText, smoothness); - else - m_MaterialEditor.TexturePropertySingleLine(Styles.metallicMapText, metallicMap); - } - } - - public static void SetupMaterialWithBlendMode(Material material, BlendMode blendMode) - { - switch (blendMode) - { - case BlendMode.Opaque: - material.SetOverrideTag("RenderType", ""); - material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One); - material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero); - material.SetInt("_ZWrite", 1); - material.DisableKeyword("_ALPHATEST_ON"); - material.DisableKeyword("_ALPHABLEND_ON"); - material.DisableKeyword("_ALPHAPREMULTIPLY_ON"); - material.renderQueue = -1; - break; - case BlendMode.Cutout: - material.SetOverrideTag("RenderType", "TransparentCutout"); - material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One); - material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero); - material.SetInt("_ZWrite", 1); - material.EnableKeyword("_ALPHATEST_ON"); - material.DisableKeyword("_ALPHABLEND_ON"); - material.DisableKeyword("_ALPHAPREMULTIPLY_ON"); - material.renderQueue = 2450; - break; - case BlendMode.Fade: - material.SetOverrideTag("RenderType", "Transparent"); - material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); - material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); - material.SetInt("_ZWrite", 0); - material.DisableKeyword("_ALPHATEST_ON"); - material.EnableKeyword("_ALPHABLEND_ON"); - material.DisableKeyword("_ALPHAPREMULTIPLY_ON"); - material.renderQueue = 3000; - break; - case BlendMode.Transparent: - material.SetOverrideTag("RenderType", "Transparent"); - material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One); - material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); - material.SetInt("_ZWrite", 0); - material.DisableKeyword("_ALPHATEST_ON"); - material.DisableKeyword("_ALPHABLEND_ON"); - material.EnableKeyword("_ALPHAPREMULTIPLY_ON"); - material.renderQueue = 3000; - break; - } - } - - static bool ShouldEmissionBeEnabled (Color color) - { - return color.maxColorComponent > (0.1f / 255.0f); - } - - static void SetMaterialKeywords(Material material, WorkflowMode workflowMode) - { - // Note: keywords must be based on Material value not on MaterialProperty due to multi-edit & material animation - // (MaterialProperty value might come from renderer material property block) - SetKeyword (material, "_NORMALMAP", material.GetTexture ("_BumpMap") || material.GetTexture ("_DetailNormalMap")); - if (workflowMode == WorkflowMode.Specular) - SetKeyword (material, "_SPECGLOSSMAP", material.GetTexture ("_SpecGlossMap")); - else if (workflowMode == WorkflowMode.Metallic) - SetKeyword (material, "_METALLICGLOSSMAP", material.GetTexture ("_MetallicGlossMap")); - SetKeyword (material, "_PARALLAXMAP", material.GetTexture ("_ParallaxMap")); - SetKeyword (material, "_DETAIL_MULX2", material.GetTexture ("_DetailAlbedoMap") || material.GetTexture ("_DetailNormalMap")); - - bool shouldEmissionBeEnabled = ShouldEmissionBeEnabled (material.GetColor("_EmissionColor")); - SetKeyword (material, "_EMISSION", shouldEmissionBeEnabled); - - bool shouldHeightFogColorBeEnabled = material.GetColor ("_HeightFogColor").a > 0; - SetKeyword (material, "_HEIGHTFOGCOLOR", shouldHeightFogColorBeEnabled); - - - // Setup lightmap emissive flags - MaterialGlobalIlluminationFlags flags = material.globalIlluminationFlags; - if ((flags & (MaterialGlobalIlluminationFlags.BakedEmissive | MaterialGlobalIlluminationFlags.RealtimeEmissive)) != 0) - { - flags &= ~MaterialGlobalIlluminationFlags.EmissiveIsBlack; - if (!shouldEmissionBeEnabled) - flags |= MaterialGlobalIlluminationFlags.EmissiveIsBlack; - - material.globalIlluminationFlags = flags; - } - } - - bool HasValidEmissiveKeyword (Material material) - { - // Material animation might be out of sync with the material keyword. - // So if the emission support is disabled on the material, but the property blocks have a value that requires it, then we need to show a warning. - // (note: (Renderer MaterialPropertyBlock applies its values to emissionColorForRendering)) - bool hasEmissionKeyword = material.IsKeywordEnabled ("_EMISSION"); - if (!hasEmissionKeyword && ShouldEmissionBeEnabled (emissionColorForRendering.colorValue)) - return false; - else - return true; - } - - static void MaterialChanged(Material material, WorkflowMode workflowMode) - { - SetupMaterialWithBlendMode(material, (BlendMode)material.GetFloat("_Mode")); - - SetMaterialKeywords(material, workflowMode); - } - - static void SetKeyword(Material m, string keyword, bool state) - { - if (state) - m.EnableKeyword (keyword); - else - m.DisableKeyword (keyword); - } -} + public override void AssignNewShaderToMaterial(Material material, Shader oldShader, Shader newShader) + { + // _Emission property is lost after assigning Standard shader to the material + // thus transfer it before assigning the new shader + if (material.HasProperty("_Emission")) + { + material.SetColor("_EmissionColor", material.GetColor("_Emission")); + } + + base.AssignNewShaderToMaterial(material, oldShader, newShader); + + if (oldShader == null || !oldShader.name.Contains("Legacy Shaders/")) + return; + + BlendMode blendMode = BlendMode.Opaque; + if (oldShader.name.Contains("/Transparent/Cutout/")) + { + blendMode = BlendMode.Cutout; + } + else if (oldShader.name.Contains("/Transparent/")) + { + // NOTE: legacy shaders did not provide physically based transparency + // therefore Fade mode + blendMode = BlendMode.Fade; + } + material.SetFloat("_Mode", (float)blendMode); + + DetermineWorkflow(MaterialEditor.GetMaterialProperties(new Material[] { material })); + MaterialChanged(material, m_WorkflowMode); + } + + void BlendModePopup() + { + EditorGUI.showMixedValue = blendMode.hasMixedValue; + var mode = (BlendMode)blendMode.floatValue; + + EditorGUI.BeginChangeCheck(); + mode = (BlendMode)EditorGUILayout.Popup(Styles.renderingMode, (int)mode, Styles.blendNames); + if (EditorGUI.EndChangeCheck()) + { + m_MaterialEditor.RegisterPropertyChangeUndo("Rendering Mode"); + blendMode.floatValue = (float)mode; + } + + EditorGUI.showMixedValue = false; + } + + void DoAlbedoArea(Material material) + { + m_MaterialEditor.TexturePropertySingleLine(Styles.albedoText, albedoMap, albedoColor); + if (((BlendMode)material.GetFloat("_Mode") == BlendMode.Cutout)) + { + m_MaterialEditor.ShaderProperty(alphaCutoff, Styles.alphaCutoffText.text, MaterialEditor.kMiniTextureFieldLabelIndentLevel + 1); + } + } + + void DoEmissionArea(Material material) + { + float brightness = emissionColorForRendering.colorValue.maxColorComponent; + bool showHelpBox = !HasValidEmissiveKeyword(material); + bool showEmissionColorAndGIControls = brightness > 0.0f; + + bool hadEmissionTexture = emissionMap.textureValue != null; + + // Texture and HDR color controls + m_MaterialEditor.TexturePropertyWithHDRColor(Styles.emissionText, emissionMap, emissionColorForRendering, m_ColorPickerHDRConfig, false); + + // If texture was assigned and color was black set color to white + if (emissionMap.textureValue != null && !hadEmissionTexture && brightness <= 0f) + emissionColorForRendering.colorValue = Color.white; + + // Dynamic Lightmapping mode + if (showEmissionColorAndGIControls) + { + bool shouldEmissionBeEnabled = ShouldEmissionBeEnabled(emissionColorForRendering.colorValue); + EditorGUI.BeginDisabledGroup(!shouldEmissionBeEnabled); + + m_MaterialEditor.LightmapEmissionProperty(MaterialEditor.kMiniTextureFieldLabelIndentLevel + 1); + + EditorGUI.EndDisabledGroup(); + } + + if (showHelpBox) + { + EditorGUILayout.HelpBox(Styles.emissiveWarning.text, MessageType.Warning); + } + } + + void DoSpecularMetallicArea() + { + if (m_WorkflowMode == WorkflowMode.Specular) + { + if (specularMap.textureValue == null) + m_MaterialEditor.TexturePropertyTwoLines(Styles.specularMapText, specularMap, specularColor, Styles.smoothnessText, smoothness); + else + m_MaterialEditor.TexturePropertySingleLine(Styles.specularMapText, specularMap); + + } + else if (m_WorkflowMode == WorkflowMode.Metallic) + { + if (metallicMap.textureValue == null) + m_MaterialEditor.TexturePropertyTwoLines(Styles.metallicMapText, metallicMap, metallic, Styles.smoothnessText, smoothness); + else + m_MaterialEditor.TexturePropertySingleLine(Styles.metallicMapText, metallicMap); + } + } + + public static void SetupMaterialWithBlendMode(Material material, BlendMode blendMode) + { + switch (blendMode) + { + case BlendMode.Opaque: + material.SetOverrideTag("RenderType", ""); + material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One); + material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero); + material.SetInt("_ZWrite", 1); + material.DisableKeyword("_ALPHATEST_ON"); + material.DisableKeyword("_ALPHABLEND_ON"); + material.DisableKeyword("_ALPHAPREMULTIPLY_ON"); + material.renderQueue = -1; + break; + case BlendMode.Cutout: + material.SetOverrideTag("RenderType", "TransparentCutout"); + material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One); + material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero); + material.SetInt("_ZWrite", 1); + material.EnableKeyword("_ALPHATEST_ON"); + material.DisableKeyword("_ALPHABLEND_ON"); + material.DisableKeyword("_ALPHAPREMULTIPLY_ON"); + material.renderQueue = 2450; + break; + case BlendMode.Fade: + material.SetOverrideTag("RenderType", "Transparent"); + material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); + material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); + material.SetInt("_ZWrite", 0); + material.DisableKeyword("_ALPHATEST_ON"); + material.EnableKeyword("_ALPHABLEND_ON"); + material.DisableKeyword("_ALPHAPREMULTIPLY_ON"); + material.renderQueue = 3000; + break; + case BlendMode.Transparent: + material.SetOverrideTag("RenderType", "Transparent"); + material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One); + material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); + material.SetInt("_ZWrite", 0); + material.DisableKeyword("_ALPHATEST_ON"); + material.DisableKeyword("_ALPHABLEND_ON"); + material.EnableKeyword("_ALPHAPREMULTIPLY_ON"); + material.renderQueue = 3000; + break; + } + } + + static bool ShouldEmissionBeEnabled(Color color) + { + return color.maxColorComponent > (0.1f / 255.0f); + } + + static void SetMaterialKeywords(Material material, WorkflowMode workflowMode) + { + // Note: keywords must be based on Material value not on MaterialProperty due to multi-edit & material animation + // (MaterialProperty value might come from renderer material property block) + SetKeyword(material, "_NORMALMAP", material.GetTexture("_BumpMap") || material.GetTexture("_DetailNormalMap")); + if (workflowMode == WorkflowMode.Specular) + SetKeyword(material, "_SPECGLOSSMAP", material.GetTexture("_SpecGlossMap")); + else if (workflowMode == WorkflowMode.Metallic) + SetKeyword(material, "_METALLICGLOSSMAP", material.GetTexture("_MetallicGlossMap")); + SetKeyword(material, "_PARALLAXMAP", material.GetTexture("_ParallaxMap")); + SetKeyword(material, "_DETAIL_MULX2", material.GetTexture("_DetailAlbedoMap") || material.GetTexture("_DetailNormalMap")); + + bool shouldEmissionBeEnabled = ShouldEmissionBeEnabled(material.GetColor("_EmissionColor")); + SetKeyword(material, "_EMISSION", shouldEmissionBeEnabled); + + bool shouldHeightFogColorBeEnabled = material.GetColor("_HeightFogColor").a > 0; + SetKeyword(material, "_HEIGHTFOGCOLOR", shouldHeightFogColorBeEnabled); + + + // Setup lightmap emissive flags + MaterialGlobalIlluminationFlags flags = material.globalIlluminationFlags; + if ((flags & (MaterialGlobalIlluminationFlags.BakedEmissive | MaterialGlobalIlluminationFlags.RealtimeEmissive)) != 0) + { + flags &= ~MaterialGlobalIlluminationFlags.EmissiveIsBlack; + if (!shouldEmissionBeEnabled) + flags |= MaterialGlobalIlluminationFlags.EmissiveIsBlack; + + material.globalIlluminationFlags = flags; + } + } + + bool HasValidEmissiveKeyword(Material material) + { + // Material animation might be out of sync with the material keyword. + // So if the emission support is disabled on the material, but the property blocks have a value that requires it, then we need to show a warning. + // (note: (Renderer MaterialPropertyBlock applies its values to emissionColorForRendering)) + bool hasEmissionKeyword = material.IsKeywordEnabled("_EMISSION"); + if (!hasEmissionKeyword && ShouldEmissionBeEnabled(emissionColorForRendering.colorValue)) + return false; + else + return true; + } + + static void MaterialChanged(Material material, WorkflowMode workflowMode) + { + SetupMaterialWithBlendMode(material, (BlendMode)material.GetFloat("_Mode")); + + SetMaterialKeywords(material, workflowMode); + } + + static void SetKeyword(Material m, string keyword, bool state) + { + if (state) + m.EnableKeyword(keyword); + else + m.DisableKeyword(keyword); + } + } } // namespace UnityEditor