From 136e3dc993ee62d39f8a13333074eb296257cf83 Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Thu, 15 May 2025 21:58:33 +0200 Subject: [PATCH 01/17] ADD Http Listener --- deps/eigen | 2 +- deps/pybind11 | 2 +- src/gh/components/DF_http_listener/code.py | 94 ++++++++++++++++++ src/gh/components/DF_http_listener/icon.png | Bin 0 -> 4346 bytes .../components/DF_http_listener/metadata.json | 52 ++++++++++ src/gh/diffCheck/diffCheck.egg-info/PKG-INFO | 2 +- 6 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 src/gh/components/DF_http_listener/code.py create mode 100644 src/gh/components/DF_http_listener/icon.png create mode 100644 src/gh/components/DF_http_listener/metadata.json diff --git a/deps/eigen b/deps/eigen index 8b4efc8e..171bd08c 160000 --- a/deps/eigen +++ b/deps/eigen @@ -1 +1 @@ -Subproject commit 8b4efc8ed8a65415e248d54fbc9afdd964c94f64 +Subproject commit 171bd08ca987987c3c50f0fa5dd8914bdd42dd3b diff --git a/deps/pybind11 b/deps/pybind11 index 0ed20f26..af231a60 160000 --- a/deps/pybind11 +++ b/deps/pybind11 @@ -1 +1 @@ -Subproject commit 0ed20f26acee626ac989568ecc6347e159ddbb47 +Subproject commit af231a6054669bcf8b0c6c8d023506497ef91c1a diff --git a/src/gh/components/DF_http_listener/code.py b/src/gh/components/DF_http_listener/code.py new file mode 100644 index 00000000..c15538fc --- /dev/null +++ b/src/gh/components/DF_http_listener/code.py @@ -0,0 +1,94 @@ +#! python3 + +from ghpythonlib.componentbase import executingcomponent as component +import os +import tempfile +import requests +import threading +import Rhino +import Rhino.Geometry as rg +import scriptcontext as sc + + +class DFHTTPListener(component): + + def RunScript(self, + i_load: bool, + i_ply_url: str): + + sc.sticky.setdefault('ply_url', None) + sc.sticky.setdefault('imported_geom', None) + sc.sticky.setdefault('status_message','Idle') + sc.sticky.setdefault('prev_load', False) + sc.sticky.setdefault('thread_running', False) + + def _import_job(url): + try: + if not url.lower().endswith('.ply'): + raise ValueError("URL must end in .ply") + + resp = requests.get(url, timeout=30); resp.raise_for_status() + fn = os.path.basename(url) + tmp = os.path.join(tempfile.gettempdir(), fn) + with open(tmp, 'wb') as f: + f.write(resp.content) + + doc = Rhino.RhinoDoc.ActiveDoc + before_ids = {o.Id for o in doc.Objects} + + opts = Rhino.FileIO.FilePlyReadOptions() + ok = Rhino.FileIO.FilePly.Read(tmp, doc, opts) + if not ok: + raise RuntimeError("Rhino.FilePly.Read failed") + + after_ids = {o.Id for o in doc.Objects} + new_ids = after_ids - before_ids + + geom = None + for guid in new_ids: + g = doc.Objects.FindId(guid).Geometry + if isinstance(g, rg.PointCloud): + geom = g.Duplicate() + break + elif isinstance(g, rg.Mesh): + geom = g.DuplicateMesh() + break + + for guid in new_ids: + doc.Objects.Delete(guid, True) + doc.Views.Redraw() + + sc.sticky['imported_geom'] = geom + count = geom.Count if isinstance(geom, rg.PointCloud) else geom.Vertices.Count + if isinstance(geom, rg.PointCloud): + sc.sticky['status_message'] = f"Done: {count} points" + else: sc.sticky['status_message'] = f"Done: {count} vertices" + ghenv.Component.Message = sc.sticky.get('status_message') + + except Exception as e: + sc.sticky['imported_geom'] = None + sc.sticky['status_message'] = f"Error: {e}" + finally: + try: os.remove(tmp) + except: pass + sc.sticky['thread_running'] = False + ghenv.Component.ExpireSolution(True) + + if sc.sticky['ply_url'] != i_ply_url: + sc.sticky['ply_url'] = i_ply_url + sc.sticky['status_message'] = "URL changed. Press Load" + sc.sticky['thread_running'] = False + sc.sticky['prev_load'] = False + + if i_load and not sc.sticky['prev_load'] and not sc.sticky['thread_running']: + sc.sticky['status_message'] = "Loading..." + sc.sticky['thread_running'] = True + threading.Thread(target=_import_job, args=(i_ply_url,), daemon=True).start() + + sc.sticky['prev_load'] = i_load + ghenv.Component.Message = sc.sticky.get('status_message', "") + + # output + o_geometry = sc.sticky.get('imported_geom') + + return [o_geometry] diff --git a/src/gh/components/DF_http_listener/icon.png b/src/gh/components/DF_http_listener/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..44df06fe5e267158def06ca89025dde203d92d75 GIT binary patch literal 4346 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 r?z(l6Gprx}R_Y_=zZ1S*{*0no;Yt4m=hvD% literal 0 HcmV?d00001 diff --git a/src/gh/components/DF_http_listener/metadata.json b/src/gh/components/DF_http_listener/metadata.json new file mode 100644 index 00000000..e029ea3f --- /dev/null +++ b/src/gh/components/DF_http_listener/metadata.json @@ -0,0 +1,52 @@ +{ + "name": "DFHTTPListener", + "nickname": "HTTPIn", + "category": "diffCheck", + "subcategory": "IO", + "description": "This component reads a ply file from the internet.", + "exposure": 4, + "instanceGuid": "ca4b5c94-6c85-4bc5-87f0-132cc34c4536", + "ghpython": { + "hideOutput": true, + "hideInput": true, + "isAdvancedMode": true, + "marshalOutGuids": true, + "iconDisplay": 2, + "inputParameters": [ + { + "name": "i_load", + "nickname": "i_load", + "description": "Button to import ply from url.", + "optional": true, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "bool" + }, + { + "name": "i_ply_url", + "nickname": "i_ply_url", + "description": "The url where to get the pointcloud", + "optional": true, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "str" + } + ], + "outputParameters": [ + { + "name": "o_geometry", + "nickname": "o_geo", + "description": "The mesh or pcd that was imported.", + "optional": false, + "sourceCount": 0, + "graft": false + } + ] + } +} \ No newline at end of file diff --git a/src/gh/diffCheck/diffCheck.egg-info/PKG-INFO b/src/gh/diffCheck/diffCheck.egg-info/PKG-INFO index 864b437b..a55be843 100644 --- a/src/gh/diffCheck/diffCheck.egg-info/PKG-INFO +++ b/src/gh/diffCheck/diffCheck.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: diffCheck -Version: 0.0.37 +Version: 1.3.0 Summary: DiffCheck is a package to check the differences between two timber structures Home-page: https://github.com/diffCheckOrg/diffCheck Author: Andrea Settimi, Damien Gilliard, Eleni Skevaki, Marirena Kladeftira, Julien Gamerro, Stefana Parascho, and Yves Weinand From 38aab63fcf0b8b5d5c4d697f91464e192803af5b Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Fri, 16 May 2025 11:58:57 +0200 Subject: [PATCH 02/17] ADD TCP Listener --- src/gh/components/DF_http_listener/code.py | 14 ++- src/gh/components/DF_tcp_listener/code.py | 111 ++++++++++++++++++ src/gh/components/DF_tcp_listener/icon.png | Bin 0 -> 4497 bytes .../components/DF_tcp_listener/metadata.json | 76 ++++++++++++ .../components/DF_websocket_listener/code.py | 94 +++++++++++++++ .../components/DF_websocket_listener/icon.png | Bin 0 -> 4346 bytes .../DF_websocket_listener/metadata.json | 52 ++++++++ 7 files changed, 342 insertions(+), 5 deletions(-) create mode 100644 src/gh/components/DF_tcp_listener/code.py create mode 100644 src/gh/components/DF_tcp_listener/icon.png create mode 100644 src/gh/components/DF_tcp_listener/metadata.json create mode 100644 src/gh/components/DF_websocket_listener/code.py create mode 100644 src/gh/components/DF_websocket_listener/icon.png create mode 100644 src/gh/components/DF_websocket_listener/metadata.json diff --git a/src/gh/components/DF_http_listener/code.py b/src/gh/components/DF_http_listener/code.py index c15538fc..d38cee6b 100644 --- a/src/gh/components/DF_http_listener/code.py +++ b/src/gh/components/DF_http_listener/code.py @@ -27,7 +27,8 @@ def _import_job(url): if not url.lower().endswith('.ply'): raise ValueError("URL must end in .ply") - resp = requests.get(url, timeout=30); resp.raise_for_status() + resp = requests.get(url, timeout=30) + resp.raise_for_status() fn = os.path.basename(url) tmp = os.path.join(tempfile.gettempdir(), fn) with open(tmp, 'wb') as f: @@ -61,16 +62,19 @@ def _import_job(url): sc.sticky['imported_geom'] = geom count = geom.Count if isinstance(geom, rg.PointCloud) else geom.Vertices.Count if isinstance(geom, rg.PointCloud): - sc.sticky['status_message'] = f"Done: {count} points" - else: sc.sticky['status_message'] = f"Done: {count} vertices" + sc.sticky['status_message'] = f"Done: {count} points" + else: + sc.sticky['status_message'] = f"Done: {count} vertices" ghenv.Component.Message = sc.sticky.get('status_message') except Exception as e: sc.sticky['imported_geom'] = None sc.sticky['status_message'] = f"Error: {e}" finally: - try: os.remove(tmp) - except: pass + try: + os.remove(tmp) + except: + pass sc.sticky['thread_running'] = False ghenv.Component.ExpireSolution(True) diff --git a/src/gh/components/DF_tcp_listener/code.py b/src/gh/components/DF_tcp_listener/code.py new file mode 100644 index 00000000..1f7ca3c9 --- /dev/null +++ b/src/gh/components/DF_tcp_listener/code.py @@ -0,0 +1,111 @@ +from ghpythonlib.componentbase import executingcomponent as component +import socket +import threading +import json +import scriptcontext as sc +import Rhino.Geometry as rg +import System.Drawing as sd + + +class DFHTTPListener(component): + def RunScript(self, i_load: bool, i_reset: bool, i_port: int, i_host: str): + + # Sticky defaults + sc.sticky.setdefault('listen_addr', None) + sc.sticky.setdefault('server_sock', None) + sc.sticky.setdefault('server_started', False) + sc.sticky.setdefault('cloud_buffer_raw', []) + sc.sticky.setdefault('latest_cloud', None) + sc.sticky.setdefault('status_message', "Waiting...") + sc.sticky.setdefault('prev_load', False) + + # Handle Reset or host/port change + addr = (i_host, i_port) + if i_reset or sc.sticky['listen_addr'] != addr: + # close old socket if any + old = sc.sticky.get('server_sock') + try: + if old: old.close() + except: pass + + sc.sticky['listen_addr'] = addr + sc.sticky['server_sock'] = None + sc.sticky['server_started'] = False + sc.sticky['cloud_buffer_raw'] = [] + sc.sticky['latest_cloud'] = None + sc.sticky['status_message'] = "Reset" if i_reset else f"Addr → {i_host}:{i_port}" + ghenv.Component.Message = sc.sticky['status_message'] + + # Client handler + def handle_client(conn): + buf = b'' + with conn: + while True: + try: + chunk = conn.recv(4096) + if not chunk: + break + buf += chunk + while b'\n' in buf: + line, buf = buf.split(b'\n', 1) + try: + raw = json.loads(line.decode()) + except Exception as e: + sc.sticky['status_message'] = f"JSON error: {e}" + ghenv.Component.Message = sc.sticky['status_message'] + continue + + if isinstance(raw, list) and all(isinstance(pt, list) and len(pt)==6 for pt in raw): + sc.sticky['cloud_buffer_raw'] = raw + sc.sticky['status_message'] = f"Buffered {len(raw)} pts" + else: + sc.sticky['status_message'] = "Unexpected format" + ghenv.Component.Message = sc.sticky['status_message'] + except Exception as e: + sc.sticky['status_message'] = f"Socket error: {e}" + ghenv.Component.Message = sc.sticky['status_message'] + break + + def accept_loop(srv_sock): + while True: + try: + conn, _ = srv_sock.accept() + threading.Thread(target=handle_client, args=(conn,), daemon=True).start() + except: + break + + #Start server + def start_server(): + try: + srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + srv.bind((i_host, i_port)) + srv.listen() + sc.sticky['server_sock'] = srv + sc.sticky['server_started'] = True + sc.sticky['status_message'] = f"Listening on {i_host}:{i_port}" + ghenv.Component.Message = sc.sticky['status_message'] + threading.Thread(target=accept_loop, args=(srv,), daemon=True).start() + except Exception as e: + sc.sticky['status_message'] = f"Server error: {e}" + ghenv.Component.Message = sc.sticky['status_message'] + + if not sc.sticky['server_started']: + start_server() + + if i_load and not sc.sticky['prev_load']: + raw = sc.sticky['cloud_buffer_raw'] + if raw: + pc = rg.PointCloud() + for x, y, z, r, g, b in raw: + col = sd.Color.FromArgb(int(r), int(g), int(b)) + pc.Add(rg.Point3d(x, y, z), col) + sc.sticky['latest_cloud'] = pc + sc.sticky['status_message'] = f"Retrieved {pc.Count} pts" + else: + sc.sticky['status_message'] = "No data buffered" + ghenv.Component.Message = sc.sticky['status_message'] + + sc.sticky['prev_load'] = i_load + + return [sc.sticky['latest_cloud']] diff --git a/src/gh/components/DF_tcp_listener/icon.png b/src/gh/components/DF_tcp_listener/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f82515818a625e14b742dd4987284fe9f568c82d GIT binary patch literal 4497 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 r?z(l6Gprx}R_Y_=zZ1S*{*0no;Yt4m=hvD% literal 0 HcmV?d00001 diff --git a/src/gh/components/DF_websocket_listener/metadata.json b/src/gh/components/DF_websocket_listener/metadata.json new file mode 100644 index 00000000..377e1698 --- /dev/null +++ b/src/gh/components/DF_websocket_listener/metadata.json @@ -0,0 +1,52 @@ +{ + "name": "DFWEBSOCKETListener", + "nickname": "WSIn", + "category": "diffCheck", + "subcategory": "IO", + "description": "This component reads a ply file from the internet.", + "exposure": 4, + "instanceGuid": "909d9a4f-2698-4fbf-8dcb-2005f51e047f", + "ghpython": { + "hideOutput": true, + "hideInput": true, + "isAdvancedMode": true, + "marshalOutGuids": true, + "iconDisplay": 2, + "inputParameters": [ + { + "name": "i_load", + "nickname": "i_load", + "description": "Button to import ply from url.", + "optional": true, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "bool" + }, + { + "name": "i_ply_url", + "nickname": "i_ply_url", + "description": "The url where to get the pointcloud", + "optional": true, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "str" + } + ], + "outputParameters": [ + { + "name": "o_geometry", + "nickname": "o_geo", + "description": "The mesh or pcd that was imported.", + "optional": false, + "sourceCount": 0, + "graft": false + } + ] + } +} \ No newline at end of file From 2fd80df5ed17c9b723c6f6b25eb9b0a4f4f5317d Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Tue, 27 May 2025 14:49:24 +0200 Subject: [PATCH 03/17] FIX lintering --- src/gh/components/DF_http_listener/code.py | 8 +++--- src/gh/components/DF_tcp_listener/code.py | 25 ++++++++++--------- .../components/DF_websocket_listener/code.py | 22 +++++++++------- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/gh/components/DF_http_listener/code.py b/src/gh/components/DF_http_listener/code.py index d38cee6b..e6480766 100644 --- a/src/gh/components/DF_http_listener/code.py +++ b/src/gh/components/DF_http_listener/code.py @@ -65,7 +65,7 @@ def _import_job(url): sc.sticky['status_message'] = f"Done: {count} points" else: sc.sticky['status_message'] = f"Done: {count} vertices" - ghenv.Component.Message = sc.sticky.get('status_message') + ghenv.Component.Message = sc.sticky.get('status_message') # noqa: F821 except Exception as e: sc.sticky['imported_geom'] = None @@ -73,10 +73,10 @@ def _import_job(url): finally: try: os.remove(tmp) - except: + except Exception: pass sc.sticky['thread_running'] = False - ghenv.Component.ExpireSolution(True) + ghenv.Component.ExpireSolution(True) # noqa: F821 if sc.sticky['ply_url'] != i_ply_url: sc.sticky['ply_url'] = i_ply_url @@ -90,7 +90,7 @@ def _import_job(url): threading.Thread(target=_import_job, args=(i_ply_url,), daemon=True).start() sc.sticky['prev_load'] = i_load - ghenv.Component.Message = sc.sticky.get('status_message', "") + ghenv.Component.Message = sc.sticky.get('status_message', "") # noqa: F821 # output o_geometry = sc.sticky.get('imported_geom') diff --git a/src/gh/components/DF_tcp_listener/code.py b/src/gh/components/DF_tcp_listener/code.py index 1f7ca3c9..d361a69d 100644 --- a/src/gh/components/DF_tcp_listener/code.py +++ b/src/gh/components/DF_tcp_listener/code.py @@ -25,8 +25,10 @@ def RunScript(self, i_load: bool, i_reset: bool, i_port: int, i_host: str): # close old socket if any old = sc.sticky.get('server_sock') try: - if old: old.close() - except: pass + if old: + old.close() + except Exception: + pass sc.sticky['listen_addr'] = addr sc.sticky['server_sock'] = None @@ -34,7 +36,7 @@ def RunScript(self, i_load: bool, i_reset: bool, i_port: int, i_host: str): sc.sticky['cloud_buffer_raw'] = [] sc.sticky['latest_cloud'] = None sc.sticky['status_message'] = "Reset" if i_reset else f"Addr → {i_host}:{i_port}" - ghenv.Component.Message = sc.sticky['status_message'] + ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821 # Client handler def handle_client(conn): @@ -52,7 +54,7 @@ def handle_client(conn): raw = json.loads(line.decode()) except Exception as e: sc.sticky['status_message'] = f"JSON error: {e}" - ghenv.Component.Message = sc.sticky['status_message'] + ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821 continue if isinstance(raw, list) and all(isinstance(pt, list) and len(pt)==6 for pt in raw): @@ -60,10 +62,10 @@ def handle_client(conn): sc.sticky['status_message'] = f"Buffered {len(raw)} pts" else: sc.sticky['status_message'] = "Unexpected format" - ghenv.Component.Message = sc.sticky['status_message'] + ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821 except Exception as e: sc.sticky['status_message'] = f"Socket error: {e}" - ghenv.Component.Message = sc.sticky['status_message'] + ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821 break def accept_loop(srv_sock): @@ -71,10 +73,10 @@ def accept_loop(srv_sock): try: conn, _ = srv_sock.accept() threading.Thread(target=handle_client, args=(conn,), daemon=True).start() - except: + except Exception: break - #Start server + # Start server def start_server(): try: srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -84,11 +86,11 @@ def start_server(): sc.sticky['server_sock'] = srv sc.sticky['server_started'] = True sc.sticky['status_message'] = f"Listening on {i_host}:{i_port}" - ghenv.Component.Message = sc.sticky['status_message'] + ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821 threading.Thread(target=accept_loop, args=(srv,), daemon=True).start() except Exception as e: sc.sticky['status_message'] = f"Server error: {e}" - ghenv.Component.Message = sc.sticky['status_message'] + ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821 if not sc.sticky['server_started']: start_server() @@ -104,8 +106,7 @@ def start_server(): sc.sticky['status_message'] = f"Retrieved {pc.Count} pts" else: sc.sticky['status_message'] = "No data buffered" - ghenv.Component.Message = sc.sticky['status_message'] - + ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821 sc.sticky['prev_load'] = i_load return [sc.sticky['latest_cloud']] diff --git a/src/gh/components/DF_websocket_listener/code.py b/src/gh/components/DF_websocket_listener/code.py index c15538fc..67fe1022 100644 --- a/src/gh/components/DF_websocket_listener/code.py +++ b/src/gh/components/DF_websocket_listener/code.py @@ -27,7 +27,8 @@ def _import_job(url): if not url.lower().endswith('.ply'): raise ValueError("URL must end in .ply") - resp = requests.get(url, timeout=30); resp.raise_for_status() + resp = requests.get(url, timeout=30) + resp.raise_for_status() fn = os.path.basename(url) tmp = os.path.join(tempfile.gettempdir(), fn) with open(tmp, 'wb') as f: @@ -61,24 +62,27 @@ def _import_job(url): sc.sticky['imported_geom'] = geom count = geom.Count if isinstance(geom, rg.PointCloud) else geom.Vertices.Count if isinstance(geom, rg.PointCloud): - sc.sticky['status_message'] = f"Done: {count} points" - else: sc.sticky['status_message'] = f"Done: {count} vertices" - ghenv.Component.Message = sc.sticky.get('status_message') + sc.sticky['status_message'] = f"Done: {count} points" + else: + sc.sticky['status_message'] = f"Done: {count} vertices" + ghenv.Component.Message = sc.sticky.get('status_message') # noqa: F821 except Exception as e: sc.sticky['imported_geom'] = None sc.sticky['status_message'] = f"Error: {e}" finally: - try: os.remove(tmp) - except: pass + try: + os.remove(tmp) + except Exception: + pass sc.sticky['thread_running'] = False - ghenv.Component.ExpireSolution(True) + ghenv.Component.ExpireSolution(True) # noqa: F821 if sc.sticky['ply_url'] != i_ply_url: sc.sticky['ply_url'] = i_ply_url sc.sticky['status_message'] = "URL changed. Press Load" sc.sticky['thread_running'] = False - sc.sticky['prev_load'] = False + sc.sticky['prev_load'] = False if i_load and not sc.sticky['prev_load'] and not sc.sticky['thread_running']: sc.sticky['status_message'] = "Loading..." @@ -86,7 +90,7 @@ def _import_job(url): threading.Thread(target=_import_job, args=(i_ply_url,), daemon=True).start() sc.sticky['prev_load'] = i_load - ghenv.Component.Message = sc.sticky.get('status_message', "") + ghenv.Component.Message = sc.sticky.get('status_message', "") # noqa: F821 # output o_geometry = sc.sticky.get('imported_geom') From aeecaa2cea97c1afb28211f0545c9c98c14f3a70 Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Tue, 27 May 2025 17:46:21 +0200 Subject: [PATCH 04/17] FIX tcp component + add example file sender --- src/gh/components/DF_http_listener/code.py | 31 +++- src/gh/components/DF_tcp_listener/code.py | 141 ++++++++++-------- .../components/DF_tcp_listener/metadata.json | 20 ++- src/gh/examples/simple_tcp_sender.py | 23 +++ 4 files changed, 137 insertions(+), 78 deletions(-) create mode 100644 src/gh/examples/simple_tcp_sender.py diff --git a/src/gh/components/DF_http_listener/code.py b/src/gh/components/DF_http_listener/code.py index e6480766..891ab405 100644 --- a/src/gh/components/DF_http_listener/code.py +++ b/src/gh/components/DF_http_listener/code.py @@ -16,27 +16,38 @@ def RunScript(self, i_load: bool, i_ply_url: str): - sc.sticky.setdefault('ply_url', None) - sc.sticky.setdefault('imported_geom', None) - sc.sticky.setdefault('status_message','Idle') - sc.sticky.setdefault('prev_load', False) - sc.sticky.setdefault('thread_running', False) + sc.sticky.setdefault('ply_url', None) #last url processed + sc.sticky.setdefault('imported_geom', None) #geo imported from ply + sc.sticky.setdefault('status_message','Idle') #status message on component + sc.sticky.setdefault('prev_load', False) #previous state of toggle + sc.sticky.setdefault('thread_running', False) #is an import thread active? def _import_job(url): + """ + Background job: + - Downloads the .ply file from the URL + - Imports it into the active Rhino document + - Extracts the new geometry (point cloud or mesh) + - Cleans up the temporary file and document objects + - Updates sticky state and status message + """ try: if not url.lower().endswith('.ply'): raise ValueError("URL must end in .ply") resp = requests.get(url, timeout=30) resp.raise_for_status() + # save om temporary file fn = os.path.basename(url) tmp = os.path.join(tempfile.gettempdir(), fn) with open(tmp, 'wb') as f: f.write(resp.content) doc = Rhino.RhinoDoc.ActiveDoc + # recordd existing object IDs to detect new ones before_ids = {o.Id for o in doc.Objects} + # import PLY using Rhino's API opts = Rhino.FileIO.FilePlyReadOptions() ok = Rhino.FileIO.FilePly.Read(tmp, doc, opts) if not ok: @@ -44,7 +55,7 @@ def _import_job(url): after_ids = {o.Id for o in doc.Objects} new_ids = after_ids - before_ids - + # get new pcd or mesh from document geom = None for guid in new_ids: g = doc.Objects.FindId(guid).Geometry @@ -54,11 +65,12 @@ def _import_job(url): elif isinstance(g, rg.Mesh): geom = g.DuplicateMesh() break - + # remove imported objects for guid in new_ids: doc.Objects.Delete(guid, True) doc.Views.Redraw() + # store new geometry sc.sticky['imported_geom'] = geom count = geom.Count if isinstance(geom, rg.PointCloud) else geom.Vertices.Count if isinstance(geom, rg.PointCloud): @@ -75,15 +87,18 @@ def _import_job(url): os.remove(tmp) except Exception: pass + # mark thread as finished sc.sticky['thread_running'] = False ghenv.Component.ExpireSolution(True) # noqa: F821 + # check if the URL input has changed if sc.sticky['ply_url'] != i_ply_url: sc.sticky['ply_url'] = i_ply_url sc.sticky['status_message'] = "URL changed. Press Load" sc.sticky['thread_running'] = False - sc.sticky['prev_load'] = False + sc.sticky['prev_load'] = False + # start importing if Load toggle is pressed and import thread is not already running if i_load and not sc.sticky['prev_load'] and not sc.sticky['thread_running']: sc.sticky['status_message'] = "Loading..." sc.sticky['thread_running'] = True diff --git a/src/gh/components/DF_tcp_listener/code.py b/src/gh/components/DF_tcp_listener/code.py index d361a69d..9171762d 100644 --- a/src/gh/components/DF_tcp_listener/code.py +++ b/src/gh/components/DF_tcp_listener/code.py @@ -1,3 +1,5 @@ +#! python3 + from ghpythonlib.componentbase import executingcomponent as component import socket import threading @@ -7,42 +9,33 @@ import System.Drawing as sd -class DFHTTPListener(component): - def RunScript(self, i_load: bool, i_reset: bool, i_port: int, i_host: str): +class DFTCPListener(component): + def RunScript(self, + i_start: bool, + i_load: bool, + i_stop: bool, + i_port: int, + i_host: str): # Sticky defaults - sc.sticky.setdefault('listen_addr', None) sc.sticky.setdefault('server_sock', None) sc.sticky.setdefault('server_started', False) sc.sticky.setdefault('cloud_buffer_raw', []) sc.sticky.setdefault('latest_cloud', None) - sc.sticky.setdefault('status_message', "Waiting...") + sc.sticky.setdefault('status_message', 'Waiting...') + sc.sticky.setdefault('prev_start', False) + sc.sticky.setdefault('prev_stop', False) sc.sticky.setdefault('prev_load', False) - - # Handle Reset or host/port change - addr = (i_host, i_port) - if i_reset or sc.sticky['listen_addr'] != addr: - # close old socket if any - old = sc.sticky.get('server_sock') - try: - if old: - old.close() - except Exception: - pass - - sc.sticky['listen_addr'] = addr - sc.sticky['server_sock'] = None - sc.sticky['server_started'] = False - sc.sticky['cloud_buffer_raw'] = [] - sc.sticky['latest_cloud'] = None - sc.sticky['status_message'] = "Reset" if i_reset else f"Addr → {i_host}:{i_port}" - ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821 + sc.sticky.setdefault('client_socks', []) # Track client sockets # Client handler def handle_client(conn): + """ + reads the incoming bytes from a single client socket and stores valid data in a shared buffer + """ buf = b'' with conn: - while True: + while sc.sticky.get('server_started', False): try: chunk = conn.recv(4096) if not chunk: @@ -52,61 +45,77 @@ def handle_client(conn): line, buf = buf.split(b'\n', 1) try: raw = json.loads(line.decode()) - except Exception as e: - sc.sticky['status_message'] = f"JSON error: {e}" - ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821 + except Exception: continue - - if isinstance(raw, list) and all(isinstance(pt, list) and len(pt)==6 for pt in raw): + if isinstance(raw, list) and all(isinstance(pt, list) and len(pt) == 6 for pt in raw): sc.sticky['cloud_buffer_raw'] = raw - sc.sticky['status_message'] = f"Buffered {len(raw)} pts" - else: - sc.sticky['status_message'] = "Unexpected format" - ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821 - except Exception as e: - sc.sticky['status_message'] = f"Socket error: {e}" - ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821 + except Exception: break - def accept_loop(srv_sock): - while True: - try: - conn, _ = srv_sock.accept() - threading.Thread(target=handle_client, args=(conn,), daemon=True).start() - except Exception: - break + # thread to accept incoming connections + def server_loop(sock): + """ + runs in its own thread, continuously calling accept() on the listening socket + Each time a client connects, it launches a new thread running handle_client to deal with that connection + """ + try: + conn, _ = sock.accept() + handle_client(conn) + except Exception: + pass - # Start server + # Start TCP server def start_server(): - try: - srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - srv.bind((i_host, i_port)) - srv.listen() - sc.sticky['server_sock'] = srv - sc.sticky['server_started'] = True - sc.sticky['status_message'] = f"Listening on {i_host}:{i_port}" - ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821 - threading.Thread(target=accept_loop, args=(srv,), daemon=True).start() - except Exception as e: - sc.sticky['status_message'] = f"Server error: {e}" - ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821 + """ + creates and binds a TCP socket on the given host/port, marks the server as started and then starts the accept_loop in a background thread + """ + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind((i_host, i_port)) + sock.listen(1) + sc.sticky['server_sock'] = sock + sc.sticky['server_started'] = True + sc.sticky['status_message'] = f'Listening on {i_host}:{i_port}' + # Only accept one connection to keep it long-lived + threading.Thread(target=server_loop, args=(sock,), daemon=True).start() - if not sc.sticky['server_started']: + def stop_server(): + sock = sc.sticky.get('server_sock') + if sock: + try: + sock.close() + except Exception: + pass + sc.sticky['server_sock'] = None + sc.sticky['server_started'] = False + sc.sticky['cloud_buffer_raw'] = [] + sc.sticky['status_message'] = 'Stopped' + + # Start or stop server based on inputs + if i_start and not sc.sticky['prev_start']: start_server() + if i_stop and not sc.sticky['prev_stop']: + stop_server() + # Load buffered points into PointCloud if i_load and not sc.sticky['prev_load']: - raw = sc.sticky['cloud_buffer_raw'] + raw = sc.sticky.get('cloud_buffer_raw', []) if raw: pc = rg.PointCloud() for x, y, z, r, g, b in raw: - col = sd.Color.FromArgb(int(r), int(g), int(b)) - pc.Add(rg.Point3d(x, y, z), col) - sc.sticky['latest_cloud'] = pc - sc.sticky['status_message'] = f"Retrieved {pc.Count} pts" + pc.Add(rg.Point3d(x, y, z), sd.Color.FromArgb(int(r), int(g), int(b))) + sc.sticky['latest_cloud'] = pc + sc.sticky['status_message'] = f'Retrieved {pc.Count} pts' else: - sc.sticky['status_message'] = "No data buffered" - ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821 + sc.sticky['status_message'] = 'No data buffered' + + # Update previous states + sc.sticky['prev_start'] = i_start + sc.sticky['prev_stop'] = i_stop sc.sticky['prev_load'] = i_load - return [sc.sticky['latest_cloud']] + # Update UI and output + ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821 + + o_cloud = sc.sticky['latest_cloud'] + return [o_cloud] diff --git a/src/gh/components/DF_tcp_listener/metadata.json b/src/gh/components/DF_tcp_listener/metadata.json index de2bafd6..cf678070 100644 --- a/src/gh/components/DF_tcp_listener/metadata.json +++ b/src/gh/components/DF_tcp_listener/metadata.json @@ -13,10 +13,22 @@ "marshalOutGuids": true, "iconDisplay": 2, "inputParameters": [ + { + "name": "i_start", + "nickname": "i_start", + "description": "Button to start the TCP server", + "optional": true, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "bool" + }, { "name": "i_load", "nickname": "i_load", - "description": "Button to get a new PCD batch", + "description": "Button to get the latest PCD from the buffer", "optional": true, "allowTreeAccess": true, "showTypeHints": true, @@ -26,9 +38,9 @@ "typeHintID": "bool" }, { - "name": "i_reset", - "nickname": "i_reset", - "description": "Reset the port", + "name": "i_stop", + "nickname": "i_stop", + "description": "Stop the server and release the port", "optional": true, "allowTreeAccess": true, "showTypeHints": true, diff --git a/src/gh/examples/simple_tcp_sender.py b/src/gh/examples/simple_tcp_sender.py new file mode 100644 index 00000000..b7ff3fcb --- /dev/null +++ b/src/gh/examples/simple_tcp_sender.py @@ -0,0 +1,23 @@ +import socket +import time +import random +import json + +host = '127.0.0.1' +port = 5000 + + +def random_colored_point(): + x, y, z = [round(random.uniform(-10, 10), 2) for _ in range(3)] + r, g, b = [random.randint(0, 255) for _ in range(3)] + return [x, y, z, r, g, b] + + +with socket.create_connection((host, port)) as s: + print("Connected to GH") + while True: + cloud = [random_colored_point() for _ in range(1000)] + msg = json.dumps(cloud) + "\n" + s.sendall(msg.encode()) + print("Sent cloud with", len(cloud), "colored points") + time.sleep(1) \ No newline at end of file From 4aafbf88f0dca31224e49be49cbd73aef36d03ae Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Wed, 28 May 2025 09:54:45 +0200 Subject: [PATCH 05/17] WIP FIX WS Receiver --- .../components/DF_websocket_listener/code.py | 196 ++++++++++-------- .../components/DF_websocket_listener/icon.png | Bin 4346 -> 3695 bytes .../DF_websocket_listener/metadata.json | 56 ++++- src/gh/examples/simple_tcp_sender.py | 2 +- src/gh/examples/simple_ws_sender.py | 37 ++++ 5 files changed, 192 insertions(+), 99 deletions(-) create mode 100644 src/gh/examples/simple_ws_sender.py diff --git a/src/gh/components/DF_websocket_listener/code.py b/src/gh/components/DF_websocket_listener/code.py index 67fe1022..abc41773 100644 --- a/src/gh/components/DF_websocket_listener/code.py +++ b/src/gh/components/DF_websocket_listener/code.py @@ -1,98 +1,118 @@ #! python3 from ghpythonlib.componentbase import executingcomponent as component -import os -import tempfile -import requests -import threading -import Rhino -import Rhino.Geometry as rg +# import threading +# import asyncio +# import json import scriptcontext as sc +# import Rhino.Geometry as rg +# import System.Drawing as sd +#import websockets -class DFHTTPListener(component): - +class DFWSServerListener(component): def RunScript(self, + i_start: bool, i_load: bool, - i_ply_url: str): - - sc.sticky.setdefault('ply_url', None) - sc.sticky.setdefault('imported_geom', None) - sc.sticky.setdefault('status_message','Idle') + i_stop: bool, + i_host: str, + i_port: int): # Port to bind + + # --- Persistent state across runs --- + sc.sticky.setdefault('ws_thread', None) + sc.sticky.setdefault('ws_loop', None) + sc.sticky.setdefault('ws_server', None) + sc.sticky.setdefault('ws_buffer', []) + sc.sticky.setdefault('ws_latest', None) + sc.sticky.setdefault('status', 'Idle') + sc.sticky.setdefault('prev_start', False) + sc.sticky.setdefault('prev_stop', False) sc.sticky.setdefault('prev_load', False) - sc.sticky.setdefault('thread_running', False) - - def _import_job(url): - try: - if not url.lower().endswith('.ply'): - raise ValueError("URL must end in .ply") - - resp = requests.get(url, timeout=30) - resp.raise_for_status() - fn = os.path.basename(url) - tmp = os.path.join(tempfile.gettempdir(), fn) - with open(tmp, 'wb') as f: - f.write(resp.content) - - doc = Rhino.RhinoDoc.ActiveDoc - before_ids = {o.Id for o in doc.Objects} - - opts = Rhino.FileIO.FilePlyReadOptions() - ok = Rhino.FileIO.FilePly.Read(tmp, doc, opts) - if not ok: - raise RuntimeError("Rhino.FilePly.Read failed") - - after_ids = {o.Id for o in doc.Objects} - new_ids = after_ids - before_ids - - geom = None - for guid in new_ids: - g = doc.Objects.FindId(guid).Geometry - if isinstance(g, rg.PointCloud): - geom = g.Duplicate() - break - elif isinstance(g, rg.Mesh): - geom = g.DuplicateMesh() - break - - for guid in new_ids: - doc.Objects.Delete(guid, True) - doc.Views.Redraw() - - sc.sticky['imported_geom'] = geom - count = geom.Count if isinstance(geom, rg.PointCloud) else geom.Vertices.Count - if isinstance(geom, rg.PointCloud): - sc.sticky['status_message'] = f"Done: {count} points" - else: - sc.sticky['status_message'] = f"Done: {count} vertices" - ghenv.Component.Message = sc.sticky.get('status_message') # noqa: F821 - - except Exception as e: - sc.sticky['imported_geom'] = None - sc.sticky['status_message'] = f"Error: {e}" - finally: - try: - os.remove(tmp) - except Exception: - pass - sc.sticky['thread_running'] = False - ghenv.Component.ExpireSolution(True) # noqa: F821 - - if sc.sticky['ply_url'] != i_ply_url: - sc.sticky['ply_url'] = i_ply_url - sc.sticky['status_message'] = "URL changed. Press Load" - sc.sticky['thread_running'] = False - sc.sticky['prev_load'] = False - - if i_load and not sc.sticky['prev_load'] and not sc.sticky['thread_running']: - sc.sticky['status_message'] = "Loading..." - sc.sticky['thread_running'] = True - threading.Thread(target=_import_job, args=(i_ply_url,), daemon=True).start() - - sc.sticky['prev_load'] = i_load - ghenv.Component.Message = sc.sticky.get('status_message', "") # noqa: F821 - - # output - o_geometry = sc.sticky.get('imported_geom') - return [o_geometry] + # async def handler(ws, path): + # """Receive JSON-encoded dicts and buffer valid points.""" + # try: + # async for msg in ws: + # data = json.loads(msg) + # if isinstance(data, dict) and {'x','y','z'}.issubset(data): + # sc.sticky['ws_buffer'].append(data) + # sc.sticky['status'] = f"Buffered {len(sc.sticky['ws_buffer'])} pts" + # ghenv.Component.ExpireSolution(True) # noqa: F821 + # except Exception: + # pass + + # def server_thread(): + # # Create and set a new event loop in this thread + # loop = asyncio.new_event_loop() + # sc.sticky['ws_loop'] = loop + # asyncio.set_event_loop(loop) + # try: + # # Start the WebSocket server on this loop + # start_srv = websockets.serve(handler, i_host, i_port) + # server = loop.run_until_complete(start_srv) + # sc.sticky['ws_server'] = server + # sc.sticky['status'] = f"Listening ws://{i_host}:{i_port}" + # ghenv.Component.ExpireSolution(True) # noqa: F821 + # # Serve forever until stopped + # loop.run_forever() + # except Exception as ex: + # sc.sticky['status'] = f"Server error: {type(ex).__name__}: {ex}" + # ghenv.Component.ExpireSolution(True) # noqa: F821 + # finally: + # # Cleanup: wait for server to close then shutdown loop + # srv = sc.sticky.get('ws_server') + # if srv: + # loop.run_until_complete(srv.wait_closed()) + # loop.close() + # sc.sticky['ws_loop'] = None + # sc.sticky['ws_server'] = None + + # def start(): + # # Begin server thread on rising edge + # if sc.sticky['ws_thread'] is None: + # sc.sticky['status'] = 'Starting WebSocket server...' + # ghenv.Component.Message = sc.sticky['status'] # noqa: F821 + # t = threading.Thread(target=server_thread, daemon=True) + # sc.sticky['ws_thread'] = t + # t.start() + + # def stop(): + # # Signal server and loop to stop + # server = sc.sticky.get('ws_server') + # loop = sc.sticky.get('ws_loop') + # if server and loop: + # loop.call_soon_threadsafe(server.close) + # loop.call_soon_threadsafe(loop.stop) + # sc.sticky['status'] = 'Stopped' + # sc.sticky['ws_buffer'] = [] + # sc.sticky['ws_thread'] = None + # ghenv.Component.Message = sc.sticky['status'] # noqa: F821 + + # # Handle toggles + # if i_start and not sc.sticky['prev_start']: + # start() + # if i_stop and not sc.sticky['prev_stop']: + # stop() + # if i_load and not sc.sticky['prev_load']: + # buf = sc.sticky['ws_buffer'] + # if buf: + # pc = rg.PointCloud() + # for pt in buf: + # pc.Add(rg.Point3d(pt['x'], pt['y'], pt['z']), sd.Color.White) + # sc.sticky['ws_latest'] = pc + # sc.sticky['status'] = f"Retrieved {pc.Count} pts" + # sc.sticky['ws_buffer'] = [] + # else: + # sc.sticky['status'] = 'No data buffered' + # ghenv.Component.Message = sc.sticky['status'] # noqa: F821 + + # # Update previous states + # sc.sticky['prev_start'] = i_start + # sc.sticky['prev_stop'] = i_stop + # sc.sticky['prev_load'] = i_load + + # # Always update message + # ghenv.Component.Message = sc.sticky['status'] # noqa: F821 + + # o_cloud = sc.sticky.get('ws_latest') + # return [o_cloud] diff --git a/src/gh/components/DF_websocket_listener/icon.png b/src/gh/components/DF_websocket_listener/icon.png index 44df06fe5e267158def06ca89025dde203d92d75..8a2268ef1880cf0660afe302439c292fd0204b54 100644 GIT binary patch delta 2632 zcma)&c{tRI8pmhKM3y>P!(hlVmf<(6nZej&${Z>VDlukZXpChNp^-IPmMLqCM7Eo) zEZL(Il8`LN;3zqgor=hHJNMk@xzGLMKF@jo`#$gUen0Q`yREtcP18LrCIteCNr6GR zw~vBA2NcqYd{s2ekf2Y%5dbRQP!EOzP*?*1Z2;g9NCFXwA_D02WiG#SpBf*skHL2b zvuyo|mVpc^mlNp7;aoIPGpBPr888QL4wn<;&GCn!aQb){{7)YMi$dzN_0a$x1@pwB zu}Cza4THg*S$=fRhKisfu^0pr4-g1=I-QC`d-C7y(@_VgXc`)Y z#2}bdBmn`?FieCe6QCiO3#sV>>>AK*Bp@D|1KN1h>qcAn90eSKChGWJ`-zAo+%| zfth6BHFZj4TBN9S%=-stV^2J?JXii8IeDmzresvd7?+2#9(9+<%*MZz7M$qvicqyE zl9MFwTN5FR98`V!K|d>Od3k9exZZT@0gV$1ZE7dAaUgrNz7!61<+kBdTxitI-?SlC6^%gH2yHnl3aMVAL8FyF#t^<1QM4+iSoA+^OFN#)lgI;Rkm(m1t!Q z?)_(-1gs4)2bK1gY#{RTB49-Roh-X`ilUHXpV{CO2MoKU>=0%6K2Q{IR0z=b;z&=BdjLePTR@?DD|6J`@18Wpl%@$R();_v_>)Gj1g1Nt1OLlBQ_w`?A z1}?!I-bbtVM)KItR)Wu;KkF6%@LzMfy$ZLjPkl@1Mcke~zZ^UNRK4VD1;ypO-Mep{ zUB9PDSS!{|)h)hmmNye!{j}WlbH0rFnC>Ms#d1l$XU%GXCPUvCKLkg zb+tDR-gB484_oCMWTtAKURd~4?tVl@;%&c5sPyif%~dY%)9&8)8siCX==Y_myKy>| zE=pxD5@o2#R$BSl))S!%eOqgX>k95yKyP+}hk_%6KpEN5maF-#jnMjV&X7Q5^RVgB zyhMbIhPIpIm#gc&8>hkt!va)1XmgwOBrK9>yt(+ywEk_J&z<4nTLYNMTRk6~HTc@3 zblHG!KCdqEG*JyFUd@KYJq(B-HQ{u%>y_}UT6-VIu77@Pa+xGCX?D>sK&Ah2{`Zp| zl+I&(;g@C6W1(Fy*zPHPn7!HI!}<(SqP%zrDM{Sw9zN{K^u6Xh`-YIZz3G!2Wzy&O zYok|2>f6SQ@Z0M>XiJ0F2zqe89m1G=Cc9-Rv#-*ccfdwD&%Q7-M$w0)A!F@U*{xF+ zRn%hKn_u|ckbK3sVRiTxr9fRxtH%UBezc9)LXu6T!HLy0*3g@4N$x3}kj)1{o9vd- z-C{QixPZl&iJ#0f470PeXv+6Owt;4 zYc%G~_cM!__d5Co=4C?!zNBi!p7_O(c3PbezZT%Pg7Mhk17?{qD9AzVg&O z`yo;-MCcWwaG{4woGX%7(6gi;?ErP6N=hq2Y_#Fox#Y!Kvk;;n(=_Y#hfHzt6q6ID zq+K#|8s?VSbMZ7O$^5TUL*?I^@G?$cN(`KN9= zW5xfo&bp^^AEox5$j{1NI}h55ZPkW{k6g_xu$}+@YqeJBU26)%nv*jc|Dv&`xXw>R zDsL3K1cQFh=;Cd@sg~mK;;ht`=umOcIW=cYoTg}ocv4}9Z~9Ty(7Gf>upjX3l!?uQhGCh#;N1ssx5DT z#1ioX4_TtYF;uu-VW6RqZZsn^md;8@mZ99Y_o_JWUWakqg{(+AoRh%|is42blv*z* zx6U@x?eGguPPacP%A*rC@o|nkWkFcKUzCt$@mw7mlfdZpJw|tP8!l?hot~a}3!MsI znAv%sAXGQetaRDWb<@{2Vlpgvl3Q5BmZ2kgN5{=l^$nI&z0^N*Yg>Y5KFxBSeZ6}r zcY6H0+C0dz(CrJX=sf`T?{N0s-+_FFhvN=NJyg8quu?zjScUd*r8_s#gM)D;?a>{jQwJS_ofME1{Clys7aEsaFgyET zFHuv9wfZE~b9BjYVrl&fl7u_dar)i5(6p$dg|nDTvRm{S{|;@DX;)!<{Qi}@-`8FA zRHqichF0z6#E)<$8uU>{io)|umUqT=#m2J+v6k1^z@^B|sY|>o9-cn*#k2MO3zn{~ zklV&diHU(CeugfY(d`8DpQM>m-(II4%j!alF2}MwD89SPik!tny9-I?hKc#?At7^6 z?k_R9{o$uSX%Z(G%l%S4;c(ru^vEnzmDM#)Lx^8D_f?;6?7sBZJ*rSKTj;ZIA=L zgr7dYjnXITe@0+0?c`?2xm6he(Si@BwIkL7Hk&JhFl~FK|H6m&j zZ}O4xwR(Wp3F+OWKkxV&RTiI8;6(mO=-Hr^g-1zs%^R&ZXKog$V6m*spn(DIf^W~> ZnyGPvz6pkgb2Vs<7~g`WIK>k81z` literal 4346 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 r?z(l6Gprx}R_Y_=zZ1S*{*0no;Yt4m=hvD% diff --git a/src/gh/components/DF_websocket_listener/metadata.json b/src/gh/components/DF_websocket_listener/metadata.json index 377e1698..4337c214 100644 --- a/src/gh/components/DF_websocket_listener/metadata.json +++ b/src/gh/components/DF_websocket_listener/metadata.json @@ -1,11 +1,11 @@ { - "name": "DFWEBSOCKETListener", + "name": "DFWSListener", "nickname": "WSIn", "category": "diffCheck", "subcategory": "IO", - "description": "This component reads a ply file from the internet.", + "description": "This component receives a pcd via websocket connection.", "exposure": 4, - "instanceGuid": "909d9a4f-2698-4fbf-8dcb-2005f51e047f", + "instanceGuid": "4e87cc43-8f9f-4f8f-a63a-49f76229db3e", "ghpython": { "hideOutput": true, "hideInput": true, @@ -13,10 +13,34 @@ "marshalOutGuids": true, "iconDisplay": 2, "inputParameters": [ + { + "name": "i_start", + "nickname": "i_start", + "description": "Button to start the TCP server", + "optional": true, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "bool" + }, { "name": "i_load", "nickname": "i_load", - "description": "Button to import ply from url.", + "description": "Button to get the latest PCD from the buffer", + "optional": true, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "bool" + }, + { + "name": "i_stop", + "nickname": "i_stop", + "description": "Stop the server and release the port", "optional": true, "allowTreeAccess": true, "showTypeHints": true, @@ -26,9 +50,21 @@ "typeHintID": "bool" }, { - "name": "i_ply_url", - "nickname": "i_ply_url", - "description": "The url where to get the pointcloud", + "name": "i_port", + "nickname": "i_port", + "description": "The port for the connection", + "optional": true, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "int" + }, + { + "name": "i_host", + "nickname": "i_host", + "description": "The host for the connection", "optional": true, "allowTreeAccess": true, "showTypeHints": true, @@ -40,9 +76,9 @@ ], "outputParameters": [ { - "name": "o_geometry", - "nickname": "o_geo", - "description": "The mesh or pcd that was imported.", + "name": "o_cloud", + "nickname": "o_cloud", + "description": "The pcd that was received.", "optional": false, "sourceCount": 0, "graft": false diff --git a/src/gh/examples/simple_tcp_sender.py b/src/gh/examples/simple_tcp_sender.py index b7ff3fcb..348d96aa 100644 --- a/src/gh/examples/simple_tcp_sender.py +++ b/src/gh/examples/simple_tcp_sender.py @@ -20,4 +20,4 @@ def random_colored_point(): msg = json.dumps(cloud) + "\n" s.sendall(msg.encode()) print("Sent cloud with", len(cloud), "colored points") - time.sleep(1) \ No newline at end of file + time.sleep(1) diff --git a/src/gh/examples/simple_ws_sender.py b/src/gh/examples/simple_ws_sender.py new file mode 100644 index 00000000..b93f12a2 --- /dev/null +++ b/src/gh/examples/simple_ws_sender.py @@ -0,0 +1,37 @@ +# import asyncio +# import websockets +# import json +# import random + +URI = "ws://127.0.0.1:8765" # match i_host and i_port on your GH component +SEND_INTERVAL = 2.0 # seconds between sends + + +# async def send_points(uri): +# """ +# Connects once to the WebSocket server and then +# sends a random point dict every SEND_INTERVAL seconds. +# """ +# async with websockets.connect(uri) as ws: +# print(f"Connected to {uri}") +# while True: +# # Generate a random point +# pt = { +# "x": random.uniform(0, 10), +# "y": random.uniform(0, 10), +# "z": random.uniform(0, 10), +# } +# msg = json.dumps(pt) +# await ws.send(msg) +# print(f"Sent point: {pt}") +# await asyncio.sleep(SEND_INTERVAL) + + +# def main(): +# try: +# asyncio.run(send_points(URI)) +# except KeyboardInterrupt: +# print("\nSender interrupted and exiting.") + +# if __name__ == "__main__": +# main() From e09fb79342af18d60e10b9eecd7237372c23eedc Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:51:00 +0200 Subject: [PATCH 06/17] FIX websocket component --- .pre-commit-config.yaml | 1 + deps/eigen | 2 +- deps/pybind11 | 2 +- environment.yml | Bin 3008 -> 3056 bytes .../components/DF_websocket_listener/code.py | 210 +++++++++--------- src/gh/diffCheck/diffCheck.egg-info/PKG-INFO | 11 +- .../diffCheck/diffCheck.egg-info/SOURCES.txt | 5 +- .../diffCheck/diffCheck.egg-info/requires.txt | 1 + src/gh/diffCheck/setup.py | 3 +- src/gh/examples/simple_ws_sender.py | 56 +++-- 10 files changed, 139 insertions(+), 152 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d037c14..49a47fc7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,6 +34,7 @@ repos: types-requests==2.31.0, numpy==2.0.1, pytest==8.3.1, + websockets>=10.4, types-setuptools>=71.1.0.20240818 ] args: [--config=pyproject.toml] diff --git a/deps/eigen b/deps/eigen index 171bd08c..ac6955eb 160000 --- a/deps/eigen +++ b/deps/eigen @@ -1 +1 @@ -Subproject commit 171bd08ca987987c3c50f0fa5dd8914bdd42dd3b +Subproject commit ac6955ebc64b5926ca9ec5aa6218e388e949483c diff --git a/deps/pybind11 b/deps/pybind11 index af231a60..f3bb0073 160000 --- a/deps/pybind11 +++ b/deps/pybind11 @@ -1 +1 @@ -Subproject commit af231a6054669bcf8b0c6c8d023506497ef91c1a +Subproject commit f3bb00732d88557fadc5b6e710b0b8563fbfd91c diff --git a/environment.yml b/environment.yml index a24851e4efd3a194ebdabe90bb30e3eeae263342..193c4dcf58b962a74e6c7d586efb1cec4e8bef86 100644 GIT binary patch delta 56 zcmX>g{y}`h0d4~Y1_;yz(&Y@P3`q>d4EYSn4B0?-36O2aV9Q|0V8EcqV8X!5z{LOn DUU>=T delta 7 Ocmew$en5P~0d4>e*aKPs diff --git a/src/gh/components/DF_websocket_listener/code.py b/src/gh/components/DF_websocket_listener/code.py index abc41773..7a99ab02 100644 --- a/src/gh/components/DF_websocket_listener/code.py +++ b/src/gh/components/DF_websocket_listener/code.py @@ -1,118 +1,118 @@ #! python3 from ghpythonlib.componentbase import executingcomponent as component -# import threading -# import asyncio -# import json +import threading +import asyncio +import json import scriptcontext as sc -# import Rhino.Geometry as rg -# import System.Drawing as sd -#import websockets +import Rhino.Geometry as rg +import System.Drawing as sd +from websockets.server import serve class DFWSServerListener(component): def RunScript(self, i_start: bool, - i_load: bool, i_stop: bool, - i_host: str, - i_port: int): # Port to bind + i_load: bool, + i_port: int, + i_host: str): - # --- Persistent state across runs --- - sc.sticky.setdefault('ws_thread', None) - sc.sticky.setdefault('ws_loop', None) + # ─── Persistent state across runs ──────────────────────────────────── sc.sticky.setdefault('ws_server', None) - sc.sticky.setdefault('ws_buffer', []) - sc.sticky.setdefault('ws_latest', None) - sc.sticky.setdefault('status', 'Idle') + sc.sticky.setdefault('ws_loop', None) + sc.sticky.setdefault('ws_thread', None) + sc.sticky.setdefault('last_pcd', None) + sc.sticky.setdefault('loaded_pcd', None) + sc.sticky.setdefault('ws_logs', []) + sc.sticky.setdefault('ws_thread_started', False) sc.sticky.setdefault('prev_start', False) - sc.sticky.setdefault('prev_stop', False) - sc.sticky.setdefault('prev_load', False) - - # async def handler(ws, path): - # """Receive JSON-encoded dicts and buffer valid points.""" - # try: - # async for msg in ws: - # data = json.loads(msg) - # if isinstance(data, dict) and {'x','y','z'}.issubset(data): - # sc.sticky['ws_buffer'].append(data) - # sc.sticky['status'] = f"Buffered {len(sc.sticky['ws_buffer'])} pts" - # ghenv.Component.ExpireSolution(True) # noqa: F821 - # except Exception: - # pass - - # def server_thread(): - # # Create and set a new event loop in this thread - # loop = asyncio.new_event_loop() - # sc.sticky['ws_loop'] = loop - # asyncio.set_event_loop(loop) - # try: - # # Start the WebSocket server on this loop - # start_srv = websockets.serve(handler, i_host, i_port) - # server = loop.run_until_complete(start_srv) - # sc.sticky['ws_server'] = server - # sc.sticky['status'] = f"Listening ws://{i_host}:{i_port}" - # ghenv.Component.ExpireSolution(True) # noqa: F821 - # # Serve forever until stopped - # loop.run_forever() - # except Exception as ex: - # sc.sticky['status'] = f"Server error: {type(ex).__name__}: {ex}" - # ghenv.Component.ExpireSolution(True) # noqa: F821 - # finally: - # # Cleanup: wait for server to close then shutdown loop - # srv = sc.sticky.get('ws_server') - # if srv: - # loop.run_until_complete(srv.wait_closed()) - # loop.close() - # sc.sticky['ws_loop'] = None - # sc.sticky['ws_server'] = None - - # def start(): - # # Begin server thread on rising edge - # if sc.sticky['ws_thread'] is None: - # sc.sticky['status'] = 'Starting WebSocket server...' - # ghenv.Component.Message = sc.sticky['status'] # noqa: F821 - # t = threading.Thread(target=server_thread, daemon=True) - # sc.sticky['ws_thread'] = t - # t.start() - - # def stop(): - # # Signal server and loop to stop - # server = sc.sticky.get('ws_server') - # loop = sc.sticky.get('ws_loop') - # if server and loop: - # loop.call_soon_threadsafe(server.close) - # loop.call_soon_threadsafe(loop.stop) - # sc.sticky['status'] = 'Stopped' - # sc.sticky['ws_buffer'] = [] - # sc.sticky['ws_thread'] = None - # ghenv.Component.Message = sc.sticky['status'] # noqa: F821 - - # # Handle toggles - # if i_start and not sc.sticky['prev_start']: - # start() - # if i_stop and not sc.sticky['prev_stop']: - # stop() - # if i_load and not sc.sticky['prev_load']: - # buf = sc.sticky['ws_buffer'] - # if buf: - # pc = rg.PointCloud() - # for pt in buf: - # pc.Add(rg.Point3d(pt['x'], pt['y'], pt['z']), sd.Color.White) - # sc.sticky['ws_latest'] = pc - # sc.sticky['status'] = f"Retrieved {pc.Count} pts" - # sc.sticky['ws_buffer'] = [] - # else: - # sc.sticky['status'] = 'No data buffered' - # ghenv.Component.Message = sc.sticky['status'] # noqa: F821 - - # # Update previous states - # sc.sticky['prev_start'] = i_start - # sc.sticky['prev_stop'] = i_stop - # sc.sticky['prev_load'] = i_load - - # # Always update message - # ghenv.Component.Message = sc.sticky['status'] # noqa: F821 - - # o_cloud = sc.sticky.get('ws_latest') - # return [o_cloud] + sc.sticky.setdefault('prev_stop', False) + sc.sticky.setdefault('prev_load', False) + + logs = sc.sticky['ws_logs'] + + # ─── STOP server ──────────────────────────────────────────────────── + if i_stop and sc.sticky.pop('ws_thread_started', False): + server = sc.sticky.pop('ws_server', None) + loop = sc.sticky.pop('ws_loop', None) + if server and loop: + try: + server.close() + asyncio.run_coroutine_threadsafe(server.wait_closed(), loop) + logs.append("WebSocket server close initiated") + except Exception as e: + logs.append(f"Error closing server: {e}") + sc.sticky['ws_thread'] = None + logs.append("Cleared previous WebSocket server flag") + ghenv.Component.ExpireSolution(True) # noqa: F821 + + # ─── START server ──────────────────────────────────────────────────── + if i_start and not sc.sticky['ws_thread_started']: + + async def echo(ws, path): + logs.append("[GH] Client connected") + try: + async for msg in ws: + try: + pcd = json.loads(msg) + if isinstance(pcd, list) and all(isinstance(pt, (list, tuple)) and len(pt) == 6 for pt in pcd): + sc.sticky['last_pcd'] = pcd + logs.append(f"Received PCD with {len(pcd)} points") + else: + logs.append("Invalid PCD format") + except Exception as inner: + logs.append(f"PCD parse error: {inner}") + except Exception as outer: + logs.append(f"Handler crashed: {outer}") + + async def server_coro(): + loop = asyncio.get_running_loop() + sc.sticky['ws_loop'] = loop + + logs.append(f"server_coro starting on {i_host}:{i_port}") + server = await serve(echo, i_host, i_port) + sc.sticky['ws_server'] = server + logs.append(f"Listening on ws://{i_host}:{i_port}") + await server.wait_closed() + logs.append("Server coroutine exited") + + def run_server(): + try: + asyncio.run(server_coro()) + except Exception as ex: + logs.append(f"WebSocket server ERROR: {ex}") + + t = threading.Thread(target=run_server, daemon=True) + t.start() + sc.sticky['ws_thread'] = t + sc.sticky['ws_thread_started'] = True + ghenv.Component.ExpireSolution(True) # noqa: F821 + + # ─── LOAD buffered PCD on i_load rising edge ───────────────────────── + if i_load and not sc.sticky['prev_load']: + sc.sticky['loaded_pcd'] = sc.sticky.get('last_pcd') + cnt = len(sc.sticky['loaded_pcd']) if sc.sticky['loaded_pcd'] else 0 + logs.append(f"Loaded pcd with {cnt} points") + ghenv.Component.ExpireSolution(True) # noqa: F821 + + # ─── BUILD output PointCloud ──────────────────────────────────────── + raw = sc.sticky.get('loaded_pcd') + if isinstance(raw, list) and all(isinstance(pt, (list, tuple)) and len(pt) == 6 for pt in raw): + pc = rg.PointCloud() + for x, y, z, r, g, b in raw: + pt = rg.Point3d(x, y, z) + col = sd.Color.FromArgb(r, g, b) + pc.Add(pt, col) + o_cloud = pc + else: + o_cloud = None + + # ─── UPDATE UI message & return outputs ───────────────────────────── + ghenv.Component.Message = logs[-1] if logs else 'Waiting' # noqa: F821 + sc.sticky['prev_start'] = i_start + sc.sticky['prev_stop'] = i_stop + sc.sticky['prev_load'] = i_load + + + return [o_cloud] diff --git a/src/gh/diffCheck/diffCheck.egg-info/PKG-INFO b/src/gh/diffCheck/diffCheck.egg-info/PKG-INFO index f88b43ec..c8897fb2 100644 --- a/src/gh/diffCheck/diffCheck.egg-info/PKG-INFO +++ b/src/gh/diffCheck/diffCheck.egg-info/PKG-INFO @@ -1,4 +1,4 @@ -Metadata-Version: 2.4 +Metadata-Version: 2.1 Name: diffCheck Version: 1.3.0 Summary: DiffCheck is a package to check the differences between two timber structures @@ -11,14 +11,7 @@ Classifier: Programming Language :: Python :: 3.9 Description-Content-Type: text/markdown Requires-Dist: numpy Requires-Dist: pybind11>=2.5.0 -Dynamic: author -Dynamic: author-email -Dynamic: classifier -Dynamic: description -Dynamic: description-content-type -Dynamic: home-page -Dynamic: requires-dist -Dynamic: summary +Requires-Dist: websockets>=10.4 # DiffCheck: CAD-Scan comparison diff --git a/src/gh/diffCheck/diffCheck.egg-info/SOURCES.txt b/src/gh/diffCheck/diffCheck.egg-info/SOURCES.txt index 8887cb03..3c10aad8 100644 --- a/src/gh/diffCheck/diffCheck.egg-info/SOURCES.txt +++ b/src/gh/diffCheck/diffCheck.egg-info/SOURCES.txt @@ -9,11 +9,8 @@ diffCheck/df_joint_detector.py diffCheck/df_transformations.py diffCheck/df_util.py diffCheck/df_visualization.py -diffCheck/diffcheck_bindings.cp312-win_amd64.pyd diffCheck.egg-info/PKG-INFO diffCheck.egg-info/SOURCES.txt diffCheck.egg-info/dependency_links.txt diffCheck.egg-info/requires.txt -diffCheck.egg-info/top_level.txt -diffCheck/dlls/Open3D.dll -diffCheck/dlls/diffCheck.dll \ No newline at end of file +diffCheck.egg-info/top_level.txt \ No newline at end of file diff --git a/src/gh/diffCheck/diffCheck.egg-info/requires.txt b/src/gh/diffCheck/diffCheck.egg-info/requires.txt index b2195e0b..15579520 100644 --- a/src/gh/diffCheck/diffCheck.egg-info/requires.txt +++ b/src/gh/diffCheck/diffCheck.egg-info/requires.txt @@ -1,2 +1,3 @@ numpy pybind11>=2.5.0 +websockets>=10.4 diff --git a/src/gh/diffCheck/setup.py b/src/gh/diffCheck/setup.py index 181bbb0c..bbb48856 100644 --- a/src/gh/diffCheck/setup.py +++ b/src/gh/diffCheck/setup.py @@ -8,7 +8,8 @@ packages=find_packages(), install_requires=[ "numpy", - "pybind11>=2.5.0" + "pybind11>=2.5.0", + "websockets>=10.4" # other dependencies... ], description="DiffCheck is a package to check the differences between two timber structures", diff --git a/src/gh/examples/simple_ws_sender.py b/src/gh/examples/simple_ws_sender.py index b93f12a2..3cc38f6f 100644 --- a/src/gh/examples/simple_ws_sender.py +++ b/src/gh/examples/simple_ws_sender.py @@ -1,37 +1,31 @@ -# import asyncio -# import websockets -# import json -# import random +import asyncio +import websockets +import random +import json -URI = "ws://127.0.0.1:8765" # match i_host and i_port on your GH component -SEND_INTERVAL = 2.0 # seconds between sends +def random_colored_point(): + x, y, z = [round(random.uniform(-10, 10), 2) for _ in range(3)] + r, g, b = [random.randint(0, 255) for _ in range(3)] + return [x, y, z, r, g, b] -# async def send_points(uri): -# """ -# Connects once to the WebSocket server and then -# sends a random point dict every SEND_INTERVAL seconds. -# """ -# async with websockets.connect(uri) as ws: -# print(f"Connected to {uri}") -# while True: -# # Generate a random point -# pt = { -# "x": random.uniform(0, 10), -# "y": random.uniform(0, 10), -# "z": random.uniform(0, 10), -# } -# msg = json.dumps(pt) -# await ws.send(msg) -# print(f"Sent point: {pt}") -# await asyncio.sleep(SEND_INTERVAL) +async def send_pointcloud(host="127.0.0.1", port=8765): + uri = f"ws://{host}:{port}" + print(f"Connecting to {uri}…") + try: + async with websockets.connect(uri) as ws: + counter = 0 + while True: + counter += 1 + # generate and send 1 000 random points + pcd = [random_colored_point() for _ in range(1000)] + await ws.send(json.dumps(pcd)) + print(f"[{counter}] Sent PCD with {len(pcd)} points") + await asyncio.sleep(5) -# def main(): -# try: -# asyncio.run(send_points(URI)) -# except KeyboardInterrupt: -# print("\nSender interrupted and exiting.") + except Exception as e: + print(f"Connection error: {e}") -# if __name__ == "__main__": -# main() +if __name__ == "__main__": + asyncio.run(send_pointcloud(host="127.0.0.1", port=9000)) From 46c6d6fe3911559812e1f72492ee74de109f16fe Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Sun, 22 Jun 2025 14:10:12 +0200 Subject: [PATCH 07/17] FIX light refactoring and input reorder --- deps/eigen | 2 +- deps/pybind11 | 2 +- src/gh/components/DF_http_listener/code.py | 52 +++++++----- src/gh/components/DF_tcp_listener/code.py | 73 ++++++++-------- .../components/DF_tcp_listener/metadata.json | 32 +++---- .../components/DF_websocket_listener/code.py | 85 ++++++++++--------- .../DF_websocket_listener/metadata.json | 28 +++--- .../diffCheck/diffCheck.egg-info/SOURCES.txt | 5 +- src/gh/examples/simple_ws_sender.py | 2 +- 9 files changed, 146 insertions(+), 135 deletions(-) diff --git a/deps/eigen b/deps/eigen index ac6955eb..81044ec1 160000 --- a/deps/eigen +++ b/deps/eigen @@ -1 +1 @@ -Subproject commit ac6955ebc64b5926ca9ec5aa6218e388e949483c +Subproject commit 81044ec13df7608d0d9d86aff2ef9805fc69bed1 diff --git a/deps/pybind11 b/deps/pybind11 index f3bb0073..03d8f487 160000 --- a/deps/pybind11 +++ b/deps/pybind11 @@ -1 +1 @@ -Subproject commit f3bb00732d88557fadc5b6e710b0b8563fbfd91c +Subproject commit 03d8f48750ba4486a2c9aeff82e9702109db5cb3 diff --git a/src/gh/components/DF_http_listener/code.py b/src/gh/components/DF_http_listener/code.py index 891ab405..1e5f1774 100644 --- a/src/gh/components/DF_http_listener/code.py +++ b/src/gh/components/DF_http_listener/code.py @@ -16,11 +16,14 @@ def RunScript(self, i_load: bool, i_ply_url: str): - sc.sticky.setdefault('ply_url', None) #last url processed - sc.sticky.setdefault('imported_geom', None) #geo imported from ply - sc.sticky.setdefault('status_message','Idle') #status message on component - sc.sticky.setdefault('prev_load', False) #previous state of toggle - sc.sticky.setdefault('thread_running', False) #is an import thread active? + prefix = 'http' + + # initialize sticky variables + sc.sticky.setdefault(f'{prefix}_ply_url', None) # last url processed + sc.sticky.setdefault(f'{prefix}_imported_geom', None) # last geo imported from ply + sc.sticky.setdefault(f'{prefix}_status_message', "Waiting..") # status message on component + sc.sticky.setdefault(f'{prefix}_prev_load', False) # previous state of toggle + sc.sticky.setdefault(f'{prefix}_thread_running', False) # is a background thread running? def _import_job(url): """ @@ -30,7 +33,10 @@ def _import_job(url): - Extracts the new geometry (point cloud or mesh) - Cleans up the temporary file and document objects - Updates sticky state and status message + - Signals to GH that it should re-solve """ + + tmp = None try: if not url.lower().endswith('.ply'): raise ValueError("URL must end in .ply") @@ -71,43 +77,43 @@ def _import_job(url): doc.Views.Redraw() # store new geometry - sc.sticky['imported_geom'] = geom + sc.sticky[f'{prefix}_imported_geom'] = geom count = geom.Count if isinstance(geom, rg.PointCloud) else geom.Vertices.Count if isinstance(geom, rg.PointCloud): - sc.sticky['status_message'] = f"Done: {count} points" + sc.sticky[f'{prefix}_status_message'] = f"Loaded pcd with {count} pts" else: - sc.sticky['status_message'] = f"Done: {count} vertices" - ghenv.Component.Message = sc.sticky.get('status_message') # noqa: F821 + sc.sticky[f'{prefix}_status_message'] = f"Loaded mesh wih {count} vertices" + ghenv.Component.Message = sc.sticky.get(f'{prefix}_status_message') # noqa: F821 except Exception as e: - sc.sticky['imported_geom'] = None - sc.sticky['status_message'] = f"Error: {e}" + sc.sticky[f'{prefix}_imported_geom'] = None + sc.sticky[f'{prefix}_status_message'] = f"Error: {e}" finally: try: os.remove(tmp) except Exception: pass # mark thread as finished - sc.sticky['thread_running'] = False + sc.sticky[f'{prefix}_thread_running'] = False ghenv.Component.ExpireSolution(True) # noqa: F821 # check if the URL input has changed - if sc.sticky['ply_url'] != i_ply_url: - sc.sticky['ply_url'] = i_ply_url - sc.sticky['status_message'] = "URL changed. Press Load" - sc.sticky['thread_running'] = False - sc.sticky['prev_load'] = False + if sc.sticky[f'{prefix}_ply_url'] != i_ply_url: + sc.sticky[f'{prefix}_ply_url'] = i_ply_url + sc.sticky[f'{prefix}_status_message'] = "URL changed. Press Load" + sc.sticky[f'{prefix}_thread_running'] = False + sc.sticky[f'{prefix}_prev_load'] = False # start importing if Load toggle is pressed and import thread is not already running - if i_load and not sc.sticky['prev_load'] and not sc.sticky['thread_running']: - sc.sticky['status_message'] = "Loading..." - sc.sticky['thread_running'] = True + if i_load and not sc.sticky[f'{prefix}_prev_load'] and not sc.sticky[f'{prefix}_thread_running']: + sc.sticky[f'{prefix}_status_message'] = "Loading..." + sc.sticky[f'{prefix}_thread_running'] = True threading.Thread(target=_import_job, args=(i_ply_url,), daemon=True).start() - sc.sticky['prev_load'] = i_load - ghenv.Component.Message = sc.sticky.get('status_message', "") # noqa: F821 + sc.sticky[f'{prefix}_prev_load'] = i_load + ghenv.Component.Message = sc.sticky.get(f'{prefix}_status_message', "") # noqa: F821 # output - o_geometry = sc.sticky.get('imported_geom') + o_geometry = sc.sticky.get(f'{prefix}_imported_geom') return [o_geometry] diff --git a/src/gh/components/DF_tcp_listener/code.py b/src/gh/components/DF_tcp_listener/code.py index 9171762d..d732b2ff 100644 --- a/src/gh/components/DF_tcp_listener/code.py +++ b/src/gh/components/DF_tcp_listener/code.py @@ -12,21 +12,22 @@ class DFTCPListener(component): def RunScript(self, i_start: bool, - i_load: bool, i_stop: bool, - i_port: int, - i_host: str): + i_load: bool, + i_host: str, + i_port: int): + + prefix = 'tcp' - # Sticky defaults - sc.sticky.setdefault('server_sock', None) - sc.sticky.setdefault('server_started', False) - sc.sticky.setdefault('cloud_buffer_raw', []) - sc.sticky.setdefault('latest_cloud', None) - sc.sticky.setdefault('status_message', 'Waiting...') - sc.sticky.setdefault('prev_start', False) - sc.sticky.setdefault('prev_stop', False) - sc.sticky.setdefault('prev_load', False) - sc.sticky.setdefault('client_socks', []) # Track client sockets + # Sticky initialization + sc.sticky.setdefault(f'{prefix}_server_sock', None) + sc.sticky.setdefault(f'{prefix}_server_started', False) + sc.sticky.setdefault(f'{prefix}_cloud_buffer_raw', []) + sc.sticky.setdefault(f'{prefix}_latest_cloud', None) + sc.sticky.setdefault(f'{prefix}_status_message', 'Waiting..') + sc.sticky.setdefault(f'{prefix}_prev_start', False) + sc.sticky.setdefault(f'{prefix}_prev_stop', False) + sc.sticky.setdefault(f'{prefix}_prev_load', False) # Client handler def handle_client(conn): @@ -35,7 +36,7 @@ def handle_client(conn): """ buf = b'' with conn: - while sc.sticky.get('server_started', False): + while sc.sticky.get(f'{prefix}_server_started', False): try: chunk = conn.recv(4096) if not chunk: @@ -48,7 +49,7 @@ def handle_client(conn): except Exception: continue if isinstance(raw, list) and all(isinstance(pt, list) and len(pt) == 6 for pt in raw): - sc.sticky['cloud_buffer_raw'] = raw + sc.sticky[f'{prefix}_cloud_buffer_raw'] = raw except Exception: break @@ -73,49 +74,49 @@ def start_server(): sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((i_host, i_port)) sock.listen(1) - sc.sticky['server_sock'] = sock - sc.sticky['server_started'] = True - sc.sticky['status_message'] = f'Listening on {i_host}:{i_port}' + sc.sticky[f'{prefix}_server_sock'] = sock + sc.sticky[f'{prefix}_server_started'] = True + sc.sticky[f'{prefix}_status_message'] = f'Listening on {i_host}:{i_port}' # Only accept one connection to keep it long-lived threading.Thread(target=server_loop, args=(sock,), daemon=True).start() def stop_server(): - sock = sc.sticky.get('server_sock') + sock = sc.sticky.get(f'{prefix}_server_sock') if sock: try: sock.close() except Exception: pass - sc.sticky['server_sock'] = None - sc.sticky['server_started'] = False - sc.sticky['cloud_buffer_raw'] = [] - sc.sticky['status_message'] = 'Stopped' + sc.sticky[f'{prefix}_server_sock'] = None + sc.sticky[f'{prefix}_server_started'] = False + sc.sticky[f'{prefix}_cloud_buffer_raw'] = [] + sc.sticky[f'{prefix}_status_message'] = 'Stopped' # Start or stop server based on inputs - if i_start and not sc.sticky['prev_start']: + if i_start and not sc.sticky[f'{prefix}_prev_start']: start_server() - if i_stop and not sc.sticky['prev_stop']: + if i_stop and not sc.sticky[f'{prefix}_prev_stop']: stop_server() - # Load buffered points into PointCloud - if i_load and not sc.sticky['prev_load']: - raw = sc.sticky.get('cloud_buffer_raw', []) + # Load buffered points into Rhino PointCloud + if i_load and not sc.sticky[f'{prefix}_prev_load']: + raw = sc.sticky.get(f'{prefix}_cloud_buffer_raw', []) if raw: pc = rg.PointCloud() for x, y, z, r, g, b in raw: pc.Add(rg.Point3d(x, y, z), sd.Color.FromArgb(int(r), int(g), int(b))) - sc.sticky['latest_cloud'] = pc - sc.sticky['status_message'] = f'Retrieved {pc.Count} pts' + sc.sticky[f'{prefix}_latest_cloud'] = pc + sc.sticky[f'{prefix}_status_message'] = f'Loaded pcd with {pc.Count} pts' else: - sc.sticky['status_message'] = 'No data buffered' + sc.sticky[f'{prefix}_status_message'] = 'No data buffered' # Update previous states - sc.sticky['prev_start'] = i_start - sc.sticky['prev_stop'] = i_stop - sc.sticky['prev_load'] = i_load + sc.sticky[f'{prefix}_prev_start'] = i_start + sc.sticky[f'{prefix}_prev_stop'] = i_stop + sc.sticky[f'{prefix}_prev_load'] = i_load # Update UI and output - ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821 + ghenv.Component.Message = sc.sticky[f'{prefix}_status_message'] # noqa: F821 - o_cloud = sc.sticky['latest_cloud'] + o_cloud = sc.sticky[f'{prefix}_latest_cloud'] return [o_cloud] diff --git a/src/gh/components/DF_tcp_listener/metadata.json b/src/gh/components/DF_tcp_listener/metadata.json index cf678070..0b13cd90 100644 --- a/src/gh/components/DF_tcp_listener/metadata.json +++ b/src/gh/components/DF_tcp_listener/metadata.json @@ -3,7 +3,7 @@ "nickname": "TCPIn", "category": "diffCheck", "subcategory": "IO", - "description": "This component get data from a tcp sender", + "description": "This component get point cloud data from a tcp sender", "exposure": 4, "instanceGuid": "61a9cc27-864d-4892-bd39-5d97dbccbefb", "ghpython": { @@ -26,9 +26,9 @@ "typeHintID": "bool" }, { - "name": "i_load", - "nickname": "i_load", - "description": "Button to get the latest PCD from the buffer", + "name": "i_stop", + "nickname": "i_stop", + "description": "Button to stop the server and release the port", "optional": true, "allowTreeAccess": true, "showTypeHints": true, @@ -38,9 +38,9 @@ "typeHintID": "bool" }, { - "name": "i_stop", - "nickname": "i_stop", - "description": "Stop the server and release the port", + "name": "i_load", + "nickname": "i_load", + "description": "Button to get the latest PCD from the buffer", "optional": true, "allowTreeAccess": true, "showTypeHints": true, @@ -50,35 +50,35 @@ "typeHintID": "bool" }, { - "name": "i_port", - "nickname": "i_port", - "description": "The port for the connection", + "name": "i_host", + "nickname": "i_host", + "description": "The host to use for the connection", "optional": true, "allowTreeAccess": true, "showTypeHints": true, "scriptParamAccess": "item", "wireDisplay": "default", "sourceCount": 0, - "typeHintID": "int" + "typeHintID": "str" }, { - "name": "i_host", - "nickname": "i_host", - "description": "The host for the connection", + "name": "i_port", + "nickname": "i_port", + "description": "The port to use for the connection", "optional": true, "allowTreeAccess": true, "showTypeHints": true, "scriptParamAccess": "item", "wireDisplay": "default", "sourceCount": 0, - "typeHintID": "str" + "typeHintID": "int" } ], "outputParameters": [ { "name": "o_cloud", "nickname": "o_cloud", - "description": "The pcd that was received.", + "description": "The Rhino pcd that was received.", "optional": false, "sourceCount": 0, "graft": false diff --git a/src/gh/components/DF_websocket_listener/code.py b/src/gh/components/DF_websocket_listener/code.py index 7a99ab02..88378f28 100644 --- a/src/gh/components/DF_websocket_listener/code.py +++ b/src/gh/components/DF_websocket_listener/code.py @@ -15,27 +15,29 @@ def RunScript(self, i_start: bool, i_stop: bool, i_load: bool, - i_port: int, - i_host: str): - - # ─── Persistent state across runs ──────────────────────────────────── - sc.sticky.setdefault('ws_server', None) - sc.sticky.setdefault('ws_loop', None) - sc.sticky.setdefault('ws_thread', None) - sc.sticky.setdefault('last_pcd', None) - sc.sticky.setdefault('loaded_pcd', None) - sc.sticky.setdefault('ws_logs', []) - sc.sticky.setdefault('ws_thread_started', False) - sc.sticky.setdefault('prev_start', False) - sc.sticky.setdefault('prev_stop', False) - sc.sticky.setdefault('prev_load', False) - - logs = sc.sticky['ws_logs'] - - # ─── STOP server ──────────────────────────────────────────────────── - if i_stop and sc.sticky.pop('ws_thread_started', False): - server = sc.sticky.pop('ws_server', None) - loop = sc.sticky.pop('ws_loop', None) + i_host: str, + i_port: int): + + prefix = 'ws' + + # Persistent state across runs + sc.sticky.setdefault(f'{prefix}_ws_server', None) + sc.sticky.setdefault(f'{prefix}_ws_loop', None) + sc.sticky.setdefault(f'{prefix}_ws_thread', None) + sc.sticky.setdefault(f'{prefix}_last_pcd', None) + sc.sticky.setdefault(f'{prefix}_loaded_pcd', None) + sc.sticky.setdefault(f'{prefix}_ws_logs', []) + sc.sticky.setdefault(f'{prefix}_ws_thread_started', False) + sc.sticky.setdefault(f'{prefix}_prev_start', False) + sc.sticky.setdefault(f'{prefix}_prev_stop', False) + sc.sticky.setdefault(f'{prefix}_prev_load', False) + + logs = sc.sticky[f'{prefix}_ws_logs'] + + # STOP server + if i_stop and sc.sticky.pop(f'{prefix}_ws_thread_started', False): + server = sc.sticky.pop(f'{prefix}_ws_server', None) + loop = sc.sticky.pop(f'{prefix}_ws_loop', None) if server and loop: try: server.close() @@ -43,12 +45,12 @@ def RunScript(self, logs.append("WebSocket server close initiated") except Exception as e: logs.append(f"Error closing server: {e}") - sc.sticky['ws_thread'] = None + sc.sticky[f'{prefix}_ws_thread'] = None logs.append("Cleared previous WebSocket server flag") ghenv.Component.ExpireSolution(True) # noqa: F821 - # ─── START server ──────────────────────────────────────────────────── - if i_start and not sc.sticky['ws_thread_started']: + # START server + if i_start and not sc.sticky[f'{prefix}_ws_thread_started']: async def echo(ws, path): logs.append("[GH] Client connected") @@ -57,7 +59,7 @@ async def echo(ws, path): try: pcd = json.loads(msg) if isinstance(pcd, list) and all(isinstance(pt, (list, tuple)) and len(pt) == 6 for pt in pcd): - sc.sticky['last_pcd'] = pcd + sc.sticky[f'{prefix}_last_pcd'] = pcd logs.append(f"Received PCD with {len(pcd)} points") else: logs.append("Invalid PCD format") @@ -68,11 +70,11 @@ async def echo(ws, path): async def server_coro(): loop = asyncio.get_running_loop() - sc.sticky['ws_loop'] = loop + sc.sticky[f'{prefix}_ws_loop'] = loop logs.append(f"server_coro starting on {i_host}:{i_port}") server = await serve(echo, i_host, i_port) - sc.sticky['ws_server'] = server + sc.sticky[f'{prefix}_ws_server'] = server logs.append(f"Listening on ws://{i_host}:{i_port}") await server.wait_closed() logs.append("Server coroutine exited") @@ -85,19 +87,19 @@ def run_server(): t = threading.Thread(target=run_server, daemon=True) t.start() - sc.sticky['ws_thread'] = t - sc.sticky['ws_thread_started'] = True + sc.sticky[f'{prefix}_ws_thread'] = t + sc.sticky[f'{prefix}_ws_thread_started'] = True ghenv.Component.ExpireSolution(True) # noqa: F821 - # ─── LOAD buffered PCD on i_load rising edge ───────────────────────── - if i_load and not sc.sticky['prev_load']: - sc.sticky['loaded_pcd'] = sc.sticky.get('last_pcd') - cnt = len(sc.sticky['loaded_pcd']) if sc.sticky['loaded_pcd'] else 0 - logs.append(f"Loaded pcd with {cnt} points") + # LOAD buffered PCD on i_load rising edge + if i_load and not sc.sticky[f'{prefix}_prev_load']: + sc.sticky[f'{prefix}_loaded_pcd'] = sc.sticky.get(f'{prefix}_last_pcd') + cnt = len(sc.sticky[f'{prefix}_loaded_pcd']) if sc.sticky[f'{prefix}_loaded_pcd'] else 0 + logs.append(f"Loaded pcd with {cnt} pts") ghenv.Component.ExpireSolution(True) # noqa: F821 - # ─── BUILD output PointCloud ──────────────────────────────────────── - raw = sc.sticky.get('loaded_pcd') + # BUILD output PointCloud + raw = sc.sticky.get(f'{prefix}_loaded_pcd') if isinstance(raw, list) and all(isinstance(pt, (list, tuple)) and len(pt) == 6 for pt in raw): pc = rg.PointCloud() for x, y, z, r, g, b in raw: @@ -108,11 +110,10 @@ def run_server(): else: o_cloud = None - # ─── UPDATE UI message & return outputs ───────────────────────────── - ghenv.Component.Message = logs[-1] if logs else 'Waiting' # noqa: F821 - sc.sticky['prev_start'] = i_start - sc.sticky['prev_stop'] = i_stop - sc.sticky['prev_load'] = i_load - + # UPDATE UI message & return outputs + ghenv.Component.Message = logs[-1] if logs else 'Waiting..' # noqa: F821 + sc.sticky[f'{prefix}_prev_start'] = i_start + sc.sticky[f'{prefix}_prev_stop'] = i_stop + sc.sticky[f'{prefix}_prev_load'] = i_load return [o_cloud] diff --git a/src/gh/components/DF_websocket_listener/metadata.json b/src/gh/components/DF_websocket_listener/metadata.json index 4337c214..ce4707e7 100644 --- a/src/gh/components/DF_websocket_listener/metadata.json +++ b/src/gh/components/DF_websocket_listener/metadata.json @@ -26,9 +26,9 @@ "typeHintID": "bool" }, { - "name": "i_load", - "nickname": "i_load", - "description": "Button to get the latest PCD from the buffer", + "name": "i_stop", + "nickname": "i_stop", + "description": "Stop the server and release the port", "optional": true, "allowTreeAccess": true, "showTypeHints": true, @@ -38,9 +38,9 @@ "typeHintID": "bool" }, { - "name": "i_stop", - "nickname": "i_stop", - "description": "Stop the server and release the port", + "name": "i_load", + "nickname": "i_load", + "description": "Button to get the latest PCD from the buffer", "optional": true, "allowTreeAccess": true, "showTypeHints": true, @@ -50,28 +50,28 @@ "typeHintID": "bool" }, { - "name": "i_port", - "nickname": "i_port", - "description": "The port for the connection", + "name": "i_host", + "nickname": "i_host", + "description": "The host for the connection", "optional": true, "allowTreeAccess": true, "showTypeHints": true, "scriptParamAccess": "item", "wireDisplay": "default", "sourceCount": 0, - "typeHintID": "int" + "typeHintID": "str" }, { - "name": "i_host", - "nickname": "i_host", - "description": "The host for the connection", + "name": "i_port", + "nickname": "i_port", + "description": "The port to use for the connection", "optional": true, "allowTreeAccess": true, "showTypeHints": true, "scriptParamAccess": "item", "wireDisplay": "default", "sourceCount": 0, - "typeHintID": "str" + "typeHintID": "int" } ], "outputParameters": [ diff --git a/src/gh/diffCheck/diffCheck.egg-info/SOURCES.txt b/src/gh/diffCheck/diffCheck.egg-info/SOURCES.txt index 3c10aad8..e64d5fc2 100644 --- a/src/gh/diffCheck/diffCheck.egg-info/SOURCES.txt +++ b/src/gh/diffCheck/diffCheck.egg-info/SOURCES.txt @@ -9,8 +9,11 @@ diffCheck/df_joint_detector.py diffCheck/df_transformations.py diffCheck/df_util.py diffCheck/df_visualization.py +diffCheck/diffcheck_bindings.cp39-win_amd64.pyd diffCheck.egg-info/PKG-INFO diffCheck.egg-info/SOURCES.txt diffCheck.egg-info/dependency_links.txt diffCheck.egg-info/requires.txt -diffCheck.egg-info/top_level.txt \ No newline at end of file +diffCheck.egg-info/top_level.txt +diffCheck/dlls/Open3D.dll +diffCheck/dlls/diffCheck.dll \ No newline at end of file diff --git a/src/gh/examples/simple_ws_sender.py b/src/gh/examples/simple_ws_sender.py index 3cc38f6f..edf1cb40 100644 --- a/src/gh/examples/simple_ws_sender.py +++ b/src/gh/examples/simple_ws_sender.py @@ -10,7 +10,7 @@ def random_colored_point(): return [x, y, z, r, g, b] -async def send_pointcloud(host="127.0.0.1", port=8765): +async def send_pointcloud(host="127.0.0.1", port=9000): uri = f"ws://{host}:{port}" print(f"Connecting to {uri}…") try: From c546fec81d31551ed90c20cac1a9dd9bb0f4adbb Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Sun, 22 Jun 2025 15:53:42 +0200 Subject: [PATCH 08/17] ADD drop btns and panel with standard inputs similar to vizualization settings --- src/gh/components/DF_http_listener/code.py | 11 ++ src/gh/components/DF_tcp_listener/code.py | 14 ++ .../DF_visualization_settings/code.py | 128 +------------ .../components/DF_websocket_listener/code.py | 14 ++ src/gh/diffCheck/diffCheck/df_gh_canvas.py | 172 ++++++++++++++++++ 5 files changed, 219 insertions(+), 120 deletions(-) create mode 100644 src/gh/diffCheck/diffCheck/df_gh_canvas.py diff --git a/src/gh/components/DF_http_listener/code.py b/src/gh/components/DF_http_listener/code.py index 1e5f1774..9b8f7244 100644 --- a/src/gh/components/DF_http_listener/code.py +++ b/src/gh/components/DF_http_listener/code.py @@ -8,10 +8,21 @@ import Rhino import Rhino.Geometry as rg import scriptcontext as sc +from diffCheck import df_gh_canvas class DFHTTPListener(component): + def __init__(self): + try: + ghenv.Component.ExpireSolution(True) # noqa: F821 + ghenv.Component.Attributes.PerformLayout() # noqa: F821 + except NameError: + pass + + df_gh_canvas.add_button(ghenv.Component, "Load", 0, x_offset=60) # noqa: F821 + df_gh_canvas.add_panel(ghenv.Component, "Ply_url", "https://github.com/diffCheckOrg/diffCheck/raw/refs/heads/main/tests/test_data/cube_mesh.ply", 1, 60, 20) # noqa: F821 + def RunScript(self, i_load: bool, i_ply_url: str): diff --git a/src/gh/components/DF_tcp_listener/code.py b/src/gh/components/DF_tcp_listener/code.py index d732b2ff..441f55cd 100644 --- a/src/gh/components/DF_tcp_listener/code.py +++ b/src/gh/components/DF_tcp_listener/code.py @@ -7,9 +7,23 @@ import scriptcontext as sc import Rhino.Geometry as rg import System.Drawing as sd +from diffCheck import df_gh_canvas class DFTCPListener(component): + def __init__(self): + try: + ghenv.Component.ExpireSolution(True) # noqa: F821 + ghenv.Component.Attributes.PerformLayout() # noqa: F821 + except NameError: + pass + + for idx, label in enumerate(("Start", "Stop", "Load")): + df_gh_canvas.add_button( + ghenv.Component, label, idx, x_offset=60) # noqa: F821 + df_gh_canvas.add_panel(ghenv.Component, "Host", "127.0.0.1", 3, 60, 20) # noqa: F821 + df_gh_canvas.add_panel(ghenv.Component, "Port", "5000", 4, 60, 20) # noqa: F821 + def RunScript(self, i_start: bool, i_stop: bool, diff --git a/src/gh/components/DF_visualization_settings/code.py b/src/gh/components/DF_visualization_settings/code.py index 565f562c..8ec7bacb 100644 --- a/src/gh/components/DF_visualization_settings/code.py +++ b/src/gh/components/DF_visualization_settings/code.py @@ -1,124 +1,11 @@ #! python3 -import System -import typing import Rhino from ghpythonlib.componentbase import executingcomponent as component -import Grasshopper as gh -from Grasshopper import Instances from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML from diffCheck import df_visualization - - -def add_str_valuelist(self, - values_list: typing.List[str], - nickname: str, - indx: int, - X_param_coord: float, - Y_param_coord: float, - X_offset: int=87 - ) -> None: - """ - Adds a value list of string values to the component input - - :param values_list: a list of string values to add to the value list - :param nickname: the nickname of the value list - :param indx: the index of the input parameter - :param X_param_coord: the x coordinate of the input parameter - :param Y_param_coord: the y coordinate of the input parameter - :param X_offset: the offset of the value list from the input parameter - """ - param = ghenv.Component.Params.Input[indx] # noqa: F821 - if param.SourceCount == 0: - valuelist = gh.Kernel.Special.GH_ValueList() - valuelist.NickName = nickname - valuelist.Description = "Select the value to use with DFVizSettings" - selected = valuelist.FirstSelectedItem - valuelist.ListItems.Clear() - for v in values_list: - vli = gh.Kernel.Special.GH_ValueListItem(str(v),str('"' + v + '"')) - valuelist.ListItems.Add(vli) - if selected in values_list: - valuelist.SelectItem(values_list.index(selected)) - valuelist.CreateAttributes() - valuelist.Attributes.Pivot = System.Drawing.PointF( - X_param_coord - (valuelist.Attributes.Bounds.Width) - X_offset, - Y_param_coord - (valuelist.Attributes.Bounds.Height / 2 + 0.1) - ) - valuelist.Attributes.ExpireLayout() - gh.Instances.ActiveCanvas.Document.AddObject(valuelist, False) - ghenv.Component.Params.Input[indx].AddSource(valuelist) # noqa: F821 - -def add_slider(self, - nickname: str, - indx: int, - lower_bound: float, - upper_bound: float, - default_value: float, - X_param_coord: float, - Y_param_coord: float, - X_offset: int=100 - ) -> None: - """ - Adds a slider to the component input - - :param nickname: the nickname of the slider - :param indx: the index of the input parameter - :param X_param_coord: the x coordinate of the input parameter - :param Y_param_coord: the y coordinate of the input parameter - :param X_offset: the offset of the slider from the input parameter - """ - param = ghenv.Component.Params.Input[indx] # noqa: F821 - if param.SourceCount == 0: - slider = gh.Kernel.Special.GH_NumberSlider() - slider.NickName = nickname - slider.Description = "Set the value for the threshold" - slider.Slider.Minimum = System.Decimal(lower_bound) - slider.Slider.Maximum = System.Decimal(upper_bound) - slider.Slider.DecimalPlaces = 3 - slider.Slider.SmallChange = System.Decimal(0.001) - slider.Slider.LargeChange = System.Decimal(0.01) - slider.Slider.Value = System.Decimal(default_value) - slider.CreateAttributes() - slider.Attributes.Pivot = System.Drawing.PointF( - X_param_coord - (slider.Attributes.Bounds.Width) - X_offset, - Y_param_coord - (slider.Attributes.Bounds.Height / 2 - 0.1) - ) - slider.Attributes.ExpireLayout() - gh.Instances.ActiveCanvas.Document.AddObject(slider, False) - ghenv.Component.Params.Input[indx].AddSource(slider) # noqa: F821 - -def add_plane_object(self, - nickname: str, - indx: int, - X_param_coord: float, - Y_param_coord: float, - X_offset: int=75 - ) -> None: - """ - Adds a plane object to the component input - - :param nickname: the nickname of the plane object - :param indx: the index of the input parameter - :param X_param_coord: the x coordinate of the input parameter - :param Y_param_coord: the y coordinate of the input parameter - :param X_offset: the offset of the plane object from the input parameter - """ - param = ghenv.Component.Params.Input[indx] # noqa: F821 - if param.SourceCount == 0: - doc = Instances.ActiveCanvas.Document - if doc: - plane = gh.Kernel.Parameters.Param_Plane() - plane.NickName = nickname - plane.CreateAttributes() - plane.Attributes.Pivot = System.Drawing.PointF( - X_param_coord - (plane.Attributes.Bounds.Width) - X_offset, - Y_param_coord - ) - plane.Attributes.ExpireLayout() - doc.AddObject(plane, False) - ghenv.Component.Params.Input[indx].AddSource(plane) # noqa: F821 +from diffCheck import df_gh_canvas class DFVisualizationSettings(component): @@ -129,43 +16,44 @@ def __init__(self): ghenv.Component.ExpireSolution(True) # noqa: F821 ghenv.Component.Attributes.PerformLayout() # noqa: F821 params = getattr(ghenv.Component.Params, "Input") # noqa: F821 + for j in range(len(params)): Y_cord = params[j].Attributes.InputGrip.Y X_cord = params[j].Attributes.Pivot.X input_indx = j if "i_value_type" == params[j].NickName: - add_str_valuelist( + df_gh_canvas.add_str_valuelist( ghenv.Component, # noqa: F821 self.poss_value_types, "DF_value_t", input_indx, X_cord, Y_cord) if "i_palette" == params[j].NickName: - add_str_valuelist( + df_gh_canvas.add_str_valuelist( ghenv.Component, # noqa: F821 self.poss_palettes, "DF_palette", input_indx, X_cord, Y_cord) if "i_legend_height" == params[j].NickName: - add_slider( + df_gh_canvas.add_slider( ghenv.Component, # noqa: F821 "DF_legend_height", input_indx, 0.000, 20.000, 10.000, X_cord, Y_cord) if "i_legend_width" == params[j].NickName: - add_slider( + df_gh_canvas.add_slider( ghenv.Component, # noqa: F821 "DF_legend_width", input_indx, 0.000, 2.000, 0.500, X_cord, Y_cord) if "i_legend_plane" == params[j].NickName: - add_plane_object( + df_gh_canvas.add_plane_object( ghenv.Component, # noqa: F821 "DF_legend_plane", input_indx, X_cord, Y_cord) if "i_histogram_scale_factor" == params[j].NickName: - add_slider( + df_gh_canvas.add_slider( ghenv.Component, # noqa: F821 "DF_histogram_scale_factor", input_indx, diff --git a/src/gh/components/DF_websocket_listener/code.py b/src/gh/components/DF_websocket_listener/code.py index 88378f28..9696accd 100644 --- a/src/gh/components/DF_websocket_listener/code.py +++ b/src/gh/components/DF_websocket_listener/code.py @@ -8,9 +8,23 @@ import Rhino.Geometry as rg import System.Drawing as sd from websockets.server import serve +from diffCheck import df_gh_canvas class DFWSServerListener(component): + def __init__(self): + try: + ghenv.Component.ExpireSolution(True) # noqa: F821 + ghenv.Component.Attributes.PerformLayout() # noqa: F821 + except NameError: + pass + + for idx, label in enumerate(("Start", "Stop", "Load")): + df_gh_canvas.add_button( + ghenv.Component, label, idx, x_offset=60) # noqa: F821 + df_gh_canvas.add_panel(ghenv.Component, "Host", "127.0.0.1", 3, 60, 20) # noqa: F821 + df_gh_canvas.add_panel(ghenv.Component, "Port", "9000", 4, 60, 20) # noqa: F821 + def RunScript(self, i_start: bool, i_stop: bool, diff --git a/src/gh/diffCheck/diffCheck/df_gh_canvas.py b/src/gh/diffCheck/diffCheck/df_gh_canvas.py new file mode 100644 index 00000000..72e986e3 --- /dev/null +++ b/src/gh/diffCheck/diffCheck/df_gh_canvas.py @@ -0,0 +1,172 @@ +from Grasshopper import Instances +import Grasshopper as gh +import System.Drawing as sd +import System +import typing + + +def add_str_valuelist(comp, + values_list: typing.List[str], + nickname: str, + indx: int, + X_param_coord: float, + Y_param_coord: float, + X_offset: int=87 + ) -> None: + """ + Adds a value list of string values to the component input + + :param values_list: a list of string values to add to the value list + :param nickname: the nickname of the value list + :param indx: the index of the input parameter + :param X_param_coord: the x coordinate of the input parameter + :param Y_param_coord: the y coordinate of the input parameter + :param X_offset: the offset of the value list from the input parameter + """ + inp = comp.Params.Input[indx] # noqa: F821 + if inp.SourceCount == 0: + valuelist = gh.Kernel.Special.GH_ValueList() + valuelist.NickName = nickname + valuelist.Description = "Select the value to use with DFVizSettings" + selected = valuelist.FirstSelectedItem + valuelist.ListItems.Clear() + for v in values_list: + vli = gh.Kernel.Special.GH_ValueListItem(str(v), str('"' + v + '"')) + valuelist.ListItems.Add(vli) + if selected in values_list: + valuelist.SelectItem(values_list.index(selected)) + valuelist.CreateAttributes() + valuelist.Attributes.Pivot = System.Drawing.PointF( + X_param_coord - (valuelist.Attributes.Bounds.Width) - X_offset, + Y_param_coord - (valuelist.Attributes.Bounds.Height / 2 + 0.1) + ) + valuelist.Attributes.ExpireLayout() + gh.Instances.ActiveCanvas.Document.AddObject(valuelist, False) + inp.AddSource(valuelist) # noqa: F821 + + +def add_slider(comp, + nickname: str, + indx: int, + lower_bound: float, + upper_bound: float, + default_value: float, + X_param_coord: float, + Y_param_coord: float, + X_offset: int=100 + ) -> None: + """ + Adds a slider to the component input + + :param nickname: the nickname of the slider + :param indx: the index of the input parameter + :param X_param_coord: the x coordinate of the input parameter + :param Y_param_coord: the y coordinate of the input parameter + :param X_offset: the offset of the slider from the input parameter + """ + inp = comp.Params.Input[indx] # noqa: F821 + if inp.SourceCount == 0: + slider = gh.Kernel.Special.GH_NumberSlider() + slider.NickName = nickname + slider.Description = "Set the value for the threshold" + slider.Slider.Minimum = System.Decimal(lower_bound) + slider.Slider.Maximum = System.Decimal(upper_bound) + slider.Slider.DecimalPlaces = 3 + slider.Slider.SmallChange = System.Decimal(0.001) + slider.Slider.LargeChange = System.Decimal(0.01) + slider.Slider.Value = System.Decimal(default_value) + slider.CreateAttributes() + slider.Attributes.Pivot = System.Drawing.PointF( + X_param_coord - (slider.Attributes.Bounds.Width) - X_offset, + Y_param_coord - (slider.Attributes.Bounds.Height / 2 - 0.1) + ) + slider.Attributes.ExpireLayout() + gh.Instances.ActiveCanvas.Document.AddObject(slider, False) + inp.AddSource(slider) # noqa: F821 + + +def add_plane_object(comp, + nickname: str, + indx: int, + X_param_coord: float, + Y_param_coord: float, + X_offset: int=75 + ) -> None: + """ + Adds a plane object to the component input + + :param nickname: the nickname of the plane object + :param indx: the index of the input parameter + :param X_param_coord: the x coordinate of the input parameter + :param Y_param_coord: the y coordinate of the input parameter + :param X_offset: the offset of the plane object from the input parameter + """ + inp = comp.Params.Input[indx] # noqa: F821 + if inp.SourceCount == 0: + doc = Instances.ActiveCanvas.Document + if doc: + plane = gh.Kernel.Parameters.Param_Plane() + plane.NickName = nickname + plane.CreateAttributes() + plane.Attributes.Pivot = System.Drawing.PointF( + X_param_coord - (plane.Attributes.Bounds.Width) - X_offset, + Y_param_coord + ) + plane.Attributes.ExpireLayout() + doc.AddObject(plane, False) + inp.AddSource(plane) # noqa: F821 + + +def add_button(comp, nickname, indx, x_offset=60): + + inp = comp.Params.Input[indx] + # only add if nothing already connected + if inp.SourceCount == 0: + # create the one-shot button + btn = gh.Kernel.Special.GH_ButtonObject() + btn.NickName = nickname + btn.Value = False # always starts False + # build its UI attributes so we can measure size & position + btn.CreateAttributes() + + # compute pivot: left of the input grip + grip = inp.Attributes.InputGrip + # X = input pivot X, Y = grip Y + pivot_x = grip.X - btn.Attributes.Bounds.Width - x_offset + pivot_y = grip.Y - btn.Attributes.Bounds.Height/2 + btn.Attributes.Pivot = sd.PointF(pivot_x, pivot_y) + btn.Attributes.ExpireLayout() + + # drop it onto the canvas (non-grouped) + Instances.ActiveCanvas.Document.AddObject(btn, False) + # wire it into the component + inp.AddSource(btn) + + +def add_panel(comp, nickname, text, indx, x_offset=60, panel_height=20): + + inp = comp.Params.Input[indx] + if inp.SourceCount == 0: + panel = gh.Kernel.Special.GH_Panel() + # Set the panel's displayed text + panel.UserText = text + panel.NickName = nickname + panel.CreateAttributes() + + # adjust height while preserving width + bounds = panel.Attributes.Bounds + panel.Attributes.Bounds = System.Drawing.RectangleF( + bounds.X, + bounds.Y, + bounds.Width, + panel_height + ) + + # Position left of input grip + grip = inp.Attributes.InputGrip + px = grip.X - panel.Attributes.Bounds.Width - x_offset + py = grip.Y - panel.Attributes.Bounds.Height / 2 + panel.Attributes.Pivot = sd.PointF(px, py) + panel.Attributes.ExpireLayout() + Instances.ActiveCanvas.Document.AddObject(panel, False) + inp.AddSource(panel) From 85091388c806fbf05c3a42d87396ad98c1246e4e Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Thu, 24 Jul 2025 14:10:27 +0200 Subject: [PATCH 09/17] FIX rename python file --- src/gh/components/DF_http_listener/code.py | 6 +++--- src/gh/components/DF_tcp_listener/code.py | 8 ++++---- .../components/DF_visualization_settings/code.py | 14 +++++++------- src/gh/components/DF_websocket_listener/code.py | 8 ++++---- .../{df_gh_canvas.py => df_gh_canvas_utils.py} | 0 5 files changed, 18 insertions(+), 18 deletions(-) rename src/gh/diffCheck/diffCheck/{df_gh_canvas.py => df_gh_canvas_utils.py} (100%) diff --git a/src/gh/components/DF_http_listener/code.py b/src/gh/components/DF_http_listener/code.py index 9b8f7244..0cdfbfe6 100644 --- a/src/gh/components/DF_http_listener/code.py +++ b/src/gh/components/DF_http_listener/code.py @@ -8,7 +8,7 @@ import Rhino import Rhino.Geometry as rg import scriptcontext as sc -from diffCheck import df_gh_canvas +from diffCheck import df_gh_canvas_utils class DFHTTPListener(component): @@ -20,8 +20,8 @@ def __init__(self): except NameError: pass - df_gh_canvas.add_button(ghenv.Component, "Load", 0, x_offset=60) # noqa: F821 - df_gh_canvas.add_panel(ghenv.Component, "Ply_url", "https://github.com/diffCheckOrg/diffCheck/raw/refs/heads/main/tests/test_data/cube_mesh.ply", 1, 60, 20) # noqa: F821 + df_gh_canvas_utils.add_button(ghenv.Component, "Load", 0, x_offset=60) # noqa: F821 + df_gh_canvas_utils.add_panel(ghenv.Component, "Ply_url", "https://github.com/diffCheckOrg/diffCheck/raw/refs/heads/main/tests/test_data/cube_mesh.ply", 1, 60, 20) # noqa: F821 def RunScript(self, i_load: bool, diff --git a/src/gh/components/DF_tcp_listener/code.py b/src/gh/components/DF_tcp_listener/code.py index 441f55cd..543d5bb3 100644 --- a/src/gh/components/DF_tcp_listener/code.py +++ b/src/gh/components/DF_tcp_listener/code.py @@ -7,7 +7,7 @@ import scriptcontext as sc import Rhino.Geometry as rg import System.Drawing as sd -from diffCheck import df_gh_canvas +from diffCheck import df_gh_canvas_utils class DFTCPListener(component): @@ -19,10 +19,10 @@ def __init__(self): pass for idx, label in enumerate(("Start", "Stop", "Load")): - df_gh_canvas.add_button( + df_gh_canvas_utils.add_button( ghenv.Component, label, idx, x_offset=60) # noqa: F821 - df_gh_canvas.add_panel(ghenv.Component, "Host", "127.0.0.1", 3, 60, 20) # noqa: F821 - df_gh_canvas.add_panel(ghenv.Component, "Port", "5000", 4, 60, 20) # noqa: F821 + df_gh_canvas_utils.add_panel(ghenv.Component, "Host", "127.0.0.1", 3, 60, 20) # noqa: F821 + df_gh_canvas_utils.add_panel(ghenv.Component, "Port", "5000", 4, 60, 20) # noqa: F821 def RunScript(self, i_start: bool, diff --git a/src/gh/components/DF_visualization_settings/code.py b/src/gh/components/DF_visualization_settings/code.py index 8ec7bacb..61c2cb98 100644 --- a/src/gh/components/DF_visualization_settings/code.py +++ b/src/gh/components/DF_visualization_settings/code.py @@ -5,7 +5,7 @@ from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML from diffCheck import df_visualization -from diffCheck import df_gh_canvas +from diffCheck import df_gh_canvas_utils class DFVisualizationSettings(component): @@ -22,38 +22,38 @@ def __init__(self): X_cord = params[j].Attributes.Pivot.X input_indx = j if "i_value_type" == params[j].NickName: - df_gh_canvas.add_str_valuelist( + df_gh_canvas_utils.add_str_valuelist( ghenv.Component, # noqa: F821 self.poss_value_types, "DF_value_t", input_indx, X_cord, Y_cord) if "i_palette" == params[j].NickName: - df_gh_canvas.add_str_valuelist( + df_gh_canvas_utils.add_str_valuelist( ghenv.Component, # noqa: F821 self.poss_palettes, "DF_palette", input_indx, X_cord, Y_cord) if "i_legend_height" == params[j].NickName: - df_gh_canvas.add_slider( + df_gh_canvas_utils.add_slider( ghenv.Component, # noqa: F821 "DF_legend_height", input_indx, 0.000, 20.000, 10.000, X_cord, Y_cord) if "i_legend_width" == params[j].NickName: - df_gh_canvas.add_slider( + df_gh_canvas_utils.add_slider( ghenv.Component, # noqa: F821 "DF_legend_width", input_indx, 0.000, 2.000, 0.500, X_cord, Y_cord) if "i_legend_plane" == params[j].NickName: - df_gh_canvas.add_plane_object( + df_gh_canvas_utils.add_plane_object( ghenv.Component, # noqa: F821 "DF_legend_plane", input_indx, X_cord, Y_cord) if "i_histogram_scale_factor" == params[j].NickName: - df_gh_canvas.add_slider( + df_gh_canvas_utils.add_slider( ghenv.Component, # noqa: F821 "DF_histogram_scale_factor", input_indx, diff --git a/src/gh/components/DF_websocket_listener/code.py b/src/gh/components/DF_websocket_listener/code.py index 9696accd..b539408f 100644 --- a/src/gh/components/DF_websocket_listener/code.py +++ b/src/gh/components/DF_websocket_listener/code.py @@ -8,7 +8,7 @@ import Rhino.Geometry as rg import System.Drawing as sd from websockets.server import serve -from diffCheck import df_gh_canvas +from diffCheck import df_gh_canvas_utils class DFWSServerListener(component): @@ -20,10 +20,10 @@ def __init__(self): pass for idx, label in enumerate(("Start", "Stop", "Load")): - df_gh_canvas.add_button( + df_gh_canvas_utils.add_button( ghenv.Component, label, idx, x_offset=60) # noqa: F821 - df_gh_canvas.add_panel(ghenv.Component, "Host", "127.0.0.1", 3, 60, 20) # noqa: F821 - df_gh_canvas.add_panel(ghenv.Component, "Port", "9000", 4, 60, 20) # noqa: F821 + df_gh_canvas_utils.add_panel(ghenv.Component, "Host", "127.0.0.1", 3, 60, 20) # noqa: F821 + df_gh_canvas_utils.add_panel(ghenv.Component, "Port", "9000", 4, 60, 20) # noqa: F821 def RunScript(self, i_start: bool, diff --git a/src/gh/diffCheck/diffCheck/df_gh_canvas.py b/src/gh/diffCheck/diffCheck/df_gh_canvas_utils.py similarity index 100% rename from src/gh/diffCheck/diffCheck/df_gh_canvas.py rename to src/gh/diffCheck/diffCheck/df_gh_canvas_utils.py From 2588656f5e7f3fa7e318f55a9e9df501f6c46dc6 Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Fri, 25 Jul 2025 10:59:25 +0200 Subject: [PATCH 10/17] FIX remove repeated ws from stickys --- .../components/DF_websocket_listener/code.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/gh/components/DF_websocket_listener/code.py b/src/gh/components/DF_websocket_listener/code.py index b539408f..799076fb 100644 --- a/src/gh/components/DF_websocket_listener/code.py +++ b/src/gh/components/DF_websocket_listener/code.py @@ -35,23 +35,23 @@ def RunScript(self, prefix = 'ws' # Persistent state across runs - sc.sticky.setdefault(f'{prefix}_ws_server', None) - sc.sticky.setdefault(f'{prefix}_ws_loop', None) - sc.sticky.setdefault(f'{prefix}_ws_thread', None) + sc.sticky.setdefault(f'{prefix}_server', None) + sc.sticky.setdefault(f'{prefix}_loop', None) + sc.sticky.setdefault(f'{prefix}_thread', None) sc.sticky.setdefault(f'{prefix}_last_pcd', None) sc.sticky.setdefault(f'{prefix}_loaded_pcd', None) - sc.sticky.setdefault(f'{prefix}_ws_logs', []) - sc.sticky.setdefault(f'{prefix}_ws_thread_started', False) + sc.sticky.setdefault(f'{prefix}_logs', []) + sc.sticky.setdefault(f'{prefix}_thread_started', False) sc.sticky.setdefault(f'{prefix}_prev_start', False) sc.sticky.setdefault(f'{prefix}_prev_stop', False) sc.sticky.setdefault(f'{prefix}_prev_load', False) - logs = sc.sticky[f'{prefix}_ws_logs'] + logs = sc.sticky[f'{prefix}_logs'] # STOP server - if i_stop and sc.sticky.pop(f'{prefix}_ws_thread_started', False): - server = sc.sticky.pop(f'{prefix}_ws_server', None) - loop = sc.sticky.pop(f'{prefix}_ws_loop', None) + if i_stop and sc.sticky.pop(f'{prefix}_thread_started', False): + server = sc.sticky.pop(f'{prefix}_server', None) + loop = sc.sticky.pop(f'{prefix}_loop', None) if server and loop: try: server.close() @@ -59,12 +59,12 @@ def RunScript(self, logs.append("WebSocket server close initiated") except Exception as e: logs.append(f"Error closing server: {e}") - sc.sticky[f'{prefix}_ws_thread'] = None + sc.sticky[f'{prefix}_thread'] = None logs.append("Cleared previous WebSocket server flag") ghenv.Component.ExpireSolution(True) # noqa: F821 # START server - if i_start and not sc.sticky[f'{prefix}_ws_thread_started']: + if i_start and not sc.sticky[f'{prefix}_thread_started']: async def echo(ws, path): logs.append("[GH] Client connected") @@ -84,11 +84,11 @@ async def echo(ws, path): async def server_coro(): loop = asyncio.get_running_loop() - sc.sticky[f'{prefix}_ws_loop'] = loop + sc.sticky[f'{prefix}_loop'] = loop logs.append(f"server_coro starting on {i_host}:{i_port}") server = await serve(echo, i_host, i_port) - sc.sticky[f'{prefix}_ws_server'] = server + sc.sticky[f'{prefix}_server'] = server logs.append(f"Listening on ws://{i_host}:{i_port}") await server.wait_closed() logs.append("Server coroutine exited") @@ -101,8 +101,8 @@ def run_server(): t = threading.Thread(target=run_server, daemon=True) t.start() - sc.sticky[f'{prefix}_ws_thread'] = t - sc.sticky[f'{prefix}_ws_thread_started'] = True + sc.sticky[f'{prefix}_thread'] = t + sc.sticky[f'{prefix}_thread_started'] = True ghenv.Component.ExpireSolution(True) # noqa: F821 # LOAD buffered PCD on i_load rising edge From 683b4e88e8ace8745a6ba3cf011c7be6cf256085 Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Fri, 25 Jul 2025 11:34:47 +0200 Subject: [PATCH 11/17] ADD warning if trying to load data from tcp/ws listener without starting the server first --- src/gh/components/DF_tcp_listener/code.py | 21 ++++++++++++------- .../components/DF_websocket_listener/code.py | 17 +++++++++------ 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/gh/components/DF_tcp_listener/code.py b/src/gh/components/DF_tcp_listener/code.py index 543d5bb3..29919cbd 100644 --- a/src/gh/components/DF_tcp_listener/code.py +++ b/src/gh/components/DF_tcp_listener/code.py @@ -114,15 +114,20 @@ def stop_server(): # Load buffered points into Rhino PointCloud if i_load and not sc.sticky[f'{prefix}_prev_load']: - raw = sc.sticky.get(f'{prefix}_cloud_buffer_raw', []) - if raw: - pc = rg.PointCloud() - for x, y, z, r, g, b in raw: - pc.Add(rg.Point3d(x, y, z), sd.Color.FromArgb(int(r), int(g), int(b))) - sc.sticky[f'{prefix}_latest_cloud'] = pc - sc.sticky[f'{prefix}_status_message'] = f'Loaded pcd with {pc.Count} pts' + if not sc.sticky.get(f'{prefix}_server_started', False): + self.AddRuntimeMessage(self.RuntimeMessageLevel.Warning, + "Please start server here before trying to send data from remote device.") + sc.sticky[f'{prefix}_status_message'] = "Server not started" else: - sc.sticky[f'{prefix}_status_message'] = 'No data buffered' + raw = sc.sticky.get(f'{prefix}_cloud_buffer_raw', []) + if raw: + pc = rg.PointCloud() + for x, y, z, r, g, b in raw: + pc.Add(rg.Point3d(x, y, z), sd.Color.FromArgb(int(r), int(g), int(b))) + sc.sticky[f'{prefix}_latest_cloud'] = pc + sc.sticky[f'{prefix}_status_message'] = f'Loaded pcd with {pc.Count} pts' + else: + sc.sticky[f'{prefix}_status_message'] = 'No data buffered' # Update previous states sc.sticky[f'{prefix}_prev_start'] = i_start diff --git a/src/gh/components/DF_websocket_listener/code.py b/src/gh/components/DF_websocket_listener/code.py index 799076fb..0b3a118b 100644 --- a/src/gh/components/DF_websocket_listener/code.py +++ b/src/gh/components/DF_websocket_listener/code.py @@ -107,10 +107,15 @@ def run_server(): # LOAD buffered PCD on i_load rising edge if i_load and not sc.sticky[f'{prefix}_prev_load']: - sc.sticky[f'{prefix}_loaded_pcd'] = sc.sticky.get(f'{prefix}_last_pcd') - cnt = len(sc.sticky[f'{prefix}_loaded_pcd']) if sc.sticky[f'{prefix}_loaded_pcd'] else 0 - logs.append(f"Loaded pcd with {cnt} pts") - ghenv.Component.ExpireSolution(True) # noqa: F821 + if not sc.sticky.get(f'{prefix}_server'): + self.AddRuntimeMessage(self.RuntimeMessageLevel.Warning, + "Please start server here before trying to send data from remote device.") + logs.append("Server not started") + else: + sc.sticky[f'{prefix}_loaded_pcd'] = sc.sticky.get(f'{prefix}_last_pcd') + cnt = len(sc.sticky[f'{prefix}_loaded_pcd']) if sc.sticky[f'{prefix}_loaded_pcd'] else 0 + logs.append(f"Loaded pcd with {cnt} pts") + ghenv.Component.ExpireSolution(True) # noqa: F821 # BUILD output PointCloud raw = sc.sticky.get(f'{prefix}_loaded_pcd') @@ -127,7 +132,7 @@ def run_server(): # UPDATE UI message & return outputs ghenv.Component.Message = logs[-1] if logs else 'Waiting..' # noqa: F821 sc.sticky[f'{prefix}_prev_start'] = i_start - sc.sticky[f'{prefix}_prev_stop'] = i_stop - sc.sticky[f'{prefix}_prev_load'] = i_load + sc.sticky[f'{prefix}_prev_stop'] = i_stop + sc.sticky[f'{prefix}_prev_load'] = i_load return [o_cloud] From 27cad328932f5f82642a0c42f4062e12abd49e03 Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Fri, 25 Jul 2025 11:54:46 +0200 Subject: [PATCH 12/17] ADD sleep to prevent CPU spin --- src/gh/components/DF_tcp_listener/code.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gh/components/DF_tcp_listener/code.py b/src/gh/components/DF_tcp_listener/code.py index 29919cbd..6d8acf67 100644 --- a/src/gh/components/DF_tcp_listener/code.py +++ b/src/gh/components/DF_tcp_listener/code.py @@ -4,6 +4,7 @@ import socket import threading import json +import time import scriptcontext as sc import Rhino.Geometry as rg import System.Drawing as sd @@ -66,6 +67,7 @@ def handle_client(conn): sc.sticky[f'{prefix}_cloud_buffer_raw'] = raw except Exception: break + time.sleep(0.05) # sleep briefly to prevent CPU spin # thread to accept incoming connections def server_loop(sock): From bd004f73ab8aa10c3798ba5965041e3bb45bb538 Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:23:51 +0200 Subject: [PATCH 13/17] ADD doc strings --- src/gh/components/DF_http_listener/code.py | 8 ++- src/gh/components/DF_tcp_listener/code.py | 23 ++++++-- .../components/DF_websocket_listener/code.py | 21 ++++++++ .../diffCheck/diffCheck/df_gh_canvas_utils.py | 52 +++++++++++++++---- 4 files changed, 89 insertions(+), 15 deletions(-) diff --git a/src/gh/components/DF_http_listener/code.py b/src/gh/components/DF_http_listener/code.py index 0cdfbfe6..a5549651 100644 --- a/src/gh/components/DF_http_listener/code.py +++ b/src/gh/components/DF_http_listener/code.py @@ -36,8 +36,10 @@ def RunScript(self, sc.sticky.setdefault(f'{prefix}_prev_load', False) # previous state of toggle sc.sticky.setdefault(f'{prefix}_thread_running', False) # is a background thread running? - def _import_job(url): + def _import_job(url: str): + """ + Downloads and imports a .ply file from a given URL in a background thread. Background job: - Downloads the .ply file from the URL - Imports it into the active Rhino document @@ -45,6 +47,10 @@ def _import_job(url): - Cleans up the temporary file and document objects - Updates sticky state and status message - Signals to GH that it should re-solve + + :param url: A string representing a direct URL to a .ply file (e.g. from GitHub or local server). + The file must end with ".ply". + :returns: None """ tmp = None diff --git a/src/gh/components/DF_tcp_listener/code.py b/src/gh/components/DF_tcp_listener/code.py index 6d8acf67..f659561c 100644 --- a/src/gh/components/DF_tcp_listener/code.py +++ b/src/gh/components/DF_tcp_listener/code.py @@ -47,7 +47,13 @@ def RunScript(self, # Client handler def handle_client(conn): """ - reads the incoming bytes from a single client socket and stores valid data in a shared buffer + Reads the incoming bytes from a single TCP client socket and stores valid data in a shared buffer. + + :param conn: A socket object returned by `accept()` representing a live client connection. + The client is expected to send newline-delimited JSON-encoded data, where each + message is a list of 6D values: [x, y, z, r, g, b]. + + :returns: None """ buf = b'' with conn: @@ -72,8 +78,12 @@ def handle_client(conn): # thread to accept incoming connections def server_loop(sock): """ - runs in its own thread, continuously calling accept() on the listening socket - Each time a client connects, it launches a new thread running handle_client to deal with that connection + Accepts a single client connection and starts a background thread to handle it. + + :param sock: A bound and listening TCP socket created by start_server(). + This socket will accept one incoming connection, then delegate it to handle_client(). + + :returns: None. This runs as a background thread and blocks on accept(). """ try: conn, _ = sock.accept() @@ -85,6 +95,8 @@ def server_loop(sock): def start_server(): """ creates and binds a TCP socket on the given host/port, marks the server as started and then starts the accept_loop in a background thread + + :returns: None. """ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -97,6 +109,11 @@ def start_server(): threading.Thread(target=server_loop, args=(sock,), daemon=True).start() def stop_server(): + """ + Stops the running TCP server by closing the listening socket and resetting internal state. + + :returns: None. + """ sock = sc.sticky.get(f'{prefix}_server_sock') if sock: try: diff --git a/src/gh/components/DF_websocket_listener/code.py b/src/gh/components/DF_websocket_listener/code.py index 0b3a118b..33d837ed 100644 --- a/src/gh/components/DF_websocket_listener/code.py +++ b/src/gh/components/DF_websocket_listener/code.py @@ -67,6 +67,16 @@ def RunScript(self, if i_start and not sc.sticky[f'{prefix}_thread_started']: async def echo(ws, path): + """ + Handles a single WebSocket client connection and reads messages containing point cloud data. + + :param ws: A WebSocket connection object from the 'websockets' server, representing a live client. + :param path: The URL path for the connection (unused here but required by the API). + + :returns: None. Updates sc.sticky['ws_last_pcd'] with the most recent valid list of points. + Each message is expected to be a JSON list of 6-element lists: + [x, y, z, r, g, b] for each point. + """ logs.append("[GH] Client connected") try: async for msg in ws: @@ -83,6 +93,12 @@ async def echo(ws, path): logs.append(f"Handler crashed: {outer}") async def server_coro(): + """ + Coroutine that starts the WebSocket server and waits for it to be closed. + + :returns: None. Stores the server object in sc.sticky['ws_server'] and the event loop + in sc.sticky['ws_loop']. Also logs progress to sc.sticky['ws_logs']. + """ loop = asyncio.get_running_loop() sc.sticky[f'{prefix}_loop'] = loop @@ -94,6 +110,11 @@ async def server_coro(): logs.append("Server coroutine exited") def run_server(): + """ + Blocking function that runs the WebSocket server coroutine in this thread. + + :returns: None. Used as the target for a background thread. Logs errors if server startup fails. + """ try: asyncio.run(server_coro()) except Exception as ex: diff --git a/src/gh/diffCheck/diffCheck/df_gh_canvas_utils.py b/src/gh/diffCheck/diffCheck/df_gh_canvas_utils.py index 72e986e3..9a46bcd3 100644 --- a/src/gh/diffCheck/diffCheck/df_gh_canvas_utils.py +++ b/src/gh/diffCheck/diffCheck/df_gh_canvas_utils.py @@ -11,7 +11,7 @@ def add_str_valuelist(comp, indx: int, X_param_coord: float, Y_param_coord: float, - X_offset: int=87 + X_offset: int = 87 ) -> None: """ Adds a value list of string values to the component input @@ -53,7 +53,7 @@ def add_slider(comp, default_value: float, X_param_coord: float, Y_param_coord: float, - X_offset: int=100 + X_offset: int = 100 ) -> None: """ Adds a slider to the component input @@ -85,13 +85,13 @@ def add_slider(comp, inp.AddSource(slider) # noqa: F821 -def add_plane_object(comp, - nickname: str, - indx: int, - X_param_coord: float, - Y_param_coord: float, - X_offset: int=75 - ) -> None: +def add_plane_object(comp, + nickname: str, + indx: int, + X_param_coord: float, + Y_param_coord: float, + X_offset: int = 75 + ) -> None: """ Adds a plane object to the component input @@ -117,7 +117,19 @@ def add_plane_object(comp, inp.AddSource(plane) # noqa: F821 -def add_button(comp, nickname, indx, x_offset=60): +def add_button(comp, + nickname: str, + indx: int, + x_offset: int = 60 + ) -> None: + """ + Adds a one-shot Boolean button to the left of a component input. + + :param comp: The Grasshopper component to which the button will be added. + :param nickname: The display label of the button (e.g. "Start", "Load"). + :param indx: The index of the component input to wire the button into. + :param x_offset: Horizontal distance (in pixels) to place the button to the left of the input. + """ inp = comp.Params.Input[indx] # only add if nothing already connected @@ -143,7 +155,25 @@ def add_button(comp, nickname, indx, x_offset=60): inp.AddSource(btn) -def add_panel(comp, nickname, text, indx, x_offset=60, panel_height=20): +def add_panel(comp, + nickname: str, + text: str, + indx: int, + x_offset: int = 60, + panel_height: int = 20 + ) -> None: + """ + Adds a text panel to the left of a component input with a default string value. + + :param comp: The Grasshopper component to which the panel will be added. + :param nickname: The label shown at the top of the panel (e.g. "Host", "Port"). + :param text: The default string to display inside the panel. + :param indx: The index of the component input to connect the panel to. + :param x_offset: Horizontal distance (in pixels) to place the panel left of the input. + :param panel_height: Height of the panel in pixels (default is 20). + + :returns: None. The panel is created, positioned, and connected if no existing source is present. + """ inp = comp.Params.Input[indx] if inp.SourceCount == 0: From b383bc1110893974da96eb355685de38f82594df Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:34:43 +0200 Subject: [PATCH 14/17] FIX add docstrings --- src/gh/components/DF_http_listener/code.py | 2 +- src/gh/components/DF_tcp_listener/code.py | 8 ++++---- src/gh/components/DF_websocket_listener/code.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/gh/components/DF_http_listener/code.py b/src/gh/components/DF_http_listener/code.py index a5549651..c35719ba 100644 --- a/src/gh/components/DF_http_listener/code.py +++ b/src/gh/components/DF_http_listener/code.py @@ -36,7 +36,7 @@ def RunScript(self, sc.sticky.setdefault(f'{prefix}_prev_load', False) # previous state of toggle sc.sticky.setdefault(f'{prefix}_thread_running', False) # is a background thread running? - def _import_job(url: str): + def _import_job(url: str) -> None: """ Downloads and imports a .ply file from a given URL in a background thread. diff --git a/src/gh/components/DF_tcp_listener/code.py b/src/gh/components/DF_tcp_listener/code.py index f659561c..baa5d096 100644 --- a/src/gh/components/DF_tcp_listener/code.py +++ b/src/gh/components/DF_tcp_listener/code.py @@ -45,7 +45,7 @@ def RunScript(self, sc.sticky.setdefault(f'{prefix}_prev_load', False) # Client handler - def handle_client(conn): + def handle_client(conn: socket.socket) -> None: """ Reads the incoming bytes from a single TCP client socket and stores valid data in a shared buffer. @@ -76,7 +76,7 @@ def handle_client(conn): time.sleep(0.05) # sleep briefly to prevent CPU spin # thread to accept incoming connections - def server_loop(sock): + def server_loop(sock: socket.socket) -> None: """ Accepts a single client connection and starts a background thread to handle it. @@ -92,7 +92,7 @@ def server_loop(sock): pass # Start TCP server - def start_server(): + def start_server() -> None: """ creates and binds a TCP socket on the given host/port, marks the server as started and then starts the accept_loop in a background thread @@ -108,7 +108,7 @@ def start_server(): # Only accept one connection to keep it long-lived threading.Thread(target=server_loop, args=(sock,), daemon=True).start() - def stop_server(): + def stop_server() -> None: """ Stops the running TCP server by closing the listening socket and resetting internal state. diff --git a/src/gh/components/DF_websocket_listener/code.py b/src/gh/components/DF_websocket_listener/code.py index 33d837ed..3a952491 100644 --- a/src/gh/components/DF_websocket_listener/code.py +++ b/src/gh/components/DF_websocket_listener/code.py @@ -66,7 +66,7 @@ def RunScript(self, # START server if i_start and not sc.sticky[f'{prefix}_thread_started']: - async def echo(ws, path): + async def echo(ws, path: str) -> None: """ Handles a single WebSocket client connection and reads messages containing point cloud data. @@ -92,7 +92,7 @@ async def echo(ws, path): except Exception as outer: logs.append(f"Handler crashed: {outer}") - async def server_coro(): + async def server_coro() -> None: """ Coroutine that starts the WebSocket server and waits for it to be closed. @@ -109,7 +109,7 @@ async def server_coro(): await server.wait_closed() logs.append("Server coroutine exited") - def run_server(): + def run_server() -> None: """ Blocking function that runs the WebSocket server coroutine in this thread. From 7f11249ccbe3d727a00f4609b99d5be2603098fa Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:38:37 +0200 Subject: [PATCH 15/17] FIX missing lines from PKG-INFO --- src/gh/diffCheck/diffCheck.egg-info/PKG-INFO | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/gh/diffCheck/diffCheck.egg-info/PKG-INFO b/src/gh/diffCheck/diffCheck.egg-info/PKG-INFO index c8897fb2..78a74a86 100644 --- a/src/gh/diffCheck/diffCheck.egg-info/PKG-INFO +++ b/src/gh/diffCheck/diffCheck.egg-info/PKG-INFO @@ -12,6 +12,14 @@ Description-Content-Type: text/markdown Requires-Dist: numpy Requires-Dist: pybind11>=2.5.0 Requires-Dist: websockets>=10.4 +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: description-content-type +Dynamic: home-page +Dynamic: requires-dist +Dynamic: summary # DiffCheck: CAD-Scan comparison From 484810ff71f46e5f77a1e08306fc64c2a2402c4b Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:59:16 +0200 Subject: [PATCH 16/17] FIX RML.Warning --- src/gh/components/DF_tcp_listener/code.py | 4 ++-- src/gh/components/DF_websocket_listener/code.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gh/components/DF_tcp_listener/code.py b/src/gh/components/DF_tcp_listener/code.py index baa5d096..6d5371f3 100644 --- a/src/gh/components/DF_tcp_listener/code.py +++ b/src/gh/components/DF_tcp_listener/code.py @@ -9,7 +9,7 @@ import Rhino.Geometry as rg import System.Drawing as sd from diffCheck import df_gh_canvas_utils - +from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML class DFTCPListener(component): def __init__(self): @@ -134,7 +134,7 @@ def stop_server() -> None: # Load buffered points into Rhino PointCloud if i_load and not sc.sticky[f'{prefix}_prev_load']: if not sc.sticky.get(f'{prefix}_server_started', False): - self.AddRuntimeMessage(self.RuntimeMessageLevel.Warning, + self.AddRuntimeMessage(RML.Warning, "Please start server here before trying to send data from remote device.") sc.sticky[f'{prefix}_status_message'] = "Server not started" else: diff --git a/src/gh/components/DF_websocket_listener/code.py b/src/gh/components/DF_websocket_listener/code.py index 3a952491..04865e8c 100644 --- a/src/gh/components/DF_websocket_listener/code.py +++ b/src/gh/components/DF_websocket_listener/code.py @@ -9,7 +9,7 @@ import System.Drawing as sd from websockets.server import serve from diffCheck import df_gh_canvas_utils - +from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML class DFWSServerListener(component): def __init__(self): @@ -129,7 +129,7 @@ def run_server() -> None: # LOAD buffered PCD on i_load rising edge if i_load and not sc.sticky[f'{prefix}_prev_load']: if not sc.sticky.get(f'{prefix}_server'): - self.AddRuntimeMessage(self.RuntimeMessageLevel.Warning, + self.AddRuntimeMessage(RML.Warning, "Please start server here before trying to send data from remote device.") logs.append("Server not started") else: From 4d287f2d2574aea64785b37fbcdead978078d4a6 Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:27:42 +0200 Subject: [PATCH 17/17] FIX cleanup warnings that didnt work --- src/gh/components/DF_tcp_listener/code.py | 4 +--- src/gh/components/DF_websocket_listener/code.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/gh/components/DF_tcp_listener/code.py b/src/gh/components/DF_tcp_listener/code.py index 6d5371f3..c6683d97 100644 --- a/src/gh/components/DF_tcp_listener/code.py +++ b/src/gh/components/DF_tcp_listener/code.py @@ -134,9 +134,7 @@ def stop_server() -> None: # Load buffered points into Rhino PointCloud if i_load and not sc.sticky[f'{prefix}_prev_load']: if not sc.sticky.get(f'{prefix}_server_started', False): - self.AddRuntimeMessage(RML.Warning, - "Please start server here before trying to send data from remote device.") - sc.sticky[f'{prefix}_status_message'] = "Server not started" + sc.sticky[f'{prefix}_status_message'] = "Start Server First!" else: raw = sc.sticky.get(f'{prefix}_cloud_buffer_raw', []) if raw: diff --git a/src/gh/components/DF_websocket_listener/code.py b/src/gh/components/DF_websocket_listener/code.py index 04865e8c..08b0c4db 100644 --- a/src/gh/components/DF_websocket_listener/code.py +++ b/src/gh/components/DF_websocket_listener/code.py @@ -129,9 +129,7 @@ def run_server() -> None: # LOAD buffered PCD on i_load rising edge if i_load and not sc.sticky[f'{prefix}_prev_load']: if not sc.sticky.get(f'{prefix}_server'): - self.AddRuntimeMessage(RML.Warning, - "Please start server here before trying to send data from remote device.") - logs.append("Server not started") + logs.append("Start Server First!") else: sc.sticky[f'{prefix}_loaded_pcd'] = sc.sticky.get(f'{prefix}_last_pcd') cnt = len(sc.sticky[f'{prefix}_loaded_pcd']) if sc.sticky[f'{prefix}_loaded_pcd'] else 0