07. Aire de jeux Basis & Mount
Contrôle de manière interactive l'appareil transformation de montage et montre comment configure.basis, configure.presetet configure.mount travailler ensemble sur le fil. Cela permet de conserver une barre d'état horizontale fixe afin que l'attention reste concentrée sur les commandes de configuration.
Ce que vous apprendrez :
- Définir un permutation de base (
"XZY"→ Cadre de fixation Y-up) - Choisir un préréglage (
arm_front) et comprendre ce qu'il configure - Remplacer la configuration de la monture à l'exécution par un quaternion de rotation (à l'aide du clavier)
- La règle d'exclusion mutuelle :
mountetpresetne peuvent pas coexister dans le même configuration de l'appareil bloc - One-shot
configuresemantique : chaque pression sur une touche envoie exactement un message de configuration - (C++ Glaze) Modélisation de champs mutuellement exclusifs avec
std::optional
Flux de travail
- Au premier message : envoyer le profil de session,
configure.basis: "XZY"etconfigure.preset: arm_front. Commencer à envoyerset_cursor_forcepour le plancher fixe. - À chaque tick : lire la coordonnée Y du curseur, calculer
force_y = max(0, (floor_pos - y) * stiffness), envoyez-le. - Lorsque l'utilisateur appuie sur une touche de rotation de la monture, drapeau
pending_configure = true. - Au prochain tick : créer un
configure.mountbloc avec une transformation dontrotationest un quaternion unitaire (composition Z puis X de l'inclinaison et du lacet actuels). Omettrepreset— les deux sont incompatibles sur le réseau. - Touche de réinitialisation (
R) annule la redéfinition ; la configuration suivante revient àpresetencore une fois.
Paramètres
| Nom | Par défaut | Objectif |
|---|---|---|
BASIS | "XZY" | Permutation d'axes — Cadre d'application Y-up |
DEVICE_PRESET / DEVICE_CONFIG_PRESET | "arm_front" | Préréglage nommé — origine à la base de l'appareil |
FLOOR_POS_Y | 0.0 m | Plan de sol fixe (application Y) |
STIFFNESS | 1000 N/m | Constante de ressort au sol |
MOUNT_STEP_DEG | 10° | Rotation par pression sur une touche |
PRINT_EVERY_MS | 200 | Manette de télémétrie |
Commandes
| Clé | Action |
|---|---|
W / S | Faire pivoter le support de ±10° autour de l'axe +X de l'appareil (inclinaison) |
A / D | Faire pivoter le support de ±10° autour de l'axe +Z de l'appareil (lacet) |
R | Réinitialiser le montage — revenir au préréglage |
H | Afficher les commandes |
Q | Quitter |
mount et preset s'excluent mutuellementLe service rejette un configuration de l'appareil bloc contenant les deux. Une fois que l'utilisateur a remplacé le montage, le tutoriel omet preset à chaque exécution ultérieure de configure. En appuyant sur R réactive preset lors de la prochaine configuration et des suppressions mount.
Les variantes en C++ lisent les entrées ligne par ligne dans un thread stdin s'exécutant en arrière-plan (appuyez sur Entrée après chaque lettre). Python utilise le keyboard Bibliothèque permettant l'interrogation des touches en temps réel dans la boucle asynchrone principale — aucune pression sur la touche Entrée n'est nécessaire. Mêmes touches, mêmes commandes.
Champs d'état lus
De data.inverse3[i].state:
cursor_position.y—vec3, utilisé pour calculer la pénétration dans le solcurrent_cursor_force— transmis par télémétrie
Envoyer / recevoir
La structure de la charge utile est identique pour toutes les variantes ; ce qui est intéressant, ce sont les différences dans la manière dont chacune construit les éléments mutuellement exclusifs mount / preset la ramification et la manière dont le thread d'entrée envoie un signal au thread WebSocket.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Boucle asynchrone unique avec interrogation des touches en temps réel via le keyboard paquet. pending_configure est un indicateur global activé par les gestionnaires de clés et réinitialisé à chaque fois qu'un configure le bloc est envoyé.
async with websockets.connect(URI) as websocket:
while True:
msg = await websocket.recv()
data = json.loads(msg)
if first_message:
first_message = False
device_id = data["inverse3"][0]["device_id"]
# Handshake: profile + basis + preset
request_msg = {
"session": {"configure": {"profile": {"name": SLUG}}},
"inverse3": [{
"device_id": device_id,
"configure": build_configure_block(first_handshake=True),
# -> {"basis": {"permutation": "XZY"},
# "preset": {"preset": "arm_front"}}
}],
}
else:
handle_key_inputs() # may set pending_configure = True (classic, not shown)
y = data["inverse3"][0]["state"]["cursor_position"]["y"]
force_y = 0.0 if y > FLOOR_POS_Y else (FLOOR_POS_Y - y) * STIFFNESS
entry = {
"device_id": device_id,
"commands": {"set_cursor_force":
{"vector": {"x": 0.0, "y": force_y, "z": 0.0}}},
}
if pending_configure:
entry["configure"] = build_configure_block(first_handshake=False)
# -> {"mount": {...}} OR {"preset": {...}} (never both)
pending_configure = False
request_msg = {"inverse3": [entry]}
await websocket.send(json.dumps(request_msg))
Modèle à deux threads : un thread d'arrière-plan chargé de lire les lignes via stdin et de les traiter pending_configure (un std::atomic<bool>); le thread d'E/S de libhv le vérifie à chaque tick et émet configure lorsqu'il est défini.
std::atomic<bool> pending_configure{false};
ws.onmessage = [&](const std::string &msg) {
const json data = json::parse(msg);
if (!data.contains("inverse3") || data["inverse3"].empty()) return;
const bool do_handshake = first_message;
if (first_message) first_message = false;
const bool do_configure = do_handshake || pending_configure.exchange(false);
json request = {};
if (do_handshake) {
request["session"] = {{"configure", {{"profile",
{{"name", "co.haply.inverse.tutorials:basis-and-mount"}}}}}};
}
request["inverse3"] = json::array();
for (auto &el : data["inverse3"].items()) {
json dev_cmd = {{"device_id", el.value()["device_id"]}};
if (do_configure) {
json cfg = {};
if (do_handshake) cfg["basis"] = {{"permutation", BASIS}};
if (mount_overridden) {
cfg["mount"] = {{"transform", {
{"position", {{"x", 0.0}, {"y", 0.0}, {"z", 0.0}}},
{"rotation", quat_from_xz_deg(mount_angle_x_deg, mount_angle_z_deg)},
{"scale", {{"x", 1.0}, {"y", 1.0}, {"z", 1.0}}},
}}};
} else {
cfg["preset"] = {{"preset", DEVICE_CONFIG_PRESET}};
}
dev_cmd["configure"] = cfg;
}
const float y = el.value()["state"]["cursor_position"]["y"].get<float>();
const float force_y = y > FLOOR_POS_Y ? 0.0f : (FLOOR_POS_Y - y) * STIFFNESS;
dev_cmd["commands"] = {{"set_cursor_force",
{{"vector", {{"x", 0.0}, {"y", force_y}, {"z", 0.0}}}}}};
request["inverse3"].push_back(dev_cmd);
}
ws.send(request.dump());
};
std::thread input_thr(input_thread_func); // stdin reader — flips pending_configure
ws.open("ws://localhost:10001");
while (running.load()) std::this_thread::sleep_for(50ms);
La règle d'exclusion mutuelle se traduit naturellement par std::optional<preset_cfg> et std::optional<mount_cfg>: seul celui qui contient des données apparaît dans le JSON sérialisé. Le nom du préréglage est représenté sous la forme d'un enum class avec un glz::meta spécialisation qui la convertit en la chaîne de caractères attendue par le service.
// The preset set modelled as an enum
enum class device_preset {
defaults, arm_front, arm_front_centered,
led_front, led_front_centered, custom,
};
// Glaze meta — serialize the enum as the JSON string the service expects
template <> struct glz::meta<device_preset> {
using enum device_preset;
static constexpr auto value =
enumerate(defaults, arm_front, arm_front_centered,
led_front, led_front_centered, custom);
};
// Transform + the configure block
struct vec3 { float x{}, y{}, z{}; };
struct quat { float w{1.0f}, x{}, y{}, z{}; };
struct transform_t { vec3 position{}; quat rotation{}; vec3 scale{1,1,1}; };
struct preset_cfg { device_preset preset; };
struct basis_cfg { std::string permutation; };
struct mount_cfg { transform_t transform; };
struct device_configure {
std::optional<preset_cfg> preset; // mutually exclusive with mount
std::optional<basis_cfg> basis;
std::optional<mount_cfg> mount; // mutually exclusive with preset
};
// Send / receive
ws.onmessage = [&](const std::string &msg) {
devices_message data{};
if (glz::read<glz_settings>(data, msg)) return;
if (data.inverse3.empty()) return;
const bool do_handshake = first_message.exchange(false);
const bool do_configure = do_handshake || pending_configure.exchange(false);
commands_message out_cmds{};
if (do_handshake) {
out_cmds.session = session_cmd{ /* profile = basis-and-mount */ };
}
for (const auto &dev : data.inverse3) {
device_commands dc{ .device_id = dev.device_id };
if (do_configure) {
device_configure cfg{};
if (do_handshake) cfg.basis = basis_cfg{BASIS};
if (mount_overridden) {
cfg.mount = mount_cfg{ .transform = transform_t{
.rotation = quat_from_xz_deg(mount_angle_x_deg, mount_angle_z_deg)}};
} else {
cfg.preset = preset_cfg{DEVICE_CONFIG_PRESET};
}
dc.configure = std::move(cfg);
}
const float y = dev.state.cursor_position.y;
const float force_y = y > FLOOR_POS_Y ? 0.0f : (FLOOR_POS_Y - y) * STIFFNESS;
dc.commands.set_cursor_force = set_cursor_force_cmd{{0.0f, force_y, 0.0f}};
out_cmds.inverse3.push_back(std::move(dc));
}
std::string out_json;
(void)glz::write_json(out_cmds, out_json);
ws.send(out_json);
};
std::thread input_thr(input_thread_func);
ws.open("ws://localhost:10001");
while (running.load()) std::this_thread::sleep_for(50ms);
Source : Python · C++ · C++ Glaze
À lire également : Permutation de base · Support et espace de travail · Configuration de l'appareil · Commandes de contrôle (set_cursor_force) · Types (transformation) · Tutoriel 04 (Bonjour Floor)