mirror of
https://github.com/OpenSpace/OpenSpace.git
synced 2026-01-01 01:01:37 -06:00
361 lines
11 KiB
Python
361 lines
11 KiB
Python
##########################################################################################
|
|
# #
|
|
# OpenSpace #
|
|
# #
|
|
# Copyright (c) 2014-2025 #
|
|
# #
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy of this #
|
|
# software and associated documentation files (the "Software"), to deal in the Software #
|
|
# without restriction, including without limitation the rights to use, copy, modify, #
|
|
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to #
|
|
# permit persons to whom the Software is furnished to do so, subject to the following #
|
|
# conditions: #
|
|
# #
|
|
# The above copyright notice and this permission notice shall be included in all copies #
|
|
# or substantial portions of the Software. #
|
|
# #
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, #
|
|
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A #
|
|
# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT #
|
|
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF #
|
|
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE #
|
|
# OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #
|
|
##########################################################################################
|
|
|
|
from openspace import Api
|
|
import asyncio
|
|
import json
|
|
import os
|
|
from tabulate import tabulate
|
|
|
|
async def createCommandNavigationState(openspace):
|
|
include = ""
|
|
while include != "y" and include != "n":
|
|
include = input("Include timestamp? (y/n, default 'y'): ")
|
|
include = include or "y"
|
|
|
|
navstate = await openspace.navigation.getNavigationState()
|
|
res = {
|
|
"type": "navigationstate",
|
|
"value": {
|
|
"anchor": navstate["Anchor"],
|
|
"position": navstate["Position"]
|
|
}
|
|
}
|
|
|
|
if "Aim" in navstate:
|
|
res["value"]["aim"] = navstate["Aim"]
|
|
if "Pitch" in navstate:
|
|
res["value"]["pitch"] = navstate["Pitch"]
|
|
if "ReferenceFrame" in navstate:
|
|
res["value"]["referenceFrame"] = navstate["ReferenceFrame"]
|
|
if "Up" in navstate:
|
|
res["value"]["up"] = navstate["Up"]
|
|
if "Yaw" in navstate:
|
|
res["value"]["yaw"] = navstate["Yaw"]
|
|
|
|
if include == "y" and ("Timestamp" in navstate):
|
|
res["value"]["timestamp"] = navstate["Timestamp"]
|
|
|
|
return res
|
|
|
|
|
|
async def createCommandAsset(openspace):
|
|
assets = await openspace.asset.rootAssets()
|
|
folder = await openspace.absPath("${ASSETS}")
|
|
assets = [asset[len(folder)+1:asset.find(".")] for index,asset in assets.items()]
|
|
assets.sort()
|
|
|
|
print("List of assets:")
|
|
for index,asset in enumerate(assets):
|
|
print( f" ({index+1}): {asset}")
|
|
|
|
asset = ""
|
|
while not asset.isnumeric():
|
|
asset = input("Select which asset to load: ")
|
|
|
|
if asset == "":
|
|
return None
|
|
|
|
idx = int(asset) - 1
|
|
|
|
if idx < 0 or idx >= len(assets):
|
|
print("Invalid index")
|
|
continue
|
|
|
|
asset = assets[idx]
|
|
asset = asset.replace("\\", "/")
|
|
|
|
return {
|
|
"type": "asset",
|
|
"value": f"{asset}.asset"
|
|
}
|
|
|
|
|
|
async def createCommandProperty(openspace, uri=None):
|
|
if not uri:
|
|
uri = input("URI of the property: ")
|
|
if uri == "":
|
|
return None
|
|
|
|
if uri.find("*") != -1 or uri.find("{") != -1:
|
|
# The URI refers to multiple property values, so we need to as for the specific value
|
|
print("As multiple properties are specified, it is not possible to query their")
|
|
print("current state from OpenSpace. Requesting the value manually. For boolean")
|
|
print("values enter 'true' or 'false', numerical and string values can be entered ")
|
|
print("directly. Vectors can currently not be entered manually.")
|
|
print()
|
|
value = input("Enter value of the property: ")
|
|
|
|
# Perform some type conversions if necessary
|
|
if value.lower() == "true":
|
|
value = True
|
|
elif value.lower() == "false":
|
|
value = False
|
|
|
|
if value.isnumeric():
|
|
value = float(value)
|
|
else:
|
|
has_property = await openspace.hasProperty(uri)
|
|
if not has_property:
|
|
print(f"Property '{uri}' not found")
|
|
return None
|
|
|
|
# This is a property identifier that we can use to request a value for
|
|
value = await openspace.propertyValue(uri)
|
|
|
|
return {
|
|
"type": "property",
|
|
"value": {
|
|
"property": uri,
|
|
"value": value
|
|
}
|
|
}
|
|
|
|
|
|
async def createCommandWait(openspace):
|
|
seconds = input("Number of seconds to wait: ")
|
|
if seconds == "":
|
|
return None
|
|
|
|
return {
|
|
"type": "wait",
|
|
"value": float(seconds)
|
|
}
|
|
|
|
|
|
async def createCommandScript(openspace):
|
|
script = input("Script that should be executed: ")
|
|
|
|
return {
|
|
"type": "script",
|
|
"value": script
|
|
}
|
|
|
|
|
|
async def createCommandTime(openspace):
|
|
time = await openspace.time.UTC()
|
|
|
|
return {
|
|
"type": "time",
|
|
"value": time
|
|
}
|
|
|
|
|
|
async def createCommandPause(openspace):
|
|
pause = input("New pause state: (y/n). Default 'y': ")
|
|
pause = pause or "y"
|
|
|
|
p = ""
|
|
if pause == "y":
|
|
p = True
|
|
else:
|
|
p = False
|
|
|
|
return {
|
|
"type": "pause",
|
|
"value": p
|
|
}
|
|
|
|
|
|
async def createCommandDeltatime(openspace):
|
|
dt = await openspace.time.deltaTime()
|
|
|
|
return {
|
|
"type": "deltatime",
|
|
"value": dt
|
|
}
|
|
|
|
|
|
async def createCommandAction(openspace):
|
|
actions = await openspace.action.actions()
|
|
actions = [action for index,action in actions.items()]
|
|
|
|
print("List of actions:")
|
|
for index,action in enumerate(actions):
|
|
print( f" ({index+1}): {action["Name"]} ({action["Identifier"]})")
|
|
|
|
action = ""
|
|
while not action.isnumeric():
|
|
action = input("Select which action to execute: ")
|
|
|
|
if action == "":
|
|
return None
|
|
|
|
idx = int(action) - 1
|
|
|
|
if idx < 0 or idx >= len(actions):
|
|
print("Invalid index")
|
|
continue
|
|
|
|
action = actions[idx]
|
|
|
|
return {
|
|
"type": "action",
|
|
"value": action["Identifier"]
|
|
}
|
|
|
|
|
|
AllCommands = [
|
|
[
|
|
"Navigation state",
|
|
createCommandNavigationState,
|
|
"Stores the current position of the camera (and optionally the time). When the regression test is run, the saved camera position is restored."
|
|
],
|
|
[
|
|
"Asset",
|
|
createCommandAsset,
|
|
"Causes the regression test to load a specific asset file that needs to be provided manually."
|
|
],
|
|
[
|
|
"Property",
|
|
createCommandProperty,
|
|
"Requests the current value of a specified property. When the regression test is run, the specified property will be set to the stored value."
|
|
],
|
|
[
|
|
"Wait",
|
|
createCommandWait,
|
|
"Causes the test to wait for a specified number of seconds before progressing to the next command in the test."
|
|
],
|
|
[
|
|
"Script",
|
|
createCommandScript,
|
|
"Requests a Lua script that will be executed by the regression test."
|
|
],
|
|
[
|
|
"Time",
|
|
createCommandTime,
|
|
"Stores the current in-game time and restore it when the regression test is run."
|
|
],
|
|
[
|
|
"Pause",
|
|
createCommandPause,
|
|
"Will either pause or resume the simulation time while running the test. This command should generally not be used as a changing clock can generally easily trigger small deviations that will cause image tests to fail."
|
|
],
|
|
[
|
|
"Deltatime",
|
|
createCommandDeltatime,
|
|
"Requests a value for the delta time, which will the be set when the regression test is run. This command should not be used as it requires the in-game time to be unpaused, which can easily trigger small deviations that cause image tests to fail."
|
|
],
|
|
[
|
|
"Action",
|
|
createCommandAction,
|
|
"Provides a selection of registered actions that can be triggered in the regression test."
|
|
]
|
|
]
|
|
|
|
|
|
async def internalRun(openspace):
|
|
test = {}
|
|
|
|
profile = await openspace.profileName()
|
|
profile = profile[:profile.find('.')]
|
|
profile = profile.replace("\\", "/")
|
|
test["profile"] = profile
|
|
|
|
name = input("Enter the name of the test: ")
|
|
if name == "":
|
|
raise "Must provide a name for the test"
|
|
|
|
test["commands"] = []
|
|
|
|
|
|
while True:
|
|
print()
|
|
print()
|
|
print("Select command to add to the test:")
|
|
for index,cmd in enumerate(AllCommands):
|
|
print(f" ({index+1}): {cmd[0]}")
|
|
print("Finalize the test by typing 's' or 'save'.")
|
|
selection = input("> ")
|
|
if selection == "s" or selection == "save":
|
|
break
|
|
|
|
if not selection.isnumeric():
|
|
print("Invalid index")
|
|
continue
|
|
|
|
idx = int(selection) - 1
|
|
|
|
if idx < 0 or idx >= len(AllCommands):
|
|
print("Invalid index")
|
|
continue
|
|
|
|
cmd = AllCommands[idx]
|
|
func = cmd[1]
|
|
command = await func(openspace)
|
|
if command:
|
|
if cmd[0] == "Navigation state":
|
|
# If we have a navigation state, we make the users life easier if we check the
|
|
# limit zoom value here and if it is not the default, offer to add a property
|
|
# change to make it fit. Otherwise there is the danger of the camera being placed
|
|
# too far away as the camera limit is hit
|
|
limitZoom = await openspace.propertyValue(
|
|
"NavigationHandler.OrbitalNavigator.LimitZoom.EnableMinimumAllowedDistance"
|
|
)
|
|
if not limitZoom:
|
|
print("The 'Limit Zoom' of the camera is disabled. Do you want to add a "
|
|
"command so that it is disabled in the test as well? (y/n). Default 'y'")
|
|
addCommand = input("> ")
|
|
if addCommand.lower() == "y" or addCommand.lower() == "":
|
|
c = await createCommandProperty(
|
|
openspace,
|
|
"NavigationHandler.OrbitalNavigator.LimitZoom.EnableMinimumAllowedDistance"
|
|
)
|
|
test["commands"].append(c)
|
|
|
|
print(f"Adding command {cmd[0]}")
|
|
test["commands"].append(command)
|
|
|
|
# Add screenshot command at the end
|
|
test["commands"].append({ "type": "screenshot" })
|
|
|
|
# Save the test to disk
|
|
print()
|
|
print(f"Saving test: {os.path.abspath(name)}.ostest")
|
|
with open(f"{name}.ostest", "w") as fp:
|
|
json.dump(test, fp, indent=2)
|
|
fp.write("\n")
|
|
|
|
|
|
async def mainLoop():
|
|
api = Api("localhost", 4681)
|
|
api.connect()
|
|
openspace = await api.singleReturnLibrary()
|
|
|
|
await asyncio.create_task(internalRun(openspace))
|
|
api.disconnect()
|
|
|
|
|
|
print("OpenSpace Visual Test Creation Wizard")
|
|
print("=====================================")
|
|
print("This wizard helps creating image regression test files. To use it, first start ")
|
|
print("OpenSpace with the profile for which to create a test. Then, execute this script ")
|
|
print("and select the commands in the order in which the test should execute them. ")
|
|
print("To finish the test, enter 's' or 'save' at the prompt.")
|
|
print()
|
|
print(tabulate([[x[0], x[2]] for x in AllCommands], tablefmt="plain", maxcolwidths=[20, 60]))
|
|
print()
|
|
|
|
asyncio.run(mainLoop())
|