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