diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/hermes_houdini_bridge.json b/hermes_houdini_bridge.json new file mode 100644 index 0000000..46102e5 --- /dev/null +++ b/hermes_houdini_bridge.json @@ -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'" +} diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..3f6c2f6 --- /dev/null +++ b/install.sh @@ -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" diff --git a/python_panels/hermes_bridge.pypanel b/python_panels/hermes_bridge.pypanel new file mode 100644 index 0000000..a01bd93 --- /dev/null +++ b/python_panels/hermes_bridge.pypanel @@ -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" +} diff --git a/scripts/python/hermes_bridge_panel.py b/scripts/python/hermes_bridge_panel.py new file mode 100644 index 0000000..984239b --- /dev/null +++ b/scripts/python/hermes_bridge_panel.py @@ -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