From ccdbb190a80376330706a73feca36ed7d5bcbf57 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 12 Jul 2026 13:02:28 +0000 Subject: [PATCH] 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) --- hermes_houdini_bridge.json | 8 + install.sh | 39 +++ python_panels/hermes_bridge.pypanel | 6 + .../hermes_bridge_panel.cpython-313.pyc | Bin 0 -> 14663 bytes scripts/python/hermes_bridge_panel.py | 269 ++++++++++++++++++ 5 files changed, 322 insertions(+) create mode 100644 hermes_houdini_bridge.json create mode 100755 install.sh create mode 100644 python_panels/hermes_bridge.pypanel create mode 100644 scripts/python/__pycache__/hermes_bridge_panel.cpython-313.pyc create mode 100644 scripts/python/hermes_bridge_panel.py 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/__pycache__/hermes_bridge_panel.cpython-313.pyc b/scripts/python/__pycache__/hermes_bridge_panel.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd2a555e1327e7f39871438878f3d924b16a6fd5 GIT binary patch literal 14663 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 L)A3IZ8|423(X*uL literal 0 HcmV?d00001 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