v0.4: Official Houdini package system integration
- hermes_houdini_bridge.json: package descriptor (generated by install.sh) - scripts/python/hermes_bridge_panel.py: moved to package-standard location - python_panels/hermes_bridge.pypanel: registered in Windows menu - install.sh: one-command installer (generates JSON with correct path) - git clone + install.sh = fully installed; git pull = update - Panel opens via Windows -> Hermes Bridge (no Python Shell needed)
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
__pycache__/
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"hpath": "/home/niklas/Documents/projects_privat/hermes-houdini-bridge",
|
||||||
|
"env": [
|
||||||
|
{"HHB_PACKAGE": "/home/niklas/Documents/projects_privat/hermes-houdini-bridge"}
|
||||||
|
],
|
||||||
|
"load_package_once": true,
|
||||||
|
"enable": "houdini_version >= '21.0'"
|
||||||
|
}
|
||||||
Executable
+39
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
# Hermes-Houdini Bridge — Installer
|
||||||
|
# Generates the Houdini package descriptor with the correct
|
||||||
|
# absolute path to this repo, so Houdini finds the panel.
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
HOUDINI_PREFS="${HOUDINI_USER_PREF_DIR:-$HOME/houdini21.0}"
|
||||||
|
PACKAGES_DIR="$HOUDINI_PREFS/packages"
|
||||||
|
DESCRIPTOR="$PACKAGES_DIR/hermes_houdini_bridge.json"
|
||||||
|
|
||||||
|
echo "=== Hermes-Houdini Bridge Installer ==="
|
||||||
|
echo "Repo: $REPO_DIR"
|
||||||
|
echo "Houdini: $HOUDINI_PREFS"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
mkdir -p "$PACKAGES_DIR"
|
||||||
|
|
||||||
|
cat > "$DESCRIPTOR" << EOF
|
||||||
|
{
|
||||||
|
"hpath": "$REPO_DIR",
|
||||||
|
"env": [
|
||||||
|
{"HHB_PACKAGE": "$REPO_DIR"}
|
||||||
|
],
|
||||||
|
"load_package_once": true,
|
||||||
|
"enable": "houdini_version >= '21.0'"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "✓ Package descriptor installed"
|
||||||
|
echo " $DESCRIPTOR"
|
||||||
|
echo ""
|
||||||
|
echo "Restart Houdini, then find the panel under:"
|
||||||
|
echo " Windows → Hermes Bridge"
|
||||||
|
echo ""
|
||||||
|
echo "To update: cd $REPO_DIR && git pull"
|
||||||
|
echo "To uninstall: rm $DESCRIPTOR"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"interface": "hermes_bridge_panel.show",
|
||||||
|
"label": "Hermes Bridge",
|
||||||
|
"icon": "MISC_python",
|
||||||
|
"help_url": "https://git.niklashmotion.art/Hermes/hermes-houdini-bridge"
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
"""
|
||||||
|
Hermes Bridge Panel — Mini UI for controlling the Hermes-Houdini Bridge.
|
||||||
|
Run in Houdini Python Shell: exec(open('/path/to/hermes_bridge_panel.py').read())
|
||||||
|
Or install as a permanent Python Panel.
|
||||||
|
"""
|
||||||
|
import hou
|
||||||
|
import socket
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PySide2 import QtWidgets, QtCore, QtGui
|
||||||
|
except ImportError:
|
||||||
|
from PySide6 import QtWidgets, QtCore, QtGui
|
||||||
|
|
||||||
|
|
||||||
|
# ── Config persistence ──────────────────────────────────
|
||||||
|
CONFIG_FILE = os.path.join(hou.homeHoudiniDirectory(), "hermes_bridge.json")
|
||||||
|
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
try:
|
||||||
|
with open(CONFIG_FILE) as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
return {"port": 12345}
|
||||||
|
|
||||||
|
|
||||||
|
def save_config(cfg):
|
||||||
|
with open(CONFIG_FILE, "w") as f:
|
||||||
|
json.dump(cfg, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Bridge logic ────────────────────────────────────────
|
||||||
|
def bridge_status(port):
|
||||||
|
result = hou.hscript("openport")[0]
|
||||||
|
return f"Port {port} is open" in result
|
||||||
|
|
||||||
|
|
||||||
|
def open_bridge(port):
|
||||||
|
hou.hscript(f"closeport {port}")
|
||||||
|
result, _ = hou.hscript(f"openport {port} 0.0.0.0")
|
||||||
|
return result.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def close_bridge(port):
|
||||||
|
result, _ = hou.hscript(f"closeport {port}")
|
||||||
|
return result.strip()
|
||||||
|
|
||||||
|
|
||||||
|
# ── UI ──────────────────────────────────────────────────
|
||||||
|
class HermesBridgePanel(QtWidgets.QWidget):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.cfg = load_config()
|
||||||
|
self.port = self.cfg["port"]
|
||||||
|
self.hostname = socket.gethostname()
|
||||||
|
self._init_ui()
|
||||||
|
self._refresh_status()
|
||||||
|
|
||||||
|
def _init_ui(self):
|
||||||
|
self.setWindowTitle("Hermes Bridge")
|
||||||
|
self.setMinimumWidth(380)
|
||||||
|
self.setStyleSheet("""
|
||||||
|
QWidget {
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
QPushButton {
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
QPushButton#btnToggle {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #e0e0e0;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
QPushButton#btnToggle[active="true"] {
|
||||||
|
background: #1a3a1a;
|
||||||
|
border: 1px solid #4a4;
|
||||||
|
}
|
||||||
|
QPushButton#btnCopy {
|
||||||
|
background: #1565c0;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
QPushButton#btnCopy:hover {
|
||||||
|
background: #1976d2;
|
||||||
|
}
|
||||||
|
QLineEdit, QSpinBox {
|
||||||
|
background: #2d2d2d;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
QLabel#statusLabel {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
QLabel#hintLabel {
|
||||||
|
color: #888;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
layout = QtWidgets.QVBoxLayout()
|
||||||
|
layout.setSpacing(10)
|
||||||
|
layout.setContentsMargins(16, 16, 16, 16)
|
||||||
|
|
||||||
|
# ── Header ──────────────────────────────────────
|
||||||
|
header = QtWidgets.QHBoxLayout()
|
||||||
|
icon = QtWidgets.QLabel("\U0001f916") # robot
|
||||||
|
icon.setStyleSheet("font-size: 24px;")
|
||||||
|
title = QtWidgets.QLabel("Hermes Bridge")
|
||||||
|
title.setStyleSheet("font-size: 18px; font-weight: bold; color: #fff;")
|
||||||
|
header.addWidget(icon)
|
||||||
|
header.addWidget(title)
|
||||||
|
header.addStretch()
|
||||||
|
layout.addLayout(header)
|
||||||
|
|
||||||
|
# ── Status ──────────────────────────────────────
|
||||||
|
self.status_label = QtWidgets.QLabel("\u25cf Not connected")
|
||||||
|
self.status_label.setObjectName("statusLabel")
|
||||||
|
self.status_label.setStyleSheet("color: #ff5555;")
|
||||||
|
layout.addWidget(self.status_label)
|
||||||
|
|
||||||
|
# ── Port control ────────────────────────────────
|
||||||
|
port_layout = QtWidgets.QHBoxLayout()
|
||||||
|
port_layout.addWidget(QtWidgets.QLabel("Port:"))
|
||||||
|
self.port_spin = QtWidgets.QSpinBox()
|
||||||
|
self.port_spin.setRange(1024, 65535)
|
||||||
|
self.port_spin.setValue(self.port)
|
||||||
|
self.port_spin.setFixedWidth(100)
|
||||||
|
self.port_spin.valueChanged.connect(self._on_port_changed)
|
||||||
|
port_layout.addWidget(self.port_spin)
|
||||||
|
port_layout.addStretch()
|
||||||
|
layout.addLayout(port_layout)
|
||||||
|
|
||||||
|
# ── Toggle button ───────────────────────────────
|
||||||
|
self.btn_toggle = QtWidgets.QPushButton("Open Bridge")
|
||||||
|
self.btn_toggle.setObjectName("btnToggle")
|
||||||
|
self.btn_toggle.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
|
||||||
|
self.btn_toggle.clicked.connect(self._toggle_bridge)
|
||||||
|
layout.addWidget(self.btn_toggle)
|
||||||
|
|
||||||
|
# ── Prompt preset ───────────────────────────────
|
||||||
|
prompt_label = QtWidgets.QLabel("Copy this to Hermes:")
|
||||||
|
prompt_label.setObjectName("hintLabel")
|
||||||
|
layout.addWidget(prompt_label)
|
||||||
|
|
||||||
|
prompt_layout = QtWidgets.QHBoxLayout()
|
||||||
|
self.prompt_field = QtWidgets.QLineEdit()
|
||||||
|
self.prompt_field.setReadOnly(True)
|
||||||
|
self.prompt_field.setText(self._build_prompt())
|
||||||
|
self.prompt_field.setCursorPosition(0)
|
||||||
|
prompt_layout.addWidget(self.prompt_field)
|
||||||
|
|
||||||
|
self.btn_copy = QtWidgets.QPushButton("Copy")
|
||||||
|
self.btn_copy.setObjectName("btnCopy")
|
||||||
|
self.btn_copy.setFixedWidth(70)
|
||||||
|
self.btn_copy.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
|
||||||
|
self.btn_copy.clicked.connect(self._copy_prompt)
|
||||||
|
prompt_layout.addWidget(self.btn_copy)
|
||||||
|
layout.addLayout(prompt_layout)
|
||||||
|
|
||||||
|
# ── Alternatives ────────────────────────────────
|
||||||
|
alt_label = QtWidgets.QLabel(
|
||||||
|
"Or just say: \"Hermes, I have Houdini open\""
|
||||||
|
)
|
||||||
|
alt_label.setObjectName("hintLabel")
|
||||||
|
alt_label.setWordWrap(True)
|
||||||
|
layout.addWidget(alt_label)
|
||||||
|
|
||||||
|
# ── Bottom ──────────────────────────────────────
|
||||||
|
layout.addStretch()
|
||||||
|
version = QtWidgets.QLabel("v1.0 | hhb " + self.hostname)
|
||||||
|
version.setObjectName("hintLabel")
|
||||||
|
layout.addWidget(version)
|
||||||
|
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def _build_prompt(self):
|
||||||
|
return (
|
||||||
|
f"Hey Hermes, I have a Houdini session running on "
|
||||||
|
f"{self.hostname} (port {self.port})."
|
||||||
|
)
|
||||||
|
|
||||||
|
def _refresh_status(self):
|
||||||
|
is_open = bridge_status(self.port)
|
||||||
|
if is_open:
|
||||||
|
self.status_label.setText(f"\u25cf Connected \u2014 {self.hostname}:{self.port}")
|
||||||
|
self.status_label.setStyleSheet("color: #4caf50; font-size: 16px; font-weight: bold;")
|
||||||
|
self.btn_toggle.setText("Close Bridge")
|
||||||
|
self.btn_toggle.setProperty("active", "true")
|
||||||
|
self.btn_toggle.setStyleSheet("""
|
||||||
|
QPushButton#btnToggle {
|
||||||
|
background: #2d1a1a;
|
||||||
|
border: 1px solid #a44;
|
||||||
|
color: #ff8888;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
QPushButton#btnToggle:hover {
|
||||||
|
background: #3d2020;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
else:
|
||||||
|
self.status_label.setText(f"\u25cf Not connected")
|
||||||
|
self.status_label.setStyleSheet("color: #ff5555; font-size: 16px; font-weight: bold;")
|
||||||
|
self.btn_toggle.setText("Open Bridge")
|
||||||
|
self.btn_toggle.setProperty("active", "false")
|
||||||
|
self.btn_toggle.setStyleSheet("""
|
||||||
|
QPushButton#btnToggle {
|
||||||
|
background: #1a2d1a;
|
||||||
|
border: 1px solid #4a4;
|
||||||
|
color: #88cc88;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
QPushButton#btnToggle:hover {
|
||||||
|
background: #203d20;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.prompt_field.setText(self._build_prompt())
|
||||||
|
|
||||||
|
def _toggle_bridge(self):
|
||||||
|
if bridge_status(self.port):
|
||||||
|
close_bridge(self.port)
|
||||||
|
else:
|
||||||
|
open_bridge(self.port)
|
||||||
|
self._refresh_status()
|
||||||
|
|
||||||
|
def _on_port_changed(self, value):
|
||||||
|
was_open = bridge_status(self.port)
|
||||||
|
if was_open:
|
||||||
|
close_bridge(self.port)
|
||||||
|
self.port = value
|
||||||
|
self.cfg["port"] = value
|
||||||
|
save_config(self.cfg)
|
||||||
|
if was_open:
|
||||||
|
open_bridge(self.port)
|
||||||
|
self._refresh_status()
|
||||||
|
|
||||||
|
def _copy_prompt(self):
|
||||||
|
QtWidgets.QApplication.clipboard().setText(self.prompt_field.text())
|
||||||
|
self.btn_copy.setText("\u2713 Copied!")
|
||||||
|
QtCore.QTimer.singleShot(2000, lambda: self.btn_copy.setText("Copy"))
|
||||||
|
|
||||||
|
|
||||||
|
# ── Launch ──────────────────────────────────────────────
|
||||||
|
def show():
|
||||||
|
"""Show or focus the Hermes Bridge panel."""
|
||||||
|
for widget in QtWidgets.QApplication.allWidgets():
|
||||||
|
if isinstance(widget, HermesBridgePanel):
|
||||||
|
widget.raise_()
|
||||||
|
widget.activateWindow()
|
||||||
|
return widget
|
||||||
|
|
||||||
|
panel = HermesBridgePanel()
|
||||||
|
panel.show()
|
||||||
|
return panel
|
||||||
Reference in New Issue
Block a user