Initial commit — Dossier Rhino 8 Plugin

OpenStudio-Suite Architektur-Plugin fuer Rhino 8 (Mac):
- Smart-Elemente: Wand, Decke, Dach (Pult/Sattel/Walm/Mansarde),
  Oeffnungen (Fenster/Tueren mit Rahmen + Sims + Glas + Fluegel),
  Treppen (gerade · L · Wendel mit Schrittmass-Validierung)
- Live-Previews mit Step-Lines + Soll-Range-Clamping
- Bidirektionale Selection-Sync zwischen Source-Linie und Volume
- Geschoss-/Ebenen-Verwaltung mit OKFF-Persistenz
- Layouts mit PDF-Export
- Ausschnitte / Massstab / Override-Regeln
- Petrol-Gruen Theme (Rapport-konform)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 04:27:41 +02:00
commit 9dc191be4f
145 changed files with 32629 additions and 0 deletions
+666
View File
@@ -0,0 +1,666 @@
<?xml version="1.0" encoding="utf-8"?>
<!--This file was generated by Rhinoceros, DO NOT modify this file-->
<RhinoUI major_ver="1" minor_ver="0" created_on_platform="Mac" created_on_rhino_version="8.31.26126.13432" name="PAUSEUI">
<!--Window Layout display name for this file-->
<name>
<locale_1033>PAUSEUI</locale_1033>
</name>
<!--Tab panel collection definitions and dock bar placement information-->
<dock_bars major_ver="1" minor_ver="0">
<dock_bar guid="171011a9-a956-41ee-853e-3ccc0c0db1d8" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="ac3566f9-fe75-4258-9210-b1e9c05a5881">
<placement dock_location="Floating" recent_dock_location="Top" dock_band_size="36,48" dock_band_item_size="1,-1" docked_placement="0,0" float_point="226,191" float_size="1000,75" />
<tabs name="Standard Toolbars" selected_item="4bb9c817-d19f-45fd-8af2-39e9805f3e9f" display_style="BitmapAndText">
<name>
<locale_1033>Standard Toolbars</locale_1033>
<locale_1029>Standardní palety nástrojů</locale_1029>
<locale_1031>Standard-Werkzeugleisten</locale_1031>
<locale_1034>Barras de herramientas estándar</locale_1034>
<locale_1036>Barres d'outils Standard</locale_1036>
<locale_1040>Barre degli strumenti standard</locale_1040>
<locale_1041>標準ツールバー</locale_1041>
<locale_1042>표준 도구모음</locale_1042>
<locale_1045>Standardowe paski narzędzi</locale_1045>
<locale_2070>Barras de Ferramentas Standard</locale_2070>
<locale_2052>标准工具列</locale_2052>
<locale_1028>標準工具列</locale_1028>
<locale_1049>Стандартные панели инструментов</locale_1049>
</name>
<removed_item Item1="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718" Item2="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718" />
<removed_item Item1="7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" Item2="7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" />
<tool_bar guid="4bb9c817-d19f-45fd-8af2-39e9805f3e9f" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="32318c40-46e9-4aa3-8f73-09371ec27a4d" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="2ed87e2b-d225-4625-aeda-b11a115c9a14" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="720d3154-34fc-4177-8e52-9f417d4b5af3" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="79d0d952-85af-4fe3-8444-46596bbe22fd" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="9772529f-ea7a-4e2f-9dab-5beff3ce96e8" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="0608110b-184c-443f-97ec-0612d9d2b605" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="9f21066b-eb0f-46f5-adeb-d62f80e04fd7" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="d767ebe9-eebd-4e75-a217-03f7431f71bf" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar_colletion="4b37b788-3a60-4c54-a4be-67e756115945" />
<tool_bar guid="b977d038-c9b6-4a9b-b097-9592a4117052" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="c8064867-a611-4b55-b747-fc2aee5d9bf3" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="4cd9a071-9337-4389-aa40-2a20f570da3b" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="625e34b2-3b19-4aba-91c2-94f79e2e1d91" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="44619cf6-b73a-46ea-93f8-46f1fa333115" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="12428ca8-b9f4-4954-8f6e-8a139226b383" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="a5379863-ee51-4b77-ab0c-44ee93c92ca3" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="fa6d5bcc-8cd8-416b-8701-f89bd697e94d" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="d0a817a1-dea9-4e03-89e3-0d63d99b5e51" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="54dcdb3a-f098-49a4-9947-d5605d675be3" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="90fd89fc-e41f-49cc-bcf0-29e0d58017a1" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar_colletion="4b37b788-3a60-4c54-a4be-67e756115945" />
<tool_bar guid="16770b13-f7fb-4060-a6e6-607f90dd8bb3" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="26b86ed9-12ff-4198-b49e-83bd3dfa7480" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="12428ca8-b9f4-4954-8f6e-8a139226b383">
<placement dock_location="Floating" recent_dock_location="Left" float_point="-1431655808,0" float_size="266,255" />
<tabs name="SubD Sidebar" selected_item="aaa213c9-f422-477a-9782-a6ae60104171" display_style="BitmapAndText">
<name>
<locale_1033>SubD Sidebar</locale_1033>
<locale_1029>Postranní panel SubD</locale_1029>
<locale_1031>SubD-Seitenleiste</locale_1031>
<locale_1034>SubD (lateral)</locale_1034>
<locale_1036>Volet SubD</locale_1036>
<locale_1040>Barra laterale SubD</locale_1040>
<locale_1041>SubDサイドバー</locale_1041>
<locale_1042>SubD 사이드바</locale_1042>
<locale_1045>SubD - pasek boczny</locale_1045>
<locale_2070>Barra Lateral SubD</locale_2070>
<locale_2052>细分边栏</locale_2052>
<locale_1028>SubD 邊欄</locale_1028>
<locale_1049>SubD - боковая</locale_1049>
</name>
<tool_bar guid="aaa213c9-f422-477a-9782-a6ae60104171" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="2d7ded60-c2e9-4ac1-8297-df58c7e8ac4e">
<placement dock_band_size="314,-1" dock_band_item_size="-1,1" docked_placement="0,0" float_point="1309,275" float_size="200,200" visible="True" />
<tabs name="EBENEN" selected_item="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718" display_style="BitmapAndText">
<name>
<locale_1033>EBENEN</locale_1033>
</name>
<removed_item Item1="4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" Item2="4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" />
<removed_item Item1="3610bf83-047d-4f7f-93fd-163ea305b493" Item2="3610bf83-047d-4f7f-93fd-163ea305b493" />
<removed_item Item1="918191ca-1105-43f9-a34a-dda4276883c1" Item2="918191ca-1105-43f9-a34a-dda4276883c1" />
<panel guid="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="EBENEN" />
<panel guid="5c9e4f3f-6d0e-4f1f-b3c4-d5e6f7081a2b" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="AUSSCHNITTE" />
<panel guid="562bda2a-184f-4b22-9607-79d992f28557" plug_in="02bf604d-799c-4cc2-830e-8d72f21b14b7" name="Layouts" />
<panel guid="de6ef821-9355-445f-a5d5-5e19748a3b7e" plug_in="02bf604d-799c-4cc2-830e-8d72f21b14b7" name="Linetypes" />
</tabs>
</dock_bar>
<dock_bar guid="303293c9-aad0-4419-994d-6765718a58ed" unmanaged="True" text="Command">
<placement dock_location="Left" recent_dock_location="Left" dock_band_size="176,200" dock_band_item_size="-1,0.35093898" docked_placement="0,0" float_point="36,104" float_size="200,200" visible="True" />
<tabs name="Container" selected_item="971fdb61-b9c6-4080-b38f-d5c72ce7a577" display_style="BitmapAndText">
<name>
<locale_1033>Container</locale_1033>
</name>
<removed_item Item1="c7da84fc-2991-4824-832a-4f2509bd0ede" Item2="1990cfb7-2241-4dd1-b01d-735fe3be65fb" />
<removed_item Item1="4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" Item2="4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" />
<removed_item Item1="c7da84fc-2991-4824-832a-4f2509bd0ede" Item2="c3eab84e-9994-4c04-b2ef-aa4060f96168" />
<tool_bar guid="971fdb61-b9c6-4080-b38f-d5c72ce7a577" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="39716459-9788-492d-adfd-8d89a7585363">
<placement dock_band_size="494,-1" dock_band_item_size="-1,1" docked_placement="0,0" float_point="2113,787" float_size="396,796" />
<tabs name="Display &amp; Rendering" selected_item="d9ac0269-811b-47d1-aa33-777986b13715" can_be_empty="True" display_style="BitmapAndText">
<name>
<locale_1033>Display &amp; Rendering</locale_1033>
<locale_1029>Zobrazení a renderování</locale_1029>
<locale_1031>Anzeige und Rendering</locale_1031>
<locale_1034>Visualización y renderizado</locale_1034>
<locale_1036>Affichage et rendu</locale_1036>
<locale_1040>Visualizzazione e rendering</locale_1040>
<locale_1041>表示 &amp; レンダリング</locale_1041>
<locale_1042>표시와 렌더링</locale_1042>
<locale_1045>Wyświetlanie i rendering</locale_1045>
<locale_2070>Visualização e Renderização</locale_2070>
<locale_2052>显示 &amp; 渲染</locale_2052>
<locale_1028>顯示 &amp; 彩現</locale_1028>
<locale_1049>Отображение и визуализация</locale_1049>
</name>
<panel guid="d9ac0269-811b-47d1-aa33-777986b13715" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Rendering" />
<panel guid="6df2a957-f12d-42ea-9fa6-95d7920c1b76" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Materials" />
</tabs>
</dock_bar>
<dock_bar guid="4711e685-da6b-40e7-8da8-725c8f104065" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="625e34b2-3b19-4aba-91c2-94f79e2e1d91">
<placement dock_location="Floating" recent_dock_location="Top" float_point="-1431655808,0" float_size="276,216" />
<tabs name="Solids Sidebar" selected_item="f1c8658b-c36e-4835-9492-14fce56924db" display_style="BitmapAndText">
<name>
<locale_1033>Solids Sidebar</locale_1033>
<locale_1029>Postranní panel Tělesa</locale_1029>
<locale_1031>Volumenkörper-Seitenleiste</locale_1031>
<locale_1034>Sólidos (lateral)</locale_1034>
<locale_1036>Volet Solides</locale_1036>
<locale_1040>Barra laterale Solidi</locale_1040>
<locale_1041>ソリッドサイドバー</locale_1041>
<locale_1042>솔리드 사이드바</locale_1042>
<locale_1045>Bryły - pasek boczny</locale_1045>
<locale_2070>Barra lateral de Sólidos</locale_2070>
<locale_2052>实体边栏</locale_2052>
<locale_1028>實體邊欄</locale_1028>
<locale_1049>Тела - боковая</locale_1049>
</name>
<tool_bar guid="f1c8658b-c36e-4835-9492-14fce56924db" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="49d50abe-9f4d-4043-81f7-7ab1574681e8">
<placement dock_location="Top" recent_dock_location="Top" dock_band_size="-1,38" dock_band_item_size="1,-1" docked_placement="0,0" float_point="442,57" float_size="200,200" visible="True" />
<tabs name="OBERLEISTE" selected_item="7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" display_style="BitmapAndText">
<name>
<locale_1033>OBERLEISTE</locale_1033>
</name>
<panel guid="7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="OBERLEISTE" />
</tabs>
</dock_bar>
<dock_bar guid="4b37b788-3a60-4c54-a4be-67e756115945">
<placement dock_location="Floating" float_point="-1400,688" float_size="200,200" />
<tabs name="Curve drawing sidebar" selected_item="1990cfb7-2241-4dd1-b01d-735fe3be65fb" can_be_empty="True" display_style="BitmapAndText">
<name>
<locale_1033>Curve drawing sidebar</locale_1033>
<locale_1029>Postranní panel Křivka</locale_1029>
<locale_1031>Kurvenzeichnung-Seitenleiste</locale_1031>
<locale_1034>Dibujo de curvas (lateral)</locale_1034>
<locale_1036>Volet Dessin de courbes</locale_1036>
<locale_1040>Barra laterale disegno curve</locale_1040>
<locale_1041>曲線作成サイドバー</locale_1041>
<locale_1042>커브 그리기 사이드바</locale_1042>
<locale_1045>Rysowanie krzywych - pasek boczny</locale_1045>
<locale_2070>Barra lateral de desenhar curva</locale_2070>
<locale_2052>曲线绘制边栏</locale_2052>
<locale_1028>繪製曲線邊欄</locale_1028>
<locale_1049>Кривые - боковая</locale_1049>
</name>
<tool_bar guid="1990cfb7-2241-4dd1-b01d-735fe3be65fb" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="50eb6c7b-0be0-4166-ad17-ea694c97a6f4" text="Drag Strength">
<placement dock_location="Floating" dock_band_size="222,69" float_point="500,400" float_size="230,77" />
</dock_bar>
<dock_bar guid="515a8004-3ff8-49e5-ac9a-356dab83a2d7">
<placement dock_location="Floating" recent_dock_location="Bottom" dock_band_size="-1,61" docked_placement="0,1" float_point="526,177" float_size="730,663" />
<tabs name="Layers" selected_item="3610bf83-047d-4f7f-93fd-163ea305b493" torn_off="True" display_style="BitmapAndText">
<name>
<locale_1033>Layers</locale_1033>
</name>
<panel guid="3610bf83-047d-4f7f-93fd-163ea305b493" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Layers" />
</tabs>
</dock_bar>
<dock_bar guid="5a5b89a0-830f-c90d-5f60-35c2e907e18c">
<placement dock_location="Floating" dock_band_size="250,200" dock_band_item_size="-1,1" docked_placement="0,0" float_point="318,215" float_size="400,574" />
<tabs name="Right Container" selected_item="b68e9e9f-c79c-473c-a7ef-846a11dc4e7b" display_style="BitmapAndText">
<name>
<locale_1033>Right Container</locale_1033>
<locale_1029>Pravý kontejner</locale_1029>
<locale_1031>Rechter Container</locale_1031>
<locale_1034>Contenedor derecho</locale_1034>
<locale_1036>Conteneur droit</locale_1036>
<locale_1040>Contenitore destro</locale_1040>
<locale_1041>右コンテナ</locale_1041>
<locale_1042>오른쪽 컨테이너</locale_1042>
<locale_1045>Prawy zbiornik</locale_1045>
<locale_2070>Contentor Direito</locale_2070>
<locale_2052>右侧容器</locale_2052>
<locale_1028>右側容器</locale_1028>
<locale_1049>Правый контейнер</locale_1049>
</name>
<removed_item Item1="562bda2a-184f-4b22-9607-79d992f28557" Item2="562bda2a-184f-4b22-9607-79d992f28557" />
<panel guid="b68e9e9f-c79c-473c-a7ef-846a11dc4e7b" name="Display" />
<panel guid="0f8fb4f9-c213-4a6e-8e79-0bece02df82a" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Help" />
<panel guid="34ffb674-c504-49d9-9fcd-99cc811dcda2" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Properties" />
<panel guid="6df55e69-e102-4a72-b181-11664046c93f" plug_in="02bf604d-799c-4cc2-830e-8d72f21b14b7" name="Layer States" />
</tabs>
</dock_bar>
<dock_bar guid="646dd24d-5637-49bf-a7d1-1610285ea402">
<placement dock_location="Left" recent_dock_location="Left" dock_band_size="176,-1" dock_band_item_size="-1,0.649061" docked_placement="0,1" float_point="75,120" float_size="200,200" visible="True" />
<tabs name="GESTALTUNG" selected_item="4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" display_style="BitmapAndText">
<name>
<locale_1033>GESTALTUNG</locale_1033>
</name>
<panel guid="4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="GESTALTUNG" />
</tabs>
</dock_bar>
<dock_bar guid="663028ce-8797-466b-a833-9fd338f4bee2">
<placement dock_location="Floating" float_point="1186,0" float_size="200,200" />
<tabs name="OVERRIDES" selected_item="8f2a7b6c-9f3a-4f4d-e6f7-08192a3c4d62" display_style="BitmapAndText">
<name>
<locale_1033>OVERRIDES</locale_1033>
</name>
<panel guid="8f2a7b6c-9f3a-4f4d-e6f7-08192a3c4d62" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="OVERRIDES" />
</tabs>
</dock_bar>
<dock_bar guid="78ce0f5a-73ce-4268-b6c9-7d815cfc9f04" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="54dcdb3a-f098-49a4-9947-d5605d675be3">
<placement dock_location="Floating" recent_dock_location="Top" float_point="500,215" float_size="276,258" />
<tabs name="Render Sidebar" selected_item="ee7a5f30-1d7c-4a0f-9ebc-138d9c096aeb" display_style="BitmapAndText">
<name>
<locale_1033>Render Sidebar</locale_1033>
<locale_1029>Postranní panel Render</locale_1029>
<locale_1031>Render-Seitenleiste</locale_1031>
<locale_1034>Renderizado (lateral)</locale_1034>
<locale_1036>Volet Rendu</locale_1036>
<locale_1040>Barra laterale Rendering</locale_1040>
<locale_1041>レンダリングサイドバー</locale_1041>
<locale_1042>렌더링 사이드바</locale_1042>
<locale_1045>Rendering - pasek boczny</locale_1045>
<locale_2070>Barra lateral de Renderizar</locale_2070>
<locale_2052>渲染边栏</locale_2052>
<locale_1028>彩現邊欄</locale_1028>
<locale_1049>Визуализация - боковая</locale_1049>
</name>
<tool_bar guid="ee7a5f30-1d7c-4a0f-9ebc-138d9c096aeb" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="7ed74116-8783-46fe-9fe1-937283478ea9">
<placement dock_location="Floating" float_point="1196,6" float_size="200,200" />
<tabs name="OBERLEISTE" selected_item="7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" display_style="BitmapAndText">
<name>
<locale_1033>OBERLEISTE</locale_1033>
</name>
<panel guid="7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="OBERLEISTE" />
</tabs>
</dock_bar>
<dock_bar guid="8b7142c1-c80b-4f2d-aa2a-b53548818d2b">
<placement dock_location="Floating" recent_dock_location="Left" dock_band_size="176,-1" dock_band_item_size="-1,0.33333334" docked_placement="0,1" float_point="98,411" float_size="200,200" />
<tabs name="WERKZEUGE" selected_item="6d9f5040-7e1f-4f2b-c4d5-f6071829304a" display_style="BitmapAndText">
<name>
<locale_1033>WERKZEUGE</locale_1033>
</name>
<panel guid="6d9f5040-7e1f-4f2b-c4d5-f6071829304a" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="WERKZEUGE" />
</tabs>
</dock_bar>
<dock_bar guid="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c">
<placement dock_location="Floating" dock_band_size="381,-1" dock_band_item_size="-1,1" docked_placement="1,0" float_point="617,148" float_size="396,796" />
<tabs name="Named Views" selected_item="8df2a957-f12d-42ea-9fa6-95d7920c1b76" torn_off="True" display_style="BitmapAndText">
<name>
<locale_1033>Named Views</locale_1033>
<locale_1029>Pojmenované pohledy</locale_1029>
<locale_1031>Benannte Ansichten</locale_1031>
<locale_1034>Vistas guardadas</locale_1034>
<locale_1036>Vues nommées</locale_1036>
<locale_1040>Viste con nome</locale_1040>
<locale_1041>名前の付いたビュー</locale_1041>
<locale_1042>명명된 뷰</locale_1042>
<locale_1045>Nazwane widoki</locale_1045>
<locale_2070>Vistas Com Nome</locale_2070>
<locale_2052>已命名视图</locale_2052>
<locale_1028>已命名視圖</locale_1028>
<locale_1049>Именованные виды</locale_1049>
</name>
<panel guid="77d33034-194d-4cd5-957c-730d9a9eac50" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Named Views" />
<panel guid="7df2a957-f12d-42ea-9fa6-95d7920c1b76" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Environments" />
<panel guid="987b1930-ecde-4e62-8282-97ab4ad325fe" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Ground Plane" />
<panel guid="1012681e-d276-49d3-9cd9-7de92dc2404a" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Sun" />
<panel guid="8df2a957-f12d-42ea-9fa6-95d7920c1b76" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Textures" />
<panel guid="f4424a46-8281-430a-b03d-911dc9b40294" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Snapshots" />
<panel guid="86777b3d-3d68-4965-84f8-9e019c402433" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Lights" />
<panel guid="b70a4973-99ca-40c0-b2b2-f03417a5ff1d" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Render Libraries" />
</tabs>
</dock_bar>
<dock_bar guid="a3439fb9-29f1-457c-915a-9e67a9876ecb">
<placement dock_location="Floating" float_point="119,113" float_size="200,200" />
<tabs name="GESTALTUNG" selected_item="4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" display_style="BitmapAndText">
<name>
<locale_1033>GESTALTUNG</locale_1033>
</name>
<panel guid="4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="GESTALTUNG" />
</tabs>
</dock_bar>
<dock_bar guid="a4206be9-c9bf-4cbc-a80d-00369bbb5392" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="c8064867-a611-4b55-b747-fc2aee5d9bf3">
<placement dock_location="Floating" recent_dock_location="Top" float_point="-1431655808,0" float_size="276,258" />
<tabs name="Surface Sidebar" selected_item="f6534c6e-b451-4c7a-b8ec-8c3ff3596913" display_style="BitmapAndText">
<name>
<locale_1033>Surface Sidebar</locale_1033>
<locale_1029>Postranní panel Plocha</locale_1029>
<locale_1031>Flächen-Seitenleiste</locale_1031>
<locale_1034>Superficies (lateral)</locale_1034>
<locale_1036>Volet Surface</locale_1036>
<locale_1040>Barra laterale Superfici</locale_1040>
<locale_1041>サーフェスサイドバー</locale_1041>
<locale_1042>서피스 사이드바</locale_1042>
<locale_1045>Powierzchnia - pasek boczny</locale_1045>
<locale_2070>Superfícies</locale_2070>
<locale_2052>曲面边栏</locale_2052>
<locale_1028>曲面邊欄</locale_1028>
<locale_1049>Поверхности - боковая</locale_1049>
</name>
<tool_bar guid="f6534c6e-b451-4c7a-b8ec-8c3ff3596913" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="a6e1bdd1-20c0-4768-8fbf-49f8ca435efd">
<placement dock_location="Floating" dock_band_size="305,64" dock_band_item_size="0.5,0.29265139" docked_placement="0,1" float_point="924,629" float_size="200,200" />
<tabs name="Command History" selected_item="1d3d1785-2332-428b-a838-b2fe39ec50f4" display_style="BitmapAndText">
<name>
<locale_1033>Command History</locale_1033>
<locale_1042>명령 히스토리</locale_1042>
<locale_1041>コマンドヒストリ</locale_1041>
<locale_1049>История команд</locale_1049>
<locale_1031>Befehlsverlauf</locale_1031>
<locale_2052>指令历史</locale_2052>
<locale_1034>Historial de comandos</locale_1034>
<locale_1036>Historique des commandes</locale_1036>
<locale_1028>指令歷史</locale_1028>
<locale_1045>Historia poleceń</locale_1045>
<locale_1040>Storico comandi</locale_1040>
<locale_1029>Historie příkazů</locale_1029>
<locale_2070>Histórico de Comandos</locale_2070>
</name>
<panel guid="1d3d1785-2332-428b-a838-b2fe39ec50f4" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Command History" />
<panel guid="918191ca-1105-43f9-a34a-dda4276883c1" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Selection Filters" />
</tabs>
</dock_bar>
<dock_bar guid="b8af1686-2da1-4410-9d37-f5be12356127">
<placement dock_location="Floating" float_point="437,471" float_size="200,200" />
<tabs name="Rectangle" selected_item="31f7922f-85ce-4387-b923-c08dde67ccca" torn_off="True" display_style="BitmapAndText">
<name>
<locale_1033>Rectangle</locale_1033>
</name>
<tool_bar guid="31f7922f-85ce-4387-b923-c08dde67ccca" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="be2f31c8-f79d-4ca9-953e-8ab3b8cd10e6">
<placement dock_location="Floating" float_point="910,176" float_size="92,116" />
<tabs name="Help" selected_item="2337f242-b576-41a4-aace-4a74772bc72e" torn_off="True" display_style="BitmapAndText">
<name>
<locale_1033>Help</locale_1033>
<locale_1042>도움말</locale_1042>
<locale_1041>ヘルプ</locale_1041>
<locale_1049>Справка</locale_1049>
<locale_1031>Hilfe</locale_1031>
<locale_2052>说明</locale_2052>
<locale_1034>Ayuda</locale_1034>
<locale_1036>Aide</locale_1036>
<locale_1028>說明</locale_1028>
<locale_1045>Pomoc</locale_1045>
<locale_1040>Aiuti</locale_1040>
<locale_1029>Nápověda</locale_1029>
<locale_2070>Ajuda</locale_2070>
</name>
<tool_bar guid="2337f242-b576-41a4-aace-4a74772bc72e" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="cb243957-ad21-4b74-901c-bb068e455e76">
<placement dock_location="Floating" float_point="1221,15" float_size="200,200" />
<tabs name="WERKZEUGE" selected_item="6d9f5040-7e1f-4f2b-c4d5-f6071829304a" display_style="BitmapAndText">
<name>
<locale_1033>WERKZEUGE</locale_1033>
</name>
<panel guid="6d9f5040-7e1f-4f2b-c4d5-f6071829304a" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="WERKZEUGE" />
</tabs>
</dock_bar>
<dock_bar guid="d1225ee0-cf16-4f05-b053-60aa46183ac0" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="42b36785-e767-4c66-8704-4d828bcb0225">
<placement dock_location="Left" recent_dock_location="Left" float_point="-1431655808,0" float_size="276,300" />
<tabs name="Main" selected_item="971fdb61-b9c6-4080-b38f-d5c72ce7a577" display_style="BitmapAndText">
<name>
<locale_1033>Main</locale_1033>
<locale_1029>Hlavní</locale_1029>
<locale_1031>Haupt</locale_1031>
<locale_1034>Principal</locale_1034>
<locale_1036>Principale</locale_1036>
<locale_1040>Principale</locale_1040>
<locale_1041>メイン</locale_1041>
<locale_1042>메인</locale_1042>
<locale_1045>Główne</locale_1045>
<locale_2070>Principal</locale_2070>
<locale_2052>主要</locale_2052>
<locale_1028>主要</locale_1028>
<locale_1049>Главная</locale_1049>
</name>
<tool_bar guid="971fdb61-b9c6-4080-b38f-d5c72ce7a577" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="d3c4a392-88de-4c4f-88a4-ba5636ef7f38">
<placement dock_location="Left" recent_dock_location="Left" dock_band_size="126,22" dock_band_item_size="1,0.5" docked_placement="0,1" float_point="628,1070" float_size="219,279" />
<tabs name="OSnap" selected_item="d3c4a392-88de-4c4f-88a4-ba5636ef7f38" display_style="BitmapAndText">
<name>
<locale_1033>OSnap</locale_1033>
<locale_1029>Uchop</locale_1029>
<locale_1031>Ofang</locale_1031>
<locale_1034>RefObj</locale_1034>
<locale_1036>Accrochages</locale_1036>
<locale_1040>Osnap</locale_1040>
<locale_1041>OSnap</locale_1041>
<locale_1042>개체스냅</locale_1042>
<locale_1045>UchwytOb</locale_1045>
<locale_2070>OSnap</locale_2070>
<locale_2052>物件锁点</locale_2052>
<locale_1028>物件鎖點</locale_1028>
<locale_1049>Привязка</locale_1049>
</name>
<panel guid="d3c4a392-88de-4c4f-88a4-ba5636ef7f38" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Osnap" />
<panel guid="918191ca-1105-43f9-a34a-dda4276883c1" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Selection Filters" />
</tabs>
</dock_bar>
<dock_bar guid="d81b7680-00fd-4c5a-9c14-9738afa2738f">
<placement dock_location="Floating" float_point="486,72" float_size="398,791" />
<tabs name="Layers" selected_item="3610bf83-047d-4f7f-93fd-163ea305b493" display_style="BitmapAndText">
<name>
<locale_1033>Layers</locale_1033>
</name>
<panel guid="3610bf83-047d-4f7f-93fd-163ea305b493" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Layers" />
</tabs>
</dock_bar>
<dock_bar guid="f8acd7e3-6464-4fe1-93ee-87c36d31880f" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="fa6d5bcc-8cd8-416b-8701-f89bd697e94d">
<placement dock_location="Floating" recent_dock_location="Top" float_point="128,256" float_size="234,174" />
<tabs name="Mesh Sidebar" selected_item="c3eab84e-9994-4c04-b2ef-aa4060f96168" display_style="BitmapAndText">
<name>
<locale_1033>Mesh Sidebar</locale_1033>
<locale_1029>Postranní panel Síť</locale_1029>
<locale_1031>Polygonnetz-Seitenleiste</locale_1031>
<locale_1034>Malla (lateral)</locale_1034>
<locale_1036>Volet Maillage</locale_1036>
<locale_1040>Barra laterale Mesh</locale_1040>
<locale_1041>メッシュサイドバー</locale_1041>
<locale_1042>메쉬 사이드바</locale_1042>
<locale_1045>Siatka - pasek boczny</locale_1045>
<locale_2070>Barra lateral de Malha</locale_2070>
<locale_2052>网格边栏</locale_2052>
<locale_1028>網格邊欄</locale_1028>
<locale_1049>Сети - боковая</locale_1049>
</name>
<tool_bar guid="c3eab84e-9994-4c04-b2ef-aa4060f96168" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="c4f7e3af-09ff-4844-b6dc-8f7a65e2f908" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="8f714f50-afd9-4e90-88a8-1432cdcfb431" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="11b1e4f8-1e1d-4caa-9796-cee7fad3ee3b" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
</dock_bars>
<last_collection_panel_was_in>
<item guid="0dfcac10-303b-48a3-b541-88316b2a719c" dock_bar="71a6e2aa-d426-4fcf-aac6-0b5761762f1b" />
<item guid="0f8fb4f9-c213-4a6e-8e79-0bece02df82a" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="1012681e-d276-49d3-9cd9-7de92dc2404a" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="1d3d1785-2332-428b-a838-b2fe39ec50f4" dock_bar="a6e1bdd1-20c0-4768-8fbf-49f8ca435efd" />
<item guid="1d55d702-028c-4aab-99cc-acfdd441fe5f" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="34ffb674-c504-49d9-9fcd-99cc811dcda2" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="3610bf83-047d-4f7f-93fd-163ea305b493" dock_bar="d81b7680-00fd-4c5a-9c14-9738afa2738f" />
<item guid="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718" dock_bar="2d7ded60-c2e9-4ac1-8297-df58c7e8ac4e" />
<item guid="3d1dfae0-8786-46a3-94dc-130c6a6e78bf" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="4a985c3a-ad29-4f8d-927f-6629dd8d355a" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" dock_bar="646dd24d-5637-49bf-a7d1-1610285ea402" />
<item guid="52606b82-9bc4-493a-b2b4-d2073d995529" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="562bda2a-184f-4b22-9607-79d992f28557" dock_bar="2d7ded60-c2e9-4ac1-8297-df58c7e8ac4e" />
<item guid="5c9e4f3f-6d0e-4f1f-b3c4-d5e6f7081a2b" dock_bar="2d7ded60-c2e9-4ac1-8297-df58c7e8ac4e" />
<item guid="679af970-96d0-4c3a-831d-b4ff878e2884" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="6b6ffd64-c279-4b45-9959-e7e5a8eef806" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="6d9f5040-7e1f-4f2b-c4d5-f6071829304a" dock_bar="8b7142c1-c80b-4f2d-aa2a-b53548818d2b" />
<item guid="6df2a957-f12d-42ea-9fa6-95d7920c1b76" dock_bar="39716459-9788-492d-adfd-8d89a7585363" />
<item guid="6df55e69-e102-4a72-b181-11664046c93f" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="77d33034-194d-4cd5-957c-730d9a9eac50" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="7df2a957-f12d-42ea-9fa6-95d7920c1b76" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" dock_bar="49d50abe-9f4d-4043-81f7-7ab1574681e8" />
<item guid="86777b3d-3d68-4965-84f8-9e019c402433" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="8df2a957-f12d-42ea-9fa6-95d7920c1b76" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="8f23551a-a05b-4a03-a8d5-3e2fc55e4d8a" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="8f2a7b6c-9f3a-4f4d-e6f7-08192a3c4d62" dock_bar="663028ce-8797-466b-a833-9fd338f4bee2" />
<item guid="8fa84eff-1da5-4788-a983-e1ec3785e6a8" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="91799cf0-059a-46f8-854c-cc1c1419e29f" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="918191ca-1105-43f9-a34a-dda4276883c1" dock_bar="a6e1bdd1-20c0-4768-8fbf-49f8ca435efd" />
<item guid="987b1930-ecde-4e62-8282-97ab4ad325fe" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="9a0fa999-295d-4d77-b160-074fa2cd8e6d" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="a650e8dd-3896-43a8-9359-0e7ad8daf38e" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="b68e9e9f-c79c-473c-a7ef-846a11dc4e7b" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="b70a4973-99ca-40c0-b2b2-f03417a5ff1d" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="d3c4a392-88de-4c4f-88a4-ba5636ef7f38" dock_bar="d3c4a392-88de-4c4f-88a4-ba5636ef7f38" />
<item guid="d9ac0269-811b-47d1-aa33-777986b13715" dock_bar="39716459-9788-492d-adfd-8d89a7585363" />
<item guid="de6ef821-9355-445f-a5d5-5e19748a3b7e" dock_bar="2d7ded60-c2e9-4ac1-8297-df58c7e8ac4e" />
<item guid="f4424a46-8281-430a-b03d-911dc9b40294" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
</last_collection_panel_was_in>
<!--Visible items in each dock site-->
<dock_sites major_ver="1" minor_ver="0">
<dock_site location="Left" auto_hide="False">
<band size="176">
<dock_bar guid="303293c9-aad0-4419-994d-6765718a58ed" size="0.35093897581100464" />
<dock_bar guid="646dd24d-5637-49bf-a7d1-1610285ea402" size="0.6490610241889954" />
</band>
</dock_site>
<dock_site location="Right" auto_hide="False">
<band size="314">
<dock_bar guid="2d7ded60-c2e9-4ac1-8297-df58c7e8ac4e" />
</band>
</dock_site>
<dock_site location="Top" auto_hide="False">
<band size="38">
<dock_bar guid="49d50abe-9f4d-4043-81f7-7ab1574681e8" />
</band>
</dock_site>
<dock_site location="Bottom" auto_hide="False" />
</dock_sites>
<ui_settings HideSingleSidebarTab="True" IsSidebarVisible="False" TabIconSize="16" ToolBarImageSize="16" />
<referenced_rui_files>
<rui_file guid="c7da84fc-2991-4824-832a-4f2509bd0ede" source="Rhino.UI.Resources.rui.default.rui">
<!--Diff files for this RUI file-->
<modifications>
<!--This file was generated by Rhinoceros and contains modified RUI file content-->
<RhinoUI major_ver="1" minor_ver="0" guid="c7da84fc-2991-4824-832a-4f2509bd0ede">
<extend_rhino_menus />
<menus />
<tool_bars>
<tool_bar source_guid="31f7922f-85ce-4387-b923-c08dde67ccca" modified="True" LastTornOffSize="200,200">
<tool_bar_items>
<tool_bar_item source_guid="e9184546-a83a-4514-9336-fc134c34db14" />
<tool_bar_item source_guid="41b9ebc0-f4ae-4b72-938e-6a66a1da1dbe" />
<tool_bar_item source_guid="292e811f-0fba-47bf-af32-776b7cf125b5" />
<tool_bar_item source_guid="e72cfefa-421a-4a42-8eb0-e265ae99b2c7" />
<tool_bar_item source_guid="72399f31-536d-4956-8861-f4d65e61acd0" />
</tool_bar_items>
</tool_bar>
<tool_bar guid="caa6efe7-4589-4095-9c57-ffd59f161e5c" modified="True" item_display_style="Bitmap">
<text>
<locale_1033>Toolbar</locale_1033>
</text>
<tool_bar_items />
</tool_bar>
<tool_bar source_guid="971fdb61-b9c6-4080-b38f-d5c72ce7a577" modified="True">
<tool_bar_items>
<tool_bar_item guid="9ab5f13a-ea7c-4315-a320-0f669e5aa7c8" source_guid="790ba6f5-a79c-4d7c-b42b-2cabb7bbd82c" modified="True" display_style="BitmapAndText" display_style_from_parent="False">
<left_macro guid="02a1a1f5-4edb-459c-8bec-70fa3afbf7c0" source_guid="be8dcb66-2e68-49c9-8994-13c16119e652" modified="True" />
<right_macro source_guid="cc3ba388-866f-407e-a0b5-8a57867105b0" />
<link link_style="Normal" source_guid="9887392d-6705-4199-a660-8284f7fe27cf" />
</tool_bar_item>
<tool_bar_item guid="fbc0dfb2-b001-4bca-b433-583377ca6f9e" source_guid="08f422b0-883d-407e-9b32-0c7c2abdc566" modified="True" display_style="BitmapAndText" display_style_from_parent="False">
<left_macro source_guid="f13a992c-48f9-4c56-8d7b-c79f34b35e74" />
<right_macro source_guid="0a9e0109-9c39-4522-b31f-355eac091b1d" />
<link link_style="Normal" source_guid="9efa5588-4e9f-48b8-831e-1a97df6bbbaa" />
</tool_bar_item>
<tool_bar_item guid="66145d10-7f51-4373-9974-ed774d7c09ca" source_guid="f6d7af12-0c34-4d5b-b325-7132167a85c8" modified="True" display_style="BitmapAndText" display_style_from_parent="False">
<left_macro source_guid="f0baa3c0-222e-4b55-89d9-20fce315f9e2" />
<right_macro source_guid="862fa3f5-693b-4f23-bb27-cf44076fe53d" />
<link link_style="Normal" source_guid="5d174df5-165b-497b-b49e-271ebe1f2786" />
</tool_bar_item>
<tool_bar_item guid="566bcf73-7b47-49f5-9251-fffdce47377a" source_guid="058f8e0d-2381-4ee0-bab4-329d142a3062" modified="True" display_style="BitmapAndText" display_style_from_parent="False">
<left_macro source_guid="9b339311-7ff9-4242-b49a-0992fc322f46" />
<link link_style="Normal" source_guid="e05357bc-15fd-4a35-8ac9-8ca66130d209" />
</tool_bar_item>
<tool_bar_item guid="64bb2452-3cc5-40f3-a1b0-aedcc4171132" source_guid="7c86745a-853f-4bb7-81dd-8987d06cc643" modified="True" display_style="BitmapAndText" display_style_from_parent="False">
<left_macro source_guid="904d2302-ac30-4094-a236-45b213c026f7" />
<link link_style="Normal" source_guid="2c9fc393-e1c4-4129-a57c-350b88505cd2" />
</tool_bar_item>
<tool_bar_item source_guid="593ef621-4288-4c5b-af19-f6cd726b446e" />
<tool_bar_item source_guid="b7d32fef-c2b5-4e3c-be1a-1ba978a71713" />
<tool_bar_item source_guid="8d497640-bab5-4e0b-b719-1834ed91adf4" />
<tool_bar_item source_guid="0846d181-dd65-4014-9671-8e456a93de31" />
<tool_bar_item source_guid="1144a093-7833-4beb-98ea-534920507d00" />
<tool_bar_item source_guid="c8c687bd-77e8-4eef-8f6b-636b7be082bb" />
<tool_bar_item source_guid="71eac7d7-2ec3-4d82-9ae3-72ec4687a661" />
<tool_bar_item source_guid="d05e7fa5-a403-4864-afd5-7c3e72382c43" />
<tool_bar_item source_guid="d2400f54-925c-4c3f-ae31-07dfeef7c65f" />
<tool_bar_item guid="226d1ea0-6237-4ce0-abe6-7964eeecbf6a" source_guid="790ba6f5-a79c-4d7c-b42b-2cabb7bbd82c" modified="True" control_source="Copy">
<left_macro source_guid="be8dcb66-2e68-49c9-8994-13c16119e652" />
<right_macro source_guid="cc3ba388-866f-407e-a0b5-8a57867105b0" />
<link link_style="Normal" source_guid="9887392d-6705-4199-a660-8284f7fe27cf" />
</tool_bar_item>
<tool_bar_item source_guid="f0c6d11f-7838-4902-9ddf-8449a818a1d1" />
<tool_bar_item source_guid="d3c081db-0bf3-4af3-9c43-cff6680c7625" />
<tool_bar_item source_guid="39b9923b-77c8-4ea8-a064-cb172fbc1f6d" />
<tool_bar_item source_guid="4f944d97-a884-499e-8ed7-ad519da2e0f6" />
<tool_bar_item source_guid="338f5655-a0a2-4839-9114-ce51dc176fa4" />
<tool_bar_item source_guid="0f757392-62d1-4f0d-a711-0396692050cc" />
<tool_bar_item source_guid="047b5cdd-f104-4f0b-a2a3-2d899c86554a" />
<tool_bar_item source_guid="cf06780d-3826-4f0b-aa8d-475de07fd932" />
<tool_bar_item source_guid="c9217c97-0202-4f90-a5a9-aefb1c721666" />
<tool_bar_item source_guid="cdef77dc-3471-4144-ad41-e2a46f4cd4b1" />
<tool_bar_item source_guid="9fb09f63-a067-4100-b80a-790e5e4fc742" />
<tool_bar_item source_guid="99c7602e-1d21-4a7f-a369-df5c451abc71" />
<tool_bar_item source_guid="b1c75c53-2f95-4224-bb8a-28cbc9e0c335" />
<tool_bar_item source_guid="18080b6f-1a5c-49ec-89ed-14d9babc1463" />
<tool_bar_item source_guid="160f1831-d7eb-454b-8b78-17b72bfaaf0e" />
<tool_bar_item source_guid="a4292914-3f6e-4158-b4e5-30d167370945" />
<tool_bar_item source_guid="d633222a-fb3a-4a7e-931e-065b8fd93b28" />
<tool_bar_item source_guid="3502801c-8c08-4b40-a614-d07ceb6527d1" />
<tool_bar_item source_guid="38a55115-9bb3-4b4a-83fc-35b457d90d0d" />
<tool_bar_item source_guid="2d99e234-a524-4d69-9650-20df849520eb" />
<tool_bar_item source_guid="21a38747-faf7-47f6-a893-e48de8f1cb57" />
<tool_bar_item source_guid="cec8035e-929d-45e1-9601-83e5edf64cf9" />
<tool_bar_item guid="255ffb63-5e06-40e1-bf37-4c55da4ec2f3" source_guid="31114066-0b74-4a4c-8d62-ea8b3659dc29" modified="True" display_style="BitmapAndText" display_style_from_parent="False">
<left_macro source_guid="a3510d12-9b16-4fc2-a643-7b3df43f723b" />
<right_macro source_guid="29dd9afe-f9f5-447a-bb11-88e6a5357be8" />
</tool_bar_item>
</tool_bar_items>
</tool_bar>
<tool_bar source_guid="2337f242-b576-41a4-aace-4a74772bc72e" modified="True" LastTornOffSize="92,116">
<tool_bar_items>
<tool_bar_item source_guid="a89658dc-863c-4a5e-918e-1a0777a72fc8" />
<tool_bar_item source_guid="769164aa-ff16-4b5e-bc30-271e104fc522" />
</tool_bar_items>
</tool_bar>
<tool_bar source_guid="00211289-60a5-460b-88dc-cc667b234dbd" modified="True">
<tool_bar_items>
<tool_bar_item source_guid="b7d32fef-c2b5-4e3c-be1a-1ba978a71713" />
<tool_bar_item source_guid="b7d32fef-c2b5-4e3c-be1a-1ba978a71713" />
<tool_bar_item source_guid="f6d7af12-0c34-4d5b-b325-7132167a85c8" />
<tool_bar_item source_guid="790ba6f5-a79c-4d7c-b42b-2cabb7bbd82c" />
<tool_bar_item source_guid="71eac7d7-2ec3-4d82-9ae3-72ec4687a661" />
<tool_bar_item source_guid="08f422b0-883d-407e-9b32-0c7c2abdc566" />
<tool_bar_item source_guid="338f5655-a0a2-4839-9114-ce51dc176fa4" />
<tool_bar_item source_guid="7c86745a-853f-4bb7-81dd-8987d06cc643" />
<tool_bar_item source_guid="31114066-0b74-4a4c-8d62-ea8b3659dc29" />
<tool_bar_item source_guid="b4d68e9f-841b-4954-974d-d4b1f56c50b5" />
</tool_bar_items>
</tool_bar>
</tool_bars>
<icons>
<icon guid="5be65a35-9b58-453a-b24d-e77f53543852" modified="True">
<light_svg>&lt;svg height="48" viewBox="0 0 48 48" width="48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"&gt;&lt;clipPath id="a"&gt;&lt;path clip-rule="evenodd" d="m0 0h36v36h-36z"/&gt;&lt;/clipPath&gt;&lt;g clip-path="url(#a)" fill-opacity=".996078" transform="matrix(1.3333333 0 0 -1.3333333 0 48)"&gt;&lt;path d="m14.297 18.846c0 1.657 1.345 3.002 3.002 3.002s3.002-1.345 3.002-3.002-1.345-3.002-3.002-3.002-3.002 1.345-3.002 3.002z" fill="#fff" fill-rule="evenodd"/&gt;&lt;path d="m13.552 18.846c0 2.068 1.678 3.746 3.747 3.746 2.068 0 3.746-1.678 3.746-3.746 0-2.069-1.678-3.747-3.746-3.747-2.069 0-3.747 1.678-3.747 3.747zm6.004 0c0 1.245-1.012 2.257-2.257 2.257-1.246 0-2.258-1.012-2.258-2.257 0-1.246 1.012-2.258 2.258-2.258 1.245 0 2.257 1.012 2.257 2.258z"/&gt;&lt;/g&gt;&lt;/svg&gt;</light_svg>
<dark_svg>&lt;svg height="48" viewBox="0 0 48 48" width="48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"&gt;
&lt;g clip-path="url(#a)" fill-opacity=".996078" transform="matrix(1.3333333 0 0 -1.3333333 0 48)"&gt;
&lt;path d="m14.297 18.846c0 1.657 1.345 3.002 3.002 3.002s3.002-1.345 3.002-3.002-1.345-3.002-3.002-3.002-3.002 1.345-3.002 3.002z" fill="#fff" fill-rule="evenodd" stroke-width="3.5px" x_rma_id="1" stroke="#666" stroke-linejoin="round" /&gt;
&lt;path d="m13.552 18.846c0 2.068 1.678 3.746 3.747 3.746 2.068 0 3.746-1.678 3.746-3.746 0-2.069-1.678-3.747-3.746-3.747-2.069 0-3.747 1.678-3.747 3.747zm6.004 0c0 1.245-1.012 2.257-2.257 2.257-1.246 0-2.258-1.012-2.258-2.257 0-1.246 1.012-2.258 2.258-2.258 1.245 0 2.257 1.012 2.257 2.258z" fill="" stroke-width="3.5px" x_rma_id="2" stroke="#666" stroke-linejoin="round" /&gt;
&lt;/g&gt;
&lt;clipPath id="a"&gt;
&lt;path clip-rule="evenodd" d="m0 0h36v36h-36z" fill="" stroke-width="" x_rma_id="0" /&gt;
&lt;/clipPath&gt;
&lt;g clip-path="url(#a)" fill-opacity=".996078" transform="matrix(1.3333333 0 0 -1.3333333 0 48)"&gt;
&lt;path d="m14.297 18.846c0 1.657 1.345 3.002 3.002 3.002s3.002-1.345 3.002-3.002-1.345-3.002-3.002-3.002-3.002 1.345-3.002 3.002z" fill="#fff" fill-rule="evenodd" stroke-width="" x_rma_id="1" /&gt;
&lt;path d="m13.552 18.846c0 2.068 1.678 3.746 3.747 3.746 2.068 0 3.746-1.678 3.746-3.746 0-2.069-1.678-3.747-3.746-3.747-2.069 0-3.747 1.678-3.747 3.747zm6.004 0c0 1.245-1.012 2.257-2.257 2.257-1.246 0-2.258-1.012-2.258-2.257 0-1.246 1.012-2.258 2.258-2.258 1.245 0 2.257 1.012 2.257 2.258z" fill="" stroke-width="" x_rma_id="2" /&gt;
&lt;/g&gt;
&lt;/svg&gt;</dark_svg>
</icon>
</icons>
<macros>
<macro guid="02a1a1f5-4edb-459c-8bec-70fa3afbf7c0" source_guid="be8dcb66-2e68-49c9-8994-13c16119e652" modified="True" bitmap_guid="5be65a35-9b58-453a-b24d-e77f53543852">
<text>
<locale_1033>Single point 01</locale_1033>
</text>
<button_text>Single point</button_text>
<menu_text>Single point</menu_text>
<help_text>Single point</help_text>
<tooltip>Line</tooltip>
<script>! _Point</script>
</macro>
</macros>
</RhinoUI>
</modifications>
</rui_file>
</referenced_rui_files>
</RhinoUI>
+98
View File
@@ -0,0 +1,98 @@
# RhinoPanel — Installation Rhino 8 (Mac)
## Voraussetzungen
- Rhino 8
- Node.js (nur fuer Dev-Modus)
---
## Dev-Modus
```bash
cd rhino-panel
npm run dev # startet auf http://localhost:5173
```
In Rhino: `_RunPythonScript``rhino/rhinopanel.py`
## Prod-Modus
```bash
npm run build # erstellt dist/
```
In Rhino: `_RunPythonScript``rhinopanel.py`
Das Script erkennt `dist/index.html` automatisch.
## Autostart
Rhino Optionen → Scripting → Startup Scripts → `rhinopanel.py` hinzufuegen
---
## Grasshopper-Anbindung
### Wie es funktioniert
Wenn "Anwenden" im Panel gedrueckt wird:
1. Rhino-Layer werden erstellt/aktualisiert
2. Ebenen-JSON wird in `doc.Strings["rhinopanel_ebenen"]` gespeichert
3. Grasshopper-Neuberechnung wird automatisch ausgeloest
### GH Python-Component einrichten
1. GH oeffnen → `Params > Util > Python Script` Komponente platzieren
2. Inhalt von `rhino/gh_ebenen.py` hineinkopieren
3. Outputs der Komponente (Rechtsklick → "Manage Outputs") anlegen:
| Output | Typ | Inhalt |
|--------|-----|--------|
| `namen` | list | Grundriss-Namen (EG, 1OG, ...) |
| `okff` | list | Bodenniveau je Geschoss in m |
| `hoehen` | list | Geschosshoehe in m |
| `schnitthoehen` | list | Schnitthoehe ueber Boden in m |
| `grundriss_ebenen` | list | Planes auf Schnitthoehe (fuer Section) |
| `boden_ebenen` | list | Planes auf OKFF (fuer Extrusion) |
| `gebaeude_hoehe` | item | Gesamthoehe in m |
| `schnitte` | list | Namen der Schnitt-Ebenen |
| `ansichten` | list | Namen der Ansichts-Ebenen |
### Typische GH-Verknuepfungen
**Automatischer Grundrissschnitt:**
```
gh_ebenen.grundriss_ebenen → Brep Split / Section
```
**Wandextrusion (2D → 3D):**
```
Curves auf Layer 01_WAND
→ Extrude (Vektor: {0,0,hoehe})
→ Cap Holes
→ BooleanUnion
```
**Stockwerke positionieren:**
```
gh_ebenen.boden_ebenen → Orient
```
### Alternativ: Get Document String (ohne Python)
- `Params > Util > Get Document String`
- Key: `rhinopanel_ebenen`
- Output → `Deserialize JSON` oder `Evaluate Expression`
---
## Dateistruktur
```
rhino-panel/
├── src/ React-App (Quellcode)
├── dist/ Gebaute App (nach npm run build)
└── rhino/
├── rhinopanel.py Panel starten (in Rhino ausfuehren)
├── layer_builder.py Layer-Erstellung
└── gh_ebenen.py GH Python-Component Code
```
+708
View File
@@ -0,0 +1,708 @@
# ! python3
# -*- coding: utf-8 -*-
"""
ausschnitte.py
AUSSCHNITTE-Panel: speichert Viewport-Ausschnitte mit Kamera, Display-Mode,
Layer-Sichtbarkeit, DOSSIER-State. Anwendbar im Model-Space und auf Layout-Details.
"""
import os
import sys
import math
import json
import uuid
import Rhino
import System
import scriptcontext as sc
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
import panel_base
PANEL_GUID_STR = "5c9e4f3f-6d0e-4f1f-b3c4-d5e6f7081a2b"
_STORE_KEY = "dossier_ausschnitte"
_FOLDERS_KEY = "dossier_ausschnitt_folders"
_PRESETS_KEY = "dossier_layer_presets"
def _orientation_from_camera(loc, tgt, parallel=True):
"""Bestimmt Orientierung:
- perspective = perspektivische Projektion (kein orthogonaler Schnitt)
- horizontal = parallele Projektion mit Blick weitgehend nach unten/oben (Grundriss)
- vertical = parallele Projektion mit Blick weitgehend seitlich (Schnitt/Ansicht)
"""
if not parallel:
return "perspective"
try:
dx = tgt[0] - loc[0]
dy = tgt[1] - loc[1]
dz = tgt[2] - loc[2]
m = math.sqrt(dx * dx + dy * dy + dz * dz)
if m <= 0: return "vertical"
return "horizontal" if (abs(dz) / m) > 0.7 else "vertical"
except Exception:
return "vertical"
def _parse_scale(scale_str):
"""Parse '1:50' / '1=50' / '50' → (page, model). Gibt None zurueck wenn nicht parsebar."""
if not scale_str:
return None
s = scale_str.strip()
for sep in (":", "=", "/"):
if sep in s:
try:
a, b = s.split(sep, 1)
pa = float(a.strip())
pb = float(b.strip())
if pa > 0 and pb > 0:
return (pa, pb)
except Exception:
pass
break
# nur Zahl: 1:N angenommen
try:
n = float(s)
if n > 0:
return (1.0, n)
except Exception:
pass
return None
# --- Capture / Apply Helpers ------------------------------------------------
def _capture_camera(vp):
dm = vp.DisplayMode
dm_id = str(dm.Id) if dm else None
dm_nm = dm.LocalName if dm else None
loc = [vp.CameraLocation.X, vp.CameraLocation.Y, vp.CameraLocation.Z]
tgt = [vp.CameraTarget.X, vp.CameraTarget.Y, vp.CameraTarget.Z]
# Frustum-Breite mitsichern: bei Parallelprojektion bestimmt sie den Zoom.
# Ohne sie laesst sich der gespeicherte Ausschnitt nicht rekonstruieren.
frustum_w = None
frustum_h = None
try:
ok, l, r, b, t, n, f = vp.GetFrustum()
if ok:
frustum_w = float(r - l)
frustum_h = float(t - b)
except Exception:
pass
return {
"viewName": vp.Name,
"location": loc,
"target": tgt,
"up": [vp.CameraUp.X, vp.CameraUp.Y, vp.CameraUp.Z],
"lens": float(vp.Camera35mmLensLength) if vp.Camera35mmLensLength else 50.0,
"parallel": bool(vp.IsParallelProjection),
"displayMode": dm_id,
"displayModeName": dm_nm,
"orientation": _orientation_from_camera(loc, tgt, bool(vp.IsParallelProjection)),
"frustumWidth": frustum_w,
"frustumHeight": frustum_h,
}
def _apply_camera(vp, cam):
if not cam: return
try:
loc = Rhino.Geometry.Point3d(*cam["location"])
tgt = Rhino.Geometry.Point3d(*cam["target"])
up = Rhino.Geometry.Vector3d(*cam["up"]) if cam.get("up") else None
if cam.get("parallel"):
vp.ChangeToParallelProjection(True)
else:
vp.ChangeToPerspectiveProjection(True, float(cam.get("lens", 50)))
vp.SetCameraLocations(tgt, loc)
if up:
try: vp.CameraUp = up
except Exception: pass
dm_id = cam.get("displayMode")
if dm_id:
try:
mode = Rhino.Display.DisplayModeDescription.GetDisplayMode(System.Guid(dm_id))
if mode is not None: vp.DisplayMode = mode
except Exception:
pass
# Zoom (Frustum-Breite) rekonstruieren — nur bei Parallelprojektion sinnvoll.
# Bei Perspective bestimmt die Lens-Length den Bildausschnitt, dort
# waere ein Magnify kontraproduktiv.
fw = cam.get("frustumWidth")
if fw and fw > 0 and cam.get("parallel"):
try:
ok, l, r, b, t, n, f = vp.GetFrustum()
if ok:
cur_w = float(r - l)
if cur_w > 0:
factor = cur_w / float(fw)
if 1e-9 < factor < 1e9:
try:
vp.Magnify(float(factor), False)
except Exception:
try:
vp.Magnify(float(factor))
except Exception:
Rhino.RhinoApp.RunScript(
"_-Zoom _Factor {:.6f} _Enter".format(factor), False)
except Exception as ex:
print("[AUSSCHNITTE] Frustum-Apply:", ex)
except Exception as ex:
print("[AUSSCHNITTE] Camera-Apply:", ex)
def _capture_layers(doc):
out = []
for layer in doc.Layers:
if layer.IsDeleted: continue
out.append({
"id": str(layer.Id),
"visible": bool(layer.IsVisible),
"locked": bool(layer.IsLocked),
})
return out
def _apply_layers_global(doc, layers):
by_id = {}
for layer in doc.Layers:
if not layer.IsDeleted:
by_id[str(layer.Id)] = layer
for ls in layers:
layer = by_id.get(ls.get("id"))
if layer is None: continue
if layer.IsVisible != ls.get("visible", True):
layer.IsVisible = ls.get("visible", True)
if layer.IsLocked != ls.get("locked", False):
layer.IsLocked = ls.get("locked", False)
def _apply_layers_per_viewport(doc, layers, vp_id):
"""Setzt Sichtbarkeit pro Viewport (fuer Layout-Details)."""
by_id = {}
for layer in doc.Layers:
if not layer.IsDeleted:
by_id[str(layer.Id)] = layer
for ls in layers:
layer = by_id.get(ls.get("id"))
if layer is None: continue
try:
layer.SetPerViewportVisible(vp_id, ls.get("visible", True))
except Exception:
pass
_DOSSIER_KEYS = (
"dossier_zeichnungsebenen",
"dossier_ebenen",
"dossier_active_id",
"dossier_active_code",
)
def _capture_dossier_state(doc):
out = {}
for k in _DOSSIER_KEYS:
v = doc.Strings.GetValue(k)
if v is not None:
out[k] = v
return out
def _apply_dossier_state(doc, state):
for k, v in (state or {}).items():
if v is not None:
doc.Strings.SetString(k, v)
def _find_selected_detail(doc):
"""Sucht nach einem aktuell selektierten Detail-Viewport-Objekt."""
for obj in doc.Objects.GetSelectedObjects(False, False):
if isinstance(obj, Rhino.DocObjects.DetailViewObject):
return obj
return None
def _load_snapshots(doc):
"""Modul-interne Snapshot-Liste (cross-modul nutzbar)."""
raw = doc.Strings.GetValue(_STORE_KEY)
if not raw: return []
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except Exception:
return []
def apply_snapshot_to_detail(doc, detail, snap_id):
"""Wendet einen Ausschnitt auf ein konkretes Detail-Object an. Wird vom
Layouts-Modul benutzt, um Ausschnitt-Detail-Bindings zu synchronisieren.
Liefert True bei Erfolg."""
snap = next((s for s in _load_snapshots(doc) if s.get("id") == snap_id), None)
if not snap:
print("[AUSSCHNITTE] apply_to_detail: snap nicht gefunden", snap_id)
return False
# Page-View ermitteln (fuer SetActiveDetail/SetPageAsActive)
page_view = None
try:
for view in doc.Views:
if isinstance(view, Rhino.Display.RhinoPageView):
try:
if any(d.Id == detail.Id for d in view.GetDetailViews()):
page_view = view
break
except Exception:
continue
except Exception as ex:
print("[AUSSCHNITTE] page-view-suche:", ex)
# Detail muss aktiv sein, damit Kamera-Aenderungen anschlagen
was_active = False
try: was_active = detail.IsActive
except Exception: pass
if page_view is not None and not was_active:
try: page_view.SetActiveDetail(detail.Id)
except Exception as ex: print("[AUSSCHNITTE] SetActiveDetail:", ex)
# Kamera + Layer + Name
vp = detail.Viewport
_apply_camera(vp, snap.get("camera"))
_apply_layers_per_viewport(doc, snap.get("layers", []), vp.Id)
try:
new_name = snap.get("name")
if new_name and vp.Name != new_name:
vp.Name = new_name
except Exception as ex:
print("[AUSSCHNITTE] Detail-Rename:", ex)
# Massstab
ratio = _parse_scale(snap.get("scale", ""))
if ratio is not None:
page_v, model_v = ratio
for label, setter in (
("DetailGeometry.SetScale", lambda: detail.DetailGeometry.SetScale(model_v, page_v)),
("Detail.SetScale", lambda: detail.SetScale(model_v, page_v)),
):
try:
setter()
break
except Exception:
continue
# Commit + Deaktivieren
try: detail.CommitViewportChanges()
except Exception:
try: detail.CommitChanges()
except Exception: pass
if page_view is not None and not was_active:
try: page_view.SetPageAsActive()
except Exception: pass
try:
(page_view or doc.Views).Redraw()
except Exception:
doc.Views.Redraw()
print("[AUSSCHNITTE] '{}' auf Detail {} angewendet".format(snap.get("name"), detail.Id))
return True
# --- Bridge -----------------------------------------------------------------
class AusschnittBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "ausschnitte")
def _on_ready(self):
self._send_list()
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
p = data.get("payload") or {}
if not isinstance(p, dict): p = {}
if t == "READY": self._on_ready()
elif t == "LIST": self._send_list()
elif t == "SAVE": self._save(p.get("name", "Ausschnitt"))
elif t == "UPDATE": self._update(p.get("id"))
elif t == "RESTORE": self._restore(p.get("id"))
elif t == "APPLY_TO_DETAIL":self._apply_to_detail(p.get("id"))
elif t == "RENAME": self._rename(p.get("id"), p.get("name"))
elif t == "DELETE": self._delete(p.get("id"))
elif t == "SET_FOLDER": self._set_field(p.get("id"), "folder", p.get("folder") or "")
elif t == "SET_SCALE": self._set_field(p.get("id"), "scale", p.get("scale") or "")
elif t == "DUPLICATE": self._duplicate(p.get("id"))
elif t == "ADD_FOLDER": self._add_folder(p.get("name"))
elif t == "REMOVE_FOLDER": self._remove_folder(p.get("name"))
elif t == "GET_LAYERS": self._send_layers(p.get("id"))
elif t == "UPDATE_LAYERS": self._update_layers(p.get("id"), p.get("layers") or [])
elif t == "SAVE_PRESET": self._save_preset(p.get("name"), p.get("layers") or [])
elif t == "DELETE_PRESET": self._delete_preset(p.get("name"))
def _load(self, doc):
raw = doc.Strings.GetValue(_STORE_KEY)
if not raw: return []
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except Exception:
return []
def _store(self, doc, snaps):
doc.Strings.SetString(_STORE_KEY, json.dumps(snaps, ensure_ascii=False))
def _load_folders(self, doc):
raw = doc.Strings.GetValue(_FOLDERS_KEY)
if not raw: return []
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except Exception:
return []
def _store_folders(self, doc, folders):
doc.Strings.SetString(_FOLDERS_KEY, json.dumps(folders, ensure_ascii=False))
def _load_presets(self, doc):
raw = doc.Strings.GetValue(_PRESETS_KEY)
if not raw: return []
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except Exception:
return []
def _store_presets(self, doc, presets):
doc.Strings.SetString(_PRESETS_KEY, json.dumps(presets, ensure_ascii=False))
def _send_list(self):
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
explicit_folders = self._load_folders(doc)
# Aus Snapshots zusaetzliche Ordner ableiten (falls Snap auf nicht-existenten Ordner zeigt)
for s in snaps:
f = s.get("folder", "")
if f and f not in explicit_folders:
explicit_folders.append(f)
slim = []
for s in snaps:
cam = s.get("camera", {}) or {}
orient = cam.get("orientation")
if not orient:
loc = cam.get("location") or [0, 0, 0]
tgt = cam.get("target") or [0, 0, 0]
orient = _orientation_from_camera(loc, tgt, bool(cam.get("parallel", True)))
slim.append({
"id": s.get("id"),
"name": s.get("name"),
"folder": s.get("folder", ""),
"scale": s.get("scale", ""),
"orientation": orient,
"displayModeName": cam.get("displayModeName"),
"parallel": cam.get("parallel", False),
})
preset_summary = [{"name": p.get("name"), "count": len(p.get("layers") or [])}
for p in self._load_presets(doc)]
self.send("LIST", {
"snapshots": slim,
"folders": explicit_folders,
"presets": preset_summary,
})
def _add_folder(self, name):
if not name: return
name = name.strip()
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
folders = self._load_folders(doc)
if name not in folders:
folders.append(name)
self._store_folders(doc, folders)
self._send_list()
def _remove_folder(self, name):
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
# Aus Folder-Liste entfernen
folders = [f for f in self._load_folders(doc) if f != name]
self._store_folders(doc, folders)
# Snapshots aus diesem Ordner herausnehmen (auf root)
snaps = self._load(doc)
for s in snaps:
if s.get("folder") == name:
s["folder"] = ""
self._store(doc, snaps)
self._send_list()
def _duplicate(self, snap_id):
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
src = next((s for s in snaps if s.get("id") == snap_id), None)
if not src: return
# Tiefe Kopie via JSON
copy = json.loads(json.dumps(src, ensure_ascii=False))
copy["id"] = "snap_" + uuid.uuid4().hex[:8]
copy["name"] = (src.get("name", "Ausschnitt") + " Kopie")
# Direkt nach Original einfuegen
idx = snaps.index(src)
snaps.insert(idx + 1, copy)
self._store(doc, snaps)
self._send_list()
print("[AUSSCHNITTE] '{}' dupliziert".format(src.get("name")))
def _set_field(self, snap_id, field, value):
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
for s in snaps:
if s.get("id") == snap_id:
s[field] = value
break
self._store(doc, snaps)
self._send_list()
def _capture(self, doc, name, existing_id=None, prior_scale=""):
view = doc.Views.ActiveView
if view is None:
print("[AUSSCHNITTE] Keine aktive View")
return None
vp = view.ActiveViewport
# Aktuelle Skala vom MASSSTAB-Modul holen — nur sinnvoll bei Parallel-
# projektion. In Perspective bleibt scale leer (Fallback: prior_scale).
scale_str = ""
try:
import massstab
# Bewusst der EINGESTELLTE Wert (User-Intent), nicht der live aus
# dem Viewport berechnete. Letzterer drifted bei Pan/Zoom.
ratio = massstab.get_applied_scale_ratio()
if ratio is not None and ratio > 0:
if ratio >= 10:
scale_str = "1:{:.0f}".format(ratio)
else:
scale_str = "1:{:.1f}".format(ratio)
except Exception as ex:
print("[AUSSCHNITTE] Live-Skala lesen:", ex)
# Fallback: wenn kein Massstab gepinnt war, die aus dem Frustum
# berechnete Live-Skala speichern. So bleibt das Massstab-Dropdown
# nach Restore konsistent (auch wenn der eigentliche Zoom-Restore
# bereits ueber frustumWidth in _apply_camera laeuft).
if not scale_str:
try:
import massstab
live = massstab.get_current_scale_ratio()
if live is not None and live > 0:
scale_str = "1:{:.0f}".format(live) if live >= 10 else "1:{:.1f}".format(live)
except Exception as ex:
print("[AUSSCHNITTE] Live-Skala (Fallback):", ex)
if not scale_str and prior_scale:
scale_str = prior_scale # Perspective -> alten Wert nicht ueberschreiben
return {
"id": existing_id or "snap_" + uuid.uuid4().hex[:8],
"name": name,
"scale": scale_str,
"camera": _capture_camera(vp),
"layers": _capture_layers(doc),
"dossier": _capture_dossier_state(doc),
}
def _save(self, name):
doc = Rhino.RhinoDoc.ActiveDoc
snap = self._capture(doc, name)
if snap is None: return
snaps = self._load(doc)
snaps.append(snap)
self._store(doc, snaps)
self._send_list()
print("[AUSSCHNITTE] '{}' gespeichert".format(name))
def _update(self, snap_id):
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
target = next((s for s in snaps if s.get("id") == snap_id), None)
if not target: return
updated = self._capture(doc, target.get("name", "Ausschnitt"),
existing_id=snap_id,
prior_scale=target.get("scale", ""))
if updated is None: return
for i, s in enumerate(snaps):
if s.get("id") == snap_id:
snaps[i] = updated
break
self._store(doc, snaps)
self._send_list()
print("[AUSSCHNITTE] '{}' aktualisiert".format(target.get("name")))
def _restore(self, snap_id):
doc = Rhino.RhinoDoc.ActiveDoc
snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None)
if not snap: return
view = doc.Views.ActiveView
if view is None: return
vp = view.ActiveViewport
_apply_camera(vp, snap.get("camera"))
_apply_layers_global(doc, snap.get("layers", []))
_apply_dossier_state(doc, snap.get("dossier") or snap.get("pause") or {})
# Viewport ZUERST umbenennen — der per-Viewport-Massstab in massstab.py
# wird unter vp.Name geschluesselt. Erst nach dem Rename schreibt
# _apply_scale unter dem neuen Namen, sonst landet der Wert beim alten
# Ausschnitt und der neue zeigt "1:?".
try:
new_name = snap.get("name")
if new_name and vp.Name != new_name:
vp.Name = new_name
except Exception as ex:
print("[AUSSCHNITTE] Rename:", ex)
# Gespeicherten Massstab anwenden (z.B. "1:50") — falls vorhanden und
# Viewport parallel ist (in Perspective ignoriert massstab._apply_scale).
try:
scale_str = (snap.get("scale") or "").strip()
if scale_str:
ratio = _parse_scale(scale_str)
if ratio:
_, model_v = ratio # (page=1, model=N) -> N
import massstab
massstab._apply_scale(doc, vp, float(model_v))
print("[AUSSCHNITTE] Massstab gesetzt auf 1:{} (applied={})".format(
model_v, massstab.get_applied_scale_ratio()))
# Andere Panels (Massstab, Oberleiste) sofort ueber den
# neuen appliedScale informieren — sonst zeigt das Dropdown
# noch den vorherigen Wert bis zum naechsten Idle-Tick mit
# Aenderung an der Live-Skala.
for key in ("massstab_bridge", "oberleiste_bridge"):
try:
b = sc.sticky.get(key)
print("[AUSSCHNITTE] force-send via {}: {}".format(key, "OK" if b is not None else "MISSING"))
if b is not None:
b._send_state(force=True)
except Exception as e:
print("[AUSSCHNITTE] force-send {} failed: {}".format(key, e))
except Exception as ex:
print("[AUSSCHNITTE] Massstab-Restore:", ex)
view.Redraw()
print("[AUSSCHNITTE] '{}' wiederhergestellt".format(snap.get("name")))
def _apply_to_detail(self, snap_id):
doc = Rhino.RhinoDoc.ActiveDoc
# 1) Detail aus Selektion oder aktiver PageView ermitteln
detail = _find_selected_detail(doc)
if detail is None:
try:
av = doc.Views.ActiveView
if isinstance(av, Rhino.Display.RhinoPageView):
for d in av.GetDetailViews():
if d.IsActive:
detail = d
break
except Exception as ex:
print("[AUSSCHNITTE] Active-Detail-Suche:", ex)
if detail is None:
print("[AUSSCHNITTE] Kein Detail ausgewaehlt — bitte:")
print(" 1) ins Layout wechseln")
print(" 2) Detail-Rahmen einmal anklicken (so dass er hervorgehoben ist)")
print(" 3) erneut 'Auf Detail anwenden' waehlen")
return
# 2) Delegieren an den oeffentlichen Helper
apply_snapshot_to_detail(doc, detail, snap_id)
def _send_layers(self, snap_id):
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None)
if not snap:
print("[AUSSCHNITTE] Snap nicht gefunden:", snap_id)
return
snap_by_id = {}
for ls in (snap.get("layers") or []):
snap_by_id[ls.get("id")] = ls
layers = []
for layer in doc.Layers:
if layer.IsDeleted: continue
lid = str(layer.Id)
ls = snap_by_id.get(lid, {})
layers.append({
"id": lid,
"name": layer.Name,
"fullPath": layer.FullPath,
"color": "#%02x%02x%02x" % (layer.Color.R, layer.Color.G, layer.Color.B),
"visible": bool(ls.get("visible", layer.IsVisible)),
"locked": bool(ls.get("locked", layer.IsLocked)),
})
layers.sort(key=lambda x: x["fullPath"])
presets = self._load_presets(doc)
self.send("LAYERS_DATA", {
"id": snap_id,
"name": snap.get("name"),
"layers": layers,
"presets": presets,
})
def _update_layers(self, snap_id, layers):
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
target = next((s for s in snaps if s.get("id") == snap_id), None)
if not target: return
new_list = []
for ls in layers:
lid = ls.get("id")
if not lid: continue
new_list.append({
"id": lid,
"visible": bool(ls.get("visible", True)),
"locked": bool(ls.get("locked", False)),
})
target["layers"] = new_list
self._store(doc, snaps)
self._send_list()
print("[AUSSCHNITTE] Ebenen-Sichtbarkeit von '{}' aktualisiert".format(target.get("name")))
def _save_preset(self, name, layers):
if not name: return
name = name.strip()
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
presets = self._load_presets(doc)
clean = []
for ls in layers:
lid = ls.get("id")
if not lid: continue
clean.append({
"id": lid,
"visible": bool(ls.get("visible", True)),
"locked": bool(ls.get("locked", False)),
})
existing = next((p for p in presets if p.get("name") == name), None)
if existing is not None:
existing["layers"] = clean
else:
presets.append({"name": name, "layers": clean})
self._store_presets(doc, presets)
self._send_list()
print("[AUSSCHNITTE] Ebenenkombination '{}' gespeichert ({} Ebenen)".format(name, len(clean)))
def _delete_preset(self, name):
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
presets = [p for p in self._load_presets(doc) if p.get("name") != name]
self._store_presets(doc, presets)
self._send_list()
def _rename(self, snap_id, name):
if not snap_id or not name: return
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
for s in snaps:
if s.get("id") == snap_id:
s["name"] = name
break
self._store(doc, snaps)
self._send_list()
def _delete(self, snap_id):
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snaps = [s for s in self._load(doc) if s.get("id") != snap_id]
self._store(doc, snaps)
self._send_list()
panel_base.register_and_open("ausschnitte", "AUSSCHNITTE", PANEL_GUID_STR, AusschnittBridge,
icon_spec=("A", "#c87050"))
+48
View File
@@ -0,0 +1,48 @@
# ! python3
"""
clean.py
Loescht ALLE sticky-Eintraege der DOSSIER-Panels, damit der naechste
startup.py-Lauf die Bridges komplett neu erzeugt.
WICHTIG: Zusaetzlich muessen die drei Panels in Rhinos Layout
einmal geschlossen werden (Rechtsklick auf Tab -> Schliessen),
damit Rhino den alten Panel-Inhalt nicht weiter verwendet.
"""
import scriptcontext as sc
KEYS = [
# Alte Keys (rueckwaerts-kompatibel)
"rhinopanel_registered", "rhinopanel_form", "rhinopanel_guid",
# panel_base-System
"panel_registered_ebenen", "panel_guid_ebenen",
"panel_registered_gestaltung", "panel_guid_gestaltung",
"panel_registered_ausschnitte","panel_guid_ausschnitte",
# EBENEN
"ebenen_layer_listener",
"ebenen_bridge_ref",
"ebenen_processing_layer",
"ebenen_processing",
# GESTALTUNG
"gestaltung_selection_listener",
"gestaltung_bridge",
]
removed = 0
for k in KEYS:
if k in sc.sticky:
del sc.sticky[k]
removed += 1
# Sicherheitshalber auch alle anderen panel_*/ebenen_*/gestaltung_*-Keys raus
for k in list(sc.sticky.keys()):
if isinstance(k, str) and (
k.startswith("panel_") or k.startswith("ebenen_") or
k.startswith("gestaltung_") or k.startswith("ausschnitt")
):
del sc.sticky[k]
removed += 1
print("[clean] {} Sticky-Keys entfernt.".format(removed))
print("[clean] Schliesse jetzt die EBENEN / GESTALTUNG / AUSSCHNITTE")
print("[clean] Panels im Rhino-Layout (Rechtsklick Tab -> Schliessen),")
print("[clean] dann startup.py neu ausfuehren.")
+51
View File
@@ -0,0 +1,51 @@
# ! python3
"""
clean_layers.py
Loescht Rhino-Standardlayer (Default, Layer 01-05 usw.)
die keine Objekte enthalten und nicht zum RhinoPanel gehoeren.
Ausfuehren via _RunPythonScript.
"""
import re
import Rhino
import scriptcontext as sc
sc.sticky["rhinopanel_registered"] = False
sc.sticky["rhinopanel_form"] = None
doc = Rhino.RhinoDoc.ActiveDoc
PROTECTED = {
"10_grundrisse", "20_schnitte", "30_ansichten",
"00_raster", "01_vermessung", "40_situation",
"90_referenzen", "99_konstruktion",
}
pattern = re.compile(r'^(default|layer\s*0*\d+)$', re.IGNORECASE)
gone = []
skip = []
for i in range(doc.Layers.Count - 1, -1, -1):
layer = doc.Layers[i]
root = layer.FullPath.split("::")[0].strip().lower()
if root in PROTECTED:
continue
if not pattern.match(layer.Name.strip()):
continue
try:
if doc.Layers.Delete(i, True):
gone.append(layer.Name)
else:
skip.append(layer.Name)
except Exception:
skip.append(layer.Name)
doc.Views.Redraw()
if gone:
print("[clean_layers] Geloescht: {}".format(", ".join(gone)))
else:
print("[clean_layers] Nichts geloescht (schon sauber?)")
if skip:
print("[clean_layers] Uebersprungen (Objekte drauf): {}".format(", ".join(skip)))
print("[clean_layers] Panel-Sticky zurueckgesetzt")
+612
View File
@@ -0,0 +1,612 @@
# ! python3
# -*- coding: utf-8 -*-
"""
dimensionen.py
DIMENSIONEN-Panel: Object Info Palette nach Vectorworks-Vorbild.
Zeigt Position + Abmessungen der Selektion an und erlaubt direktes Eintippen
mit 9-Punkt-Referenz, World/CPlane-Modus und Shape-spezifischen Feldern
(Kreis, Linie, Rechteck).
"""
import os
import sys
import math
import Rhino
import Rhino.Geometry as rg
import System
import scriptcontext as sc
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
import panel_base
PANEL_GUID_STR = "9e3c8c5d-6d4a-4f3e-b3c5-d4e5f6071a2c"
# Idle-Polling fuer geometrische Aenderungen (Gumball-Move feuert keine
# SelectObjects-Events). Tick alle N Idle-Calls — N hoeher = weniger CPU.
_IDLE_GEOM_POLL = 8
# --- Geometrie-Helpers ------------------------------------------------------
def _get_selected_objects(doc):
"""Liste aller aktuell selektierten RhinoObjects."""
if doc is None: return []
try:
return list(doc.Objects.GetSelectedObjects(False, False))
except Exception:
return []
def _get_cplane(doc):
"""Aktive Construction Plane oder Plane.WorldXY als Fallback."""
try:
v = doc.Views.ActiveView
if v is not None:
cp = v.ActiveViewport.ConstructionPlane()
if cp is not None and cp.Plane.IsValid:
return cp.Plane
except Exception:
pass
return rg.Plane.WorldXY
def _active_plane(doc, mode):
"""Plane fuer die aktuelle Koordinatenangabe — World oder CPlane."""
if mode == "cplane":
return _get_cplane(doc)
return rg.Plane.WorldXY
def _bbox_in_plane(objs, plane):
"""BBox aller selektierten Objekte im Koordinatensystem der gegebenen
Plane (achsen-aligned zur Plane). Liefert (BoundingBox, plane) oder None."""
if not objs:
return None
# World -> Plane Transform anwenden -> BBox in plane-Koordinaten
xform = rg.Transform.PlaneToPlane(plane, rg.Plane.WorldXY)
bbox = rg.BoundingBox.Empty
for obj in objs:
try:
geom = obj.Geometry
if geom is None: continue
bb = geom.GetBoundingBox(xform)
if bb.IsValid:
bbox.Union(bb)
except Exception:
pass
return bbox if bbox.IsValid else None
def _ref_point_local(bbox, ref):
"""Referenzpunkt in plane-lokalen Koordinaten anhand ref-Dict
{x: 'min'|'mid'|'max', y: ..., z: ...}."""
def axis(amin, amax, code):
if code == "min": return amin
if code == "max": return amax
return (amin + amax) * 0.5
return rg.Point3d(
axis(bbox.Min.X, bbox.Max.X, ref.get("x", "min")),
axis(bbox.Min.Y, bbox.Max.Y, ref.get("y", "min")),
axis(bbox.Min.Z, bbox.Max.Z, ref.get("z", "mid")),
)
def _ref_point_world(bbox_local, ref, plane):
"""Referenzpunkt in Welt-Koordinaten: lokal -> plane.PointAt."""
p_local = _ref_point_local(bbox_local, ref)
return plane.PointAt(p_local.X, p_local.Y, p_local.Z)
def _round(v, digits=4):
try:
return round(float(v), digits)
except Exception:
return v
# --- Shape-Detection --------------------------------------------------------
def _detect_shape(objs):
"""Erkennt spezifische Formen: Kreis, Linie, Rechteck (geschlossene
planare Polyline mit 4 perpendikularen Segmenten). Liefert dict oder None.
Nur bei genau einem selektierten Curve-Objekt."""
if len(objs) != 1:
return None
obj = objs[0]
geom = obj.Geometry
if not isinstance(geom, rg.Curve):
return None
# Kreis?
try:
ok, circle = geom.TryGetCircle(0.001)
if ok and circle.IsValid:
return {
"type": "circle",
"radius": _round(circle.Radius, 4),
"center": [_round(circle.Center.X), _round(circle.Center.Y), _round(circle.Center.Z)],
}
except Exception:
pass
# Linie?
try:
if isinstance(geom, rg.LineCurve):
line = geom.Line
length = line.Length
angle_deg = math.degrees(math.atan2(line.Direction.Y, line.Direction.X))
return {
"type": "line",
"length": _round(length, 4),
"angle": _round(angle_deg, 3),
"start": [_round(line.From.X), _round(line.From.Y), _round(line.From.Z)],
"end": [_round(line.To.X), _round(line.To.Y), _round(line.To.Z)],
}
except Exception:
pass
# Rechteck als geschlossene Polyline mit 4 perpendikularen Segmenten?
try:
ok, poly = geom.TryGetPolyline()
if ok and poly is not None and poly.Count == 5 and poly[0].DistanceTo(poly[-1]) < 1e-6:
pts = [poly[i] for i in range(4)]
v0 = pts[1] - pts[0]
v1 = pts[2] - pts[1]
v2 = pts[3] - pts[2]
v3 = pts[0] - pts[3]
def _dot(a, b): return a.X * b.X + a.Y * b.Y + a.Z * b.Z
# Adjacente Kanten perpendikular?
if (abs(_dot(v0, v1)) < 1e-4 and
abs(_dot(v1, v2)) < 1e-4 and
abs(_dot(v2, v3)) < 1e-4):
w = v0.Length
h = v1.Length
return {
"type": "rectangle",
"width": _round(w, 4),
"height": _round(h, 4),
}
except Exception:
pass
return None
# --- Transform-Operationen --------------------------------------------------
def _apply_xform(doc, objs, xform):
"""Transform auf alle Objekte anwenden (in-place via ID)."""
if not xform.IsValid: return 0
n = 0
for obj in objs:
try:
if doc.Objects.Transform(obj.Id, xform, True):
n += 1
except Exception as ex:
print("[DIMENSIONEN] Transform-Fehler:", ex)
return n
# --- Undo-Wrapper -----------------------------------------------------------
# Ohne BeginUndoRecord/EndUndoRecord wird ein Multi-Objekt-Transform nicht
# zuverlaessig als ein einziger Undo-Schritt registriert — Ctrl+Z ueberspringt
# dann unsere Aenderung. Wir packen jede User-Aktion in einen benannten Record.
class _UndoRecord(object):
def __init__(self, doc, label):
self.doc = doc
self.label = label
self.serial = 0
def __enter__(self):
try:
self.serial = self.doc.BeginUndoRecord(self.label)
except Exception as ex:
print("[DIMENSIONEN] BeginUndoRecord:", ex)
self.serial = 0
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.serial:
try: self.doc.EndUndoRecord(self.serial)
except Exception as ex:
print("[DIMENSIONEN] EndUndoRecord:", ex)
return False # exceptions propagieren
def _translate_in_plane(doc, objs, plane, dx, dy, dz):
"""Verschiebt um (dx, dy, dz) in plane-lokalen Achsen."""
if dx == 0 and dy == 0 and dz == 0: return
delta_world = plane.XAxis * dx + plane.YAxis * dy + plane.ZAxis * dz
xform = rg.Transform.Translation(delta_world)
with _UndoRecord(doc, "Dossier: Position aendern"):
_apply_xform(doc, objs, xform)
doc.Views.Redraw()
def _scale_around_point(doc, objs, plane, ref_world, sx, sy, sz):
"""Skalierung mit eigenen Faktoren pro Achse, zentriert am Referenzpunkt,
ausgerichtet an plane."""
if sx == 1 and sy == 1 and sz == 1: return
if sx <= 0 or sy <= 0 or sz <= 0:
print("[DIMENSIONEN] Ungueltige Skalierungsfaktoren:", sx, sy, sz)
return
p = rg.Plane(plane)
p.Origin = ref_world
xform = rg.Transform.Scale(p, sx, sy, sz)
with _UndoRecord(doc, "Dossier: Abmessung aendern"):
_apply_xform(doc, objs, xform)
doc.Views.Redraw()
def _rotate_around_axis(doc, objs, ref_world, axis_dir, angle_deg):
"""Rotation um axis_dir durch ref_world."""
if angle_deg == 0: return
xform = rg.Transform.Rotation(math.radians(angle_deg), axis_dir, ref_world)
with _UndoRecord(doc, "Dossier: Drehen"):
_apply_xform(doc, objs, xform)
doc.Views.Redraw()
# --- Shape-Edit-Operationen -------------------------------------------------
def _set_circle_radius(doc, obj, new_radius, plane, ref_world):
"""Skaliert ein Kreis-Curve so, dass es genau new_radius hat — Referenz
bleibt fix. Wird ueber globale Scale realisiert, damit das Objekt
konsistent mit dem Rest selektierter Objekte transformiert wird."""
geom = obj.Geometry
ok, circle = geom.TryGetCircle(0.001)
if not ok or circle.Radius <= 0:
return False
factor = float(new_radius) / circle.Radius
if factor <= 0: return False
p = rg.Plane(plane)
p.Origin = ref_world
xform = rg.Transform.Scale(p, factor, factor, factor)
return bool(doc.Objects.Transform(obj.Id, xform, True))
def _set_line_length(doc, obj, new_length, ref_world):
"""Linie so verlaengern/verkuerzen, dass sie new_length hat. Skaliert
Linie entlang ihrer Richtung um den Referenzpunkt."""
geom = obj.Geometry
if not isinstance(geom, rg.LineCurve): return False
line = geom.Line
cur = line.Length
if cur <= 0: return False
factor = float(new_length) / cur
if factor <= 0: return False
# Linie skaliert sich nur entlang ihrer Direction. Scale-1D ueber eine
# Plane mit der Linien-Direction als X-Achse waere ideal — vereinfacht:
# uniformer Scale, falls Linie achsen-parallel zur lokalen X-Plane
# ist das aequivalent zu Length-Scaling.
xaxis = rg.Vector3d(line.Direction)
xaxis.Unitize()
yaxis = rg.Vector3d.CrossProduct(rg.Vector3d.ZAxis, xaxis)
if yaxis.Length < 1e-6:
yaxis = rg.Vector3d.YAxis
yaxis.Unitize()
plane = rg.Plane(ref_world, xaxis, yaxis)
xform = rg.Transform.Scale(plane, factor, 1.0, 1.0)
return bool(doc.Objects.Transform(obj.Id, xform, True))
def _set_rectangle_dims(doc, obj, new_w, new_h, plane, ref_world):
"""Skaliert Rechteck-Curve auf (new_w, new_h). Annahme: width = Laenge
erster Seite (v0), height = zweiter Seite (v1) in der Polyline-
Reihenfolge — entspricht der Reihenfolge aus _detect_shape."""
geom = obj.Geometry
if not isinstance(geom, rg.Curve): return False
ok, poly = geom.TryGetPolyline()
if not ok or poly is None or poly.Count != 5: return False
pts = [poly[i] for i in range(4)]
v0 = pts[1] - pts[0]
v1 = pts[2] - pts[1]
w_cur = v0.Length
h_cur = v1.Length
if w_cur <= 0 or h_cur <= 0: return False
sw = float(new_w) / w_cur
sh = float(new_h) / h_cur
if sw <= 0 or sh <= 0: return False
# Achsen des Rechtecks als Plane fuer den Scale
xaxis = rg.Vector3d(v0); xaxis.Unitize()
yaxis = rg.Vector3d(v1); yaxis.Unitize()
rect_plane = rg.Plane(ref_world, xaxis, yaxis)
xform = rg.Transform.Scale(rect_plane, sw, sh, 1.0)
return bool(doc.Objects.Transform(obj.Id, xform, True))
# --- Bridge -----------------------------------------------------------------
class DimensionenBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "dimensionen")
self._ref = {"x": "min", "y": "min", "z": "mid"}
self._coord_sys = "world" # "world" | "cplane"
self._last_sig = None
self._last_ids = ()
self._idle_cnt = 0
def _on_ready(self):
self._send_state(force=True)
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
p = data.get("payload") or {}
if not isinstance(p, dict): p = {}
if t == "READY": self._on_ready()
elif t == "REQUEST_STATE": self._send_state(force=True)
elif t == "SET_REF_POINT": self._set_ref_point(p)
elif t == "SET_COORD_SYSTEM": self._set_coord_system(p)
elif t == "SET_POSITION": self._set_position(p)
elif t == "SET_DIMENSION": self._set_dimension(p)
elif t == "SET_ROTATION_Z": self._set_rotation_z(p)
elif t == "SET_CIRCLE_RADIUS":self._set_circle_radius(p)
elif t == "SET_LINE_LENGTH": self._set_line_length(p)
elif t == "SET_RECTANGLE": self._set_rectangle(p)
# --- State-Snapshot -----------------------------------------------------
def _compute_state(self):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None:
return {"selection": {"count": 0, "type": "none", "shape": None},
"refPoint": self._ref, "coordSystem": self._coord_sys}
objs = _get_selected_objects(doc)
plane = _active_plane(doc, self._coord_sys)
bbox_local = _bbox_in_plane(objs, plane)
# Typ der Selektion
typ = "none"
if len(objs) == 0:
typ = "none"
elif len(objs) == 1:
g = objs[0].Geometry
if isinstance(g, rg.Curve): typ = "curve"
elif isinstance(g, rg.Brep): typ = "brep"
elif isinstance(g, rg.Mesh): typ = "mesh"
elif isinstance(g, rg.Extrusion): typ = "extrusion"
elif isinstance(g, rg.InstanceReferenceGeometry): typ = "block"
elif isinstance(g, rg.Point): typ = "point"
elif isinstance(g, rg.TextEntity): typ = "text"
else: typ = "other"
else:
typ = "mixed"
out = {
"selection": {"count": len(objs), "type": typ},
"refPoint": self._ref,
"coordSystem": self._coord_sys,
"planeName": "CPlane" if self._coord_sys == "cplane" else "Welt",
}
shape = _detect_shape(objs)
out["shape"] = shape
if bbox_local is None:
out["position"] = None
out["dimensions"] = None
return out
# Position des Referenzpunkts (in plane-lokalen Koordinaten — das ist
# das, was der User typischerweise sehen will: World ist Plane=WorldXY,
# CPlane ist Plane=ActiveCPlane).
ref_local = _ref_point_local(bbox_local, self._ref)
out["position"] = {
"x": _round(ref_local.X, 4),
"y": _round(ref_local.Y, 4),
"z": _round(ref_local.Z, 4),
}
# Abmessungen (Plane-aligned BBox-Spannweite)
out["dimensions"] = {
"width": _round(bbox_local.Max.X - bbox_local.Min.X, 4),
"depth": _round(bbox_local.Max.Y - bbox_local.Min.Y, 4),
"height": _round(bbox_local.Max.Z - bbox_local.Min.Z, 4),
}
return out
def _send_state(self, force=False):
state = self._compute_state()
# Signature fuer Diff — komplette JSON-Repr ist ok bei kleinen Dicts
sig = (
state.get("selection", {}).get("count"),
state.get("selection", {}).get("type"),
(state.get("shape") or {}).get("type"),
state.get("coordSystem"),
tuple(sorted((state.get("refPoint") or {}).items())),
tuple(sorted((state.get("position") or {}).items())),
tuple(sorted((state.get("dimensions") or {}).items())),
tuple(sorted((state.get("shape") or {}).items())) if state.get("shape") else None,
)
if not force and sig == self._last_sig:
return
self._last_sig = sig
self.send("STATE", state)
def tick_idle(self):
# 1) Schnelle Selektions-Erkennung: ID-Liste vergleichen
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
try:
objs = _get_selected_objects(doc)
ids = tuple(sorted(str(o.Id) for o in objs))
if ids != self._last_ids:
self._last_ids = ids
self._send_state(force=True)
self._idle_cnt = 0
return
except Exception:
pass
# 2) Geometrie kann sich aendern ohne Selektions-Change (Gumball-Move).
# Niedriger-frequenter Geom-Poll.
self._idle_cnt += 1
if self._idle_cnt >= _IDLE_GEOM_POLL:
self._idle_cnt = 0
self._send_state(force=False)
# --- Handler ------------------------------------------------------------
def _set_ref_point(self, p):
ref = self._ref
for k in ("x", "y", "z"):
v = p.get(k)
if v in ("min", "mid", "max"):
ref[k] = v
self._send_state(force=True)
def _set_coord_system(self, p):
mode = p.get("mode")
if mode in ("world", "cplane"):
self._coord_sys = mode
self._send_state(force=True)
def _set_position(self, p):
"""Verschiebt Selektion so, dass der Referenzpunkt auf den neuen
Wert kommt. Nur die angegebene Achse wird geaendert."""
doc = Rhino.RhinoDoc.ActiveDoc
objs = _get_selected_objects(doc)
if not objs: return
axis = p.get("axis")
try: value = float(p.get("value"))
except Exception: return
if axis not in ("x", "y", "z"): return
plane = _active_plane(doc, self._coord_sys)
bbox_local = _bbox_in_plane(objs, plane)
if bbox_local is None: return
ref_local = _ref_point_local(bbox_local, self._ref)
dx = dy = dz = 0.0
if axis == "x": dx = value - ref_local.X
if axis == "y": dy = value - ref_local.Y
if axis == "z": dz = value - ref_local.Z
_translate_in_plane(doc, objs, plane, dx, dy, dz)
self._send_state(force=True)
def _set_dimension(self, p):
"""Skaliert Selektion in der angegebenen Achse, sodass der angegebene
Wert die neue Plane-aligned BBox-Spannweite ist. Referenzpunkt bleibt
fix."""
doc = Rhino.RhinoDoc.ActiveDoc
objs = _get_selected_objects(doc)
if not objs: return
axis = p.get("axis") # 'width' | 'depth' | 'height'
try: value = float(p.get("value"))
except Exception: return
if axis not in ("width", "depth", "height"): return
if value <= 0: return
plane = _active_plane(doc, self._coord_sys)
bbox_local = _bbox_in_plane(objs, plane)
if bbox_local is None: return
ref_world = _ref_point_world(bbox_local, self._ref, plane)
cur_w = bbox_local.Max.X - bbox_local.Min.X
cur_d = bbox_local.Max.Y - bbox_local.Min.Y
cur_h = bbox_local.Max.Z - bbox_local.Min.Z
sx = sy = sz = 1.0
if axis == "width" and cur_w > 0: sx = value / cur_w
if axis == "depth" and cur_d > 0: sy = value / cur_d
if axis == "height" and cur_h > 0: sz = value / cur_h
_scale_around_point(doc, objs, plane, ref_world, sx, sy, sz)
self._send_state(force=True)
def _set_rotation_z(self, p):
"""Rotiert Selektion um die Z-Achse der aktiven Plane durch den
Referenzpunkt um angle Grad."""
doc = Rhino.RhinoDoc.ActiveDoc
objs = _get_selected_objects(doc)
if not objs: return
try: angle = float(p.get("angle"))
except Exception: return
plane = _active_plane(doc, self._coord_sys)
bbox_local = _bbox_in_plane(objs, plane)
if bbox_local is None: return
ref_world = _ref_point_world(bbox_local, self._ref, plane)
_rotate_around_axis(doc, objs, ref_world, plane.ZAxis, angle)
self._send_state(force=True)
def _set_circle_radius(self, p):
doc = Rhino.RhinoDoc.ActiveDoc
objs = _get_selected_objects(doc)
if len(objs) != 1: return
try: radius = float(p.get("value"))
except Exception: return
if radius <= 0: return
plane = _active_plane(doc, self._coord_sys)
bbox_local = _bbox_in_plane(objs, plane)
if bbox_local is None: return
ref_world = _ref_point_world(bbox_local, self._ref, plane)
with _UndoRecord(doc, "Dossier: Radius aendern"):
_set_circle_radius(doc, objs[0], radius, plane, ref_world)
doc.Views.Redraw()
self._send_state(force=True)
def _set_line_length(self, p):
doc = Rhino.RhinoDoc.ActiveDoc
objs = _get_selected_objects(doc)
if len(objs) != 1: return
try: length = float(p.get("value"))
except Exception: return
if length <= 0: return
plane = _active_plane(doc, self._coord_sys)
bbox_local = _bbox_in_plane(objs, plane)
if bbox_local is None: return
ref_world = _ref_point_world(bbox_local, self._ref, plane)
with _UndoRecord(doc, "Dossier: Linienlaenge aendern"):
_set_line_length(doc, objs[0], length, ref_world)
doc.Views.Redraw()
self._send_state(force=True)
def _set_rectangle(self, p):
doc = Rhino.RhinoDoc.ActiveDoc
objs = _get_selected_objects(doc)
if len(objs) != 1: return
try:
w = float(p.get("width"))
h = float(p.get("height"))
except Exception:
return
if w <= 0 or h <= 0: return
plane = _active_plane(doc, self._coord_sys)
bbox_local = _bbox_in_plane(objs, plane)
if bbox_local is None: return
ref_world = _ref_point_world(bbox_local, self._ref, plane)
with _UndoRecord(doc, "Dossier: Rechteck-Abmessung"):
_set_rectangle_dims(doc, objs[0], w, h, plane, ref_world)
doc.Views.Redraw()
self._send_state(force=True)
# --- Listener-Installation --------------------------------------------------
def _install_listeners(bridge):
flag = "dimensionen_listeners"
sc.sticky["dimensionen_bridge"] = bridge
if sc.sticky.get(flag):
return
def on_idle(s, e):
b = sc.sticky.get("dimensionen_bridge")
if b is not None:
try: b.tick_idle()
except Exception as ex: print("[DIMENSIONEN] idle:", ex)
def on_select(s, e):
b = sc.sticky.get("dimensionen_bridge")
if b is not None:
try: b._send_state(force=True)
except Exception: pass
Rhino.RhinoApp.Idle += on_idle
try:
Rhino.RhinoDoc.SelectObjects += on_select
Rhino.RhinoDoc.DeselectObjects += on_select
Rhino.RhinoDoc.DeselectAllObjects += on_select
except Exception as ex:
print("[DIMENSIONEN] select-events:", ex)
sc.sticky[flag] = True
print("[DIMENSIONEN] Listener aktiv (Idle + SelectObjects)")
def _bridge_factory():
b = DimensionenBridge()
_install_listeners(b)
return b
panel_base.register_and_open("dimensionen", "DIMENSIONEN", PANEL_GUID_STR,
_bridge_factory, icon_spec=("D", "#9e7050"))
+4775
View File
File diff suppressed because it is too large Load Diff
+1635
View File
File diff suppressed because it is too large Load Diff
+163
View File
@@ -0,0 +1,163 @@
# ! python3
"""
inspect_section.py
Schreibt ALLE Eigenschaften der SectionStyle der aktuellen Ebene ins Log,
ohne dass irgendein Panel-Setup gebraucht wird.
Aufruf:
_-RunPythonScript "/Users/karim/STUDIO/rhino-panel/rhino/inspect_section.py"
"""
import Rhino
def dump(label, obj):
print("--- {} ({})".format(label, type(obj).__name__ if obj is not None else "None"))
if obj is None:
return
for name in dir(obj):
if name.startswith("_"):
continue
try:
v = getattr(obj, name)
except Exception as ex:
print(" {} -> err: {}".format(name, ex))
continue
if callable(v):
continue
try:
print(" {} = {!r}".format(name, v))
except Exception:
try:
print(" {} = (unprintable)".format(name))
except Exception:
pass
doc = Rhino.RhinoDoc.ActiveDoc
layer = doc.Layers.CurrentLayer
print("============================================")
print("Aktive Ebene:")
print(" Name =", layer.Name)
try: print(" Id =", layer.Id)
except Exception: pass
print("\n--- Layer-Properties die mit 'Section' anfangen ---")
for n in dir(layer):
if n.startswith("_"):
continue
if "section" in n.lower() or "hatch" in n.lower() or "fill" in n.lower():
try:
v = getattr(layer, n)
if callable(v):
continue
print(" layer.{} = {!r}".format(n, v))
except Exception as ex:
print(" layer.{} -> err: {}".format(n, ex))
# layer.SectionStyle dumpen wenn vorhanden
try:
if hasattr(layer, "SectionStyle"):
dump("layer.SectionStyle", layer.SectionStyle)
except Exception as ex:
print(" layer.SectionStyle err:", ex)
# Via doc.SectionStyles + layer.SectionStyleId
try:
if hasattr(layer, "SectionStyleId"):
sid = layer.SectionStyleId
print("\n layer.SectionStyleId =", sid)
for tname in ("SectionStyles", "SectionAttributes"):
if hasattr(doc, tname):
tbl = getattr(doc, tname)
print(" doc.{} =".format(tname), tbl, "Count:",
getattr(tbl, "Count", "?"))
try:
ss = tbl.FindId(sid)
dump("doc.{}.FindId({})".format(tname, sid), ss)
except Exception as ex:
print(" FindId err:", ex)
except Exception as ex:
print("SectionStyleId-Zweig err:", ex)
print("\n--- Doc-Tabellen (alles was 'Section' enthaelt) ---")
for n in dir(doc):
if n.startswith("_"): continue
if "section" in n.lower():
try:
v = getattr(doc, n)
if callable(v): continue
print(" doc.{} = {!r} (count={})".format(
n, v, getattr(v, "Count", "?")))
except Exception as ex:
print(" doc.{} -> err: {}".format(n, ex))
print("\n--- Layer UserDictionary ---")
try:
ud = layer.UserDictionary
cnt = ud.Count
print(" Count:", cnt)
for key in ud.Keys:
try:
v = ud[key]
print(" [{}] = {!r} (type={})".format(key, v, type(v).__name__))
except Exception as ex:
print(" [{}] err: {}".format(key, ex))
except Exception as ex:
print(" UserDictionary err:", ex)
print("\n--- Layer UserStrings (NameValueCollection) ---")
try:
nvc = layer.GetUserStrings()
print(" Count:", nvc.Count)
for k in nvc.AllKeys:
print(" [{}] = {!r}".format(k, nvc[k]))
except Exception as ex:
print(" GetUserStrings err:", ex)
# Fallback: ueber Count + GetUserString
try:
# Manche Rhino-Versionen haben GetUserStringKeys() oder iterieren anders
if hasattr(layer, "GetUserStringKeys"):
keys = layer.GetUserStringKeys()
print(" GetUserStringKeys ->", list(keys))
except Exception as ex:
print(" GetUserStringKeys err:", ex)
print("\n--- Layer UserData (Custom .NET Blobs) ---")
try:
udl = layer.UserData
print(" Count:", udl.Count if udl else "None")
if udl:
for i in range(udl.Count):
ud_item = udl[i]
print(" UserData[{}]:".format(i))
for prop in ("Description", "Name", "Id", "DataCRC", "InstanceId"):
try:
if hasattr(ud_item, prop):
print(" {} = {!r}".format(prop, getattr(ud_item, prop)))
except Exception as ex:
print(" {} err: {}".format(prop, ex))
print(" type:", type(ud_item).__name__)
print(" full type:", type(ud_item).__module__ + "." + type(ud_item).__name__)
# Dump alle public Attrs
for a in sorted(dir(ud_item)):
if a.startswith("_"): continue
try:
v = getattr(ud_item, a)
if callable(v): continue
print(" .{} = {!r}".format(a, v))
except Exception:
pass
except Exception as ex:
print(" UserData err:", ex)
print("\n--- Layer-Properties vollstaendig (alphabetisch) ---")
for n in sorted(dir(layer)):
if n.startswith("_"): continue
try:
v = getattr(layer, n)
if callable(v): continue
print(" layer.{} = {!r}".format(n, v))
except Exception as ex:
print(" layer.{} -> err: {}".format(n, ex))
print("============================================")
+436
View File
@@ -0,0 +1,436 @@
# ! python3
# -*- coding: utf-8 -*-
"""
layer_builder.py
Layer-Struktur:
<Zeichnungsebene-Name>
+-- 00_RASTER
+-- 01_VERMESSUNG
+-- 20_WAENDE
+-- ...
Jede Zeichnungsebene erhaelt alle definierten Ebenen als Sublayer.
"""
import System
import System.Drawing as Drawing
import Rhino
GREY = Drawing.Color.FromArgb(150, 150, 150)
_FROM_LAYER = Rhino.DocObjects.ObjectColorSource.ColorFromLayer
_FROM_OBJECT = Rhino.DocObjects.ObjectColorSource.ColorFromObject
_EMPTY_GUID = System.Guid.Empty
def _color(hex_str):
h = (hex_str or "#888888").lstrip("#")
if len(h) == 3:
h = "".join(c * 2 for c in h)
return Drawing.Color.FromArgb(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
def _is_top_level(layer):
return layer.ParentLayerId == _EMPTY_GUID
def _find_top_by_id(doc, dossier_id):
for i, layer in enumerate(doc.Layers):
if not _is_top_level(layer):
continue
v = layer.GetUserString("dossier_id")
if v == dossier_id:
return i
return -1
def _find_top_by_name(doc, name):
for i, layer in enumerate(doc.Layers):
if _is_top_level(layer) and layer.Name == name:
return i
return -1
def _find_sublayer_by_code(doc, parent_id, code):
prefix = code + "_"
for i, layer in enumerate(doc.Layers):
if layer.ParentLayerId == parent_id and layer.Name.startswith(prefix):
return i
return -1
def _add_layer(doc, name, parent_id=None, color=None, lw=None):
layer = Rhino.DocObjects.Layer()
layer.Name = name
if parent_id is not None and parent_id != _EMPTY_GUID:
layer.ParentLayerId = parent_id
if color is not None:
layer.Color = color
if lw is not None:
layer.PlotWeight = lw
return doc.Layers.Add(layer)
def build_layers(doc, zeichnungsebenen, ebenen):
"""
Stellt sicher dass fuer jede Zeichnungsebene ein Parent-Layer existiert
und unter jedem alle Ebenen als Sublayer angelegt/aktualisiert sind.
"""
for z in zeichnungsebenen:
z_id = z["id"]
z_name = z["name"]
# Parent finden oder anlegen
idx = _find_top_by_id(doc, z_id)
if idx < 0:
idx = _find_top_by_name(doc, z_name)
if idx < 0:
idx = _add_layer(doc, z_name)
doc.Layers[idx].SetUserString("dossier_id", z_id)
else:
parent = doc.Layers[idx]
if parent.Name != z_name:
parent.Name = z_name
parent.SetUserString("dossier_id", z_id)
parent_id = doc.Layers[idx].Id
# Sublayer pro Ebene
for e in ebenen:
sub_name = "{}_{}".format(e["code"], e["name"])
col = _color(e.get("color"))
lw = float(e.get("lw", 0.13))
sub_idx = _find_sublayer_by_code(doc, parent_id, e["code"])
if sub_idx < 0:
sub_idx = _add_layer(doc, sub_name, parent_id, col, lw)
doc.Layers[sub_idx].SetUserString("dossier_code", e["code"])
else:
sub = doc.Layers[sub_idx]
if sub.Name != sub_name:
sub.Name = sub_name
sub.Color = col
try:
import massstab as _ms
_ms.write_plotweight(doc, sub, float(lw))
except Exception:
sub.PlotWeight = lw
sub.SetUserString("dossier_code", e["code"])
doc.Views.Redraw()
print("[EBENEN] {} Zeichnungsebenen x {} Ebenen aktualisiert".format(
len(zeichnungsebenen), len(ebenen)))
def update_layer_style(doc, code, color_hex=None, lw=None):
"""Aendert Farbe und/oder Stiftdicke fuer alle Sublayer mit dem gegebenen Code."""
col = _color(color_hex) if color_hex else None
try:
import massstab as _ms
except Exception:
_ms = None
for i, layer in enumerate(doc.Layers):
if _is_top_level(layer):
continue
if layer.Name.startswith(code + "_"):
if col is not None:
layer.Color = col
if lw is not None:
if _ms is not None:
_ms.write_plotweight(doc, layer, float(lw))
else:
layer.PlotWeight = float(lw)
doc.Views.Redraw()
def set_ebene_visible(doc, code, visible):
"""Schaltet alle Sublayer mit Code in/aus Zeichnungsebenen."""
for i, layer in enumerate(doc.Layers):
if _is_top_level(layer):
continue
if layer.Name.startswith(code + "_"):
layer.IsVisible = visible
doc.Views.Redraw()
def set_ebene_locked(doc, code, locked):
for i, layer in enumerate(doc.Layers):
if _is_top_level(layer):
continue
if layer.Name.startswith(code + "_"):
layer.IsLocked = locked
doc.Views.Redraw()
def delete_ebene(doc, code, move_to=None):
"""
Loescht alle Sublayer mit dem gegebenen Code in allen Zeichnungsebenen.
Falls move_to gesetzt: verschiebt vorher alle Objekte zum Sublayer
mit move_to-Code unter dem selben Parent. Sonst: loescht Objekte mit.
"""
if not code:
return
from_prefix = code + "_"
to_prefix = (move_to + "_") if move_to else None
# Top-Level Parents finden
parents = [layer for layer in doc.Layers if _is_top_level(layer)]
moved = 0
deleted_objs = 0
deleted_layers = 0
for parent in parents:
from_layer = None
to_layer = None
for layer in doc.Layers:
if layer.ParentLayerId != parent.Id:
continue
if layer.Name.startswith(from_prefix):
from_layer = layer
elif to_prefix and layer.Name.startswith(to_prefix):
to_layer = layer
if from_layer is None:
continue
from_idx = doc.Layers.FindByFullPath(from_layer.FullPath, -1)
if from_idx < 0:
continue
objs = list(doc.Objects.FindByLayer(doc.Layers[from_idx]))
if move_to and to_layer is not None:
to_idx = doc.Layers.FindByFullPath(to_layer.FullPath, -1)
if to_idx >= 0:
for obj in objs:
attrs = obj.Attributes.Duplicate()
attrs.LayerIndex = to_idx
if doc.Objects.ModifyAttributes(obj, attrs, True):
moved += 1
else:
for obj in objs:
if doc.Objects.Delete(obj.Id, True):
deleted_objs += 1
# Sublayer loeschen
try:
if doc.Layers.Delete(from_idx, True):
deleted_layers += 1
except Exception as ex:
print("[EBENEN] Layer-Delete:", ex)
doc.Views.Redraw()
print("[EBENEN] Ebene {} entfernt: {} Sublayer, {} Objekte verschoben, {} Objekte geloescht".format(
code, deleted_layers, moved, deleted_objs))
# --- Clipping Plane Management ----------------------------------------------
_CLIP_KEY = "dossier_clipping_plane"
def _find_clipping_plane(doc):
for obj in doc.Objects:
try:
if obj.Attributes.GetUserString(_CLIP_KEY) == "1":
return obj
except Exception:
pass
return None
def update_clipping_plane(doc, active_z, enabled):
"""
Erstellt/aktualisiert/entfernt die DOSSIER-Clipping-Plane an OKFF + Schnitthoehe
des aktiven Geschosses. Plane zeigt nach +Z, schneidet alles oberhalb weg.
"""
import Rhino.Geometry as rg
existing = _find_clipping_plane(doc)
is_geschoss = bool(active_z and active_z.get("isGeschoss") and active_z.get("okff") is not None)
if (not enabled) or (not is_geschoss):
if existing is not None:
try:
doc.Objects.Delete(existing.Id, True)
except Exception:
pass
return
okff = float(active_z.get("okff", 0.0))
sh = float(active_z.get("schnitthoehe", 1.0))
cut_z = okff + sh
plane = rg.Plane(rg.Point3d(0.0, 0.0, cut_z), rg.Vector3d.ZAxis)
du, dv = 50000.0, 50000.0
if existing is not None:
try:
new_surf = rg.PlaneSurface(plane, rg.Interval(-du/2.0, du/2.0), rg.Interval(-dv/2.0, dv/2.0))
doc.Objects.Replace(existing.Id, new_surf)
except Exception as ex:
print("[EBENEN] Clip-Update:", ex)
else:
vp_ids = []
for view in doc.Views:
try:
vp_ids.append(view.ActiveViewportID)
except Exception:
try: vp_ids.append(view.ActiveViewport.Id)
except Exception: pass
try:
new_id = doc.Objects.AddClippingPlane(plane, du, dv, vp_ids)
if new_id != System.Guid.Empty:
obj = doc.Objects.FindId(new_id)
if obj is not None:
attrs = obj.Attributes.Duplicate()
attrs.SetUserString(_CLIP_KEY, "1")
attrs.Mode = Rhino.DocObjects.ObjectMode.Locked
doc.Objects.ModifyAttributes(obj, attrs, True)
print("[EBENEN] Clipping-Plane bei Z={} erstellt".format(cut_z))
except Exception as ex:
print("[EBENEN] Clip-Create:", ex)
doc.Views.Redraw()
def cleanup_default_layers(doc):
"""Loescht leere Rhino-Default-Layer (Default, Layer 01, ...) — nicht-leere bleiben unberuehrt."""
import re
pattern = re.compile(r'^(default|layer\s*0*\d+)$', re.IGNORECASE)
deleted = []
for i in range(doc.Layers.Count - 1, -1, -1):
layer = doc.Layers[i]
if layer.IsDeleted:
continue
if not _is_top_level(layer):
continue
if not pattern.match(layer.Name.strip()):
continue
try:
# Name VOR Delete sichern — sonst liefert layer.Name danach None
nm = layer.Name
if doc.Layers.Delete(i, True):
if nm: deleted.append(nm)
except Exception:
pass
if deleted:
print("[EBENEN] Default-Layer entfernt: {}".format(", ".join(deleted)))
def set_active_sublayer(doc, zeichnungsebene_id, code):
"""Macht den Sublayer 'code' unter Zeichnungsebene 'zeichnungsebene_id' aktiv."""
parent_idx = _find_top_by_id(doc, zeichnungsebene_id)
if parent_idx < 0:
print("[EBENEN] Parent-Layer fuer Zeichnungsebene {} nicht gefunden".format(zeichnungsebene_id))
return
parent_id = doc.Layers[parent_idx].Id
sub_idx = _find_sublayer_by_code(doc, parent_id, code)
if sub_idx >= 0:
doc.Layers.SetCurrentLayerIndex(sub_idx, True)
else:
print("[EBENEN] Sublayer mit Code {} unter Parent {} nicht gefunden".format(code, doc.Layers[parent_idx].Name))
def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_mode, e_mode):
"""
Kombinierte Sichtbarkeit aus Z-Mode (Zeichnungsebenen) und E-Mode (Ebenen).
Beide Modi: 'all' | 'active' | 'grey' | 'grey_locked'
"""
canonical = {e["code"]: _color(e.get("color")) for e in ebenen}
e_eye_vis = {e["code"]: e.get("visible", True) for e in ebenen}
e_eye_locked = {e["code"]: e.get("locked", False) for e in ebenen}
id_to_top, name_to_top, children_by_parent = {}, {}, {}
for layer in doc.Layers:
if _is_top_level(layer):
uid = layer.GetUserString("dossier_id")
if uid:
id_to_top[uid] = layer
name_to_top[layer.Name] = layer
else:
children_by_parent.setdefault(layer.ParentLayerId, []).append(layer)
for z in zeichnungsebenen:
parent = id_to_top.get(z["id"]) or name_to_top.get(z["name"])
if parent is None:
continue
children = children_by_parent.get(parent.Id, [])
is_active_z = z["id"] == active_z_id
z_visible_flag = z.get("visible", True)
# Z-Mode -> Parent-Zustand
if is_active_z:
p_vis, p_grey, p_lock = True, False, False
elif z_mode == "active":
p_vis, p_grey, p_lock = False, False, False
elif not z_visible_flag:
p_vis, p_grey, p_lock = False, False, False
elif z_mode == "all":
p_vis, p_grey, p_lock = True, False, False
elif z_mode == "grey_locked":
p_vis, p_grey, p_lock = True, True, True
else: # grey
p_vis, p_grey, p_lock = True, True, False
parent_changed = False
if parent.IsVisible != p_vis:
parent.IsVisible = p_vis
parent_changed = True
if parent.IsLocked != p_lock:
parent.IsLocked = p_lock
parent_changed = True
if parent_changed:
try: doc.Layers.Modify(parent, parent.LayerIndex, True)
except Exception: pass
if not p_vis:
continue # Children erben Parent-Hidden
# E-Mode -> Sublayer-Zustand
for child in children:
if "_" not in child.Name:
continue
code = child.Name.split("_", 1)[0]
if code not in canonical:
continue
is_active_e = (code == active_code)
eye_v = e_eye_vis.get(code, True)
eye_l = e_eye_locked.get(code, False)
if is_active_e:
e_vis, e_grey, e_lock = True, False, False
elif e_mode == "active":
e_vis, e_grey, e_lock = False, False, False
elif not eye_v:
e_vis, e_grey, e_lock = False, False, False
elif e_mode == "all":
e_vis, e_grey, e_lock = True, False, False
elif e_mode == "grey_locked":
e_vis, e_grey, e_lock = True, True, True
else: # grey
e_vis, e_grey, e_lock = True, True, False
# Kombination
child_vis = e_vis
child_grey = p_grey or e_grey
child_lock = e_lock or eye_l
changed = False
if child.IsVisible != child_vis:
child.IsVisible = child_vis
changed = True
if child.IsLocked != child_lock:
child.IsLocked = child_lock
changed = True
if child_grey:
if child.Color != GREY:
child.Color = GREY
changed = True
else:
canon = canonical.get(code)
if canon is not None and child.Color != canon:
child.Color = canon
changed = True
# In neueren Rhino-Versionen committed der Property-Setter direkt,
# in manchen Faellen (besonders auf Mac) wird IsLocked nicht
# persistiert ohne explizites Modify. Defensiv:
if changed:
try:
doc.Layers.Modify(child, child.LayerIndex, True)
except Exception:
pass
doc.Views.Redraw()
+748
View File
@@ -0,0 +1,748 @@
# ! python3
# -*- coding: utf-8 -*-
"""
layouts.py
LAYOUTS-Panel: Layout-Pages erstellen + Details mit Ausschnitten bestuecken.
Phase 1 — Snapshot-Mode: Ausschnitt wird beim Zuweisen auf das Detail angewendet,
Re-Sync per Knopf. Live-Link und Masterlayouts kommen spaeter.
"""
import os
import sys
import json
import Rhino
import Rhino.Geometry as rg
import System
import scriptcontext as sc
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
import panel_base
PANEL_GUID_STR = "4e5d6c7b-8a9f-4e3d-c4b5-a6b7c8d9e0f1"
# UserString-Key auf jedem Detail — speichert die Ausschnitt-Bindung.
_BIND_KEY = "dossier_bound_ausschnitt"
# Doc-Strings fuer Layout-Folder-Organisation (analog Ausschnitte-Ordner).
_FOLDER_LIST_KEY = "dossier_layout_folders" # JSON-Array: ["A","B",...]
_FOLDER_MAP_KEY = "dossier_layout_folder_map" # JSON-Dict: {pageId: "A"}
# Vordefinierte Papierformate in Millimetern (Welt-Einheit wird beim Erstellen
# umgerechnet, falls das Doc nicht auf mm steht).
PAPER_SIZES_MM = {
"A0": (841, 1189),
"A1": (594, 841),
"A2": (420, 594),
"A3": (297, 420),
"A4": (210, 297),
"Letter": (216, 279),
}
def _page_unit_system(doc):
"""Rhino hat ein eigenes Unit-System fuer Layouts (PageUnitSystem), das
sich vom Modell-Unit-System unterscheiden kann (z.B. Modell in Meter,
Plan in Millimeter — Standard bei Architektur). AddPageView und
SetPageSize erwarten Werte in PageUnitSystem, NICHT in ModelUnitSystem."""
try:
return doc.PageUnitSystem
except Exception:
return doc.ModelUnitSystem
def _mm_to_page(doc):
"""Faktor: mm -> Page-Unit. Wird fuer AddPageView/SetPageSize benutzt."""
try:
return Rhino.RhinoMath.UnitScale(Rhino.UnitSystem.Millimeters,
_page_unit_system(doc))
except Exception:
return 1.0
def _page_to_mm(doc):
"""Faktor: Page-Unit -> mm. Wird beim PDF-Export gebraucht."""
try:
return Rhino.RhinoMath.UnitScale(_page_unit_system(doc),
Rhino.UnitSystem.Millimeters)
except Exception:
return 1.0
def _page_size_in_doc(doc, fmt, landscape):
"""Liefert (width, height) in Page-Units."""
if fmt not in PAPER_SIZES_MM: return None
w_mm, h_mm = PAPER_SIZES_MM[fmt]
if landscape:
w_mm, h_mm = h_mm, w_mm
f = _mm_to_page(doc)
return (w_mm * f, h_mm * f)
def _load_folder_list(doc):
"""Liefert die Liste explizit angelegter Ordner (Reihenfolge bleibt)."""
raw = doc.Strings.GetValue(_FOLDER_LIST_KEY)
if not raw: return []
try:
data = json.loads(raw)
return [n for n in data if isinstance(n, str) and n]
except Exception:
return []
def _save_folder_list(doc, names):
try:
doc.Strings.SetString(_FOLDER_LIST_KEY, json.dumps(list(names), ensure_ascii=False))
except Exception as ex:
print("[LAYOUTS] _save_folder_list:", ex)
def _load_folder_map(doc):
"""page_id -> folder_name."""
raw = doc.Strings.GetValue(_FOLDER_MAP_KEY)
if not raw: return {}
try:
data = json.loads(raw)
if isinstance(data, dict):
return {k: v for k, v in data.items() if isinstance(v, str) and v}
except Exception:
pass
return {}
def _save_folder_map(doc, m):
try:
doc.Strings.SetString(_FOLDER_MAP_KEY, json.dumps(m, ensure_ascii=False))
except Exception as ex:
print("[LAYOUTS] _save_folder_map:", ex)
def _list_layouts(doc):
"""Liefert dict-Liste aller PageView-Layouts inkl. Ordner-Zuweisung.
Groessen werden ZUSAETZLICH in mm geliefert, damit das Frontend ohne
Unit-Kenntnis formatieren kann."""
fmap = _load_folder_map(doc)
pu_to_mm = _page_to_mm(doc)
out = []
for v in doc.Views:
if isinstance(v, Rhino.Display.RhinoPageView):
try:
pid = str(v.MainViewport.Id)
w = float(v.PageWidth)
h = float(v.PageHeight)
out.append({
"id": pid,
"name": v.PageName or "",
"width": w,
"height": h,
"widthMm": w * pu_to_mm,
"heightMm": h * pu_to_mm,
"detailCount": len(v.GetDetailViews() or []),
"folder": fmap.get(pid, ""),
})
except Exception as ex:
print("[LAYOUTS] list_layouts:", ex)
return out
def _find_page_by_id(doc, page_id):
"""page_id ist die MainViewport.Id (str). Liefert RhinoPageView oder None."""
for v in doc.Views:
if isinstance(v, Rhino.Display.RhinoPageView):
try:
if str(v.MainViewport.Id) == page_id:
return v
except Exception:
pass
return None
def _find_detail_by_id(doc, page, detail_id):
"""Detail-Object auf einer Page anhand seiner Detail-ID."""
try:
for d in page.GetDetailViews():
if str(d.Id) == detail_id:
return d
except Exception as ex:
print("[LAYOUTS] find_detail:", ex)
return None
def _get_detail_binding(detail):
"""Liest gebundene Ausschnitt-ID aus dem Detail-UserString."""
try:
v = detail.Attributes.GetUserString(_BIND_KEY)
return v if v else None
except Exception:
return None
def _set_detail_binding(detail, snap_id):
"""Schreibt/loescht die Ausschnitt-ID auf das Detail-UserString."""
try:
if snap_id:
detail.Attributes.SetUserString(_BIND_KEY, snap_id)
else:
detail.Attributes.SetUserString(_BIND_KEY, "") # leerer String = "kein Binding"
detail.CommitChanges()
return True
except Exception as ex:
print("[LAYOUTS] set_binding:", ex)
return False
def _detail_dict(detail, snap_lookup):
"""Serialisiert ein Detail fuer das Frontend."""
bbox = detail.Geometry.GetBoundingBox(True) # in Welt-Koordinaten der Page
bound_id = _get_detail_binding(detail)
bound_name = (snap_lookup.get(bound_id, {}) or {}).get("name") if bound_id else None
try:
vp_name = detail.Viewport.Name
except Exception:
vp_name = ""
return {
"id": str(detail.Id),
"name": vp_name,
"x": float(bbox.Min.X),
"y": float(bbox.Min.Y),
"width": float(bbox.Max.X - bbox.Min.X),
"height": float(bbox.Max.Y - bbox.Min.Y),
"boundAusschnitt": bound_id,
"boundAusschnittName": bound_name,
}
def _snap_lookup(doc):
"""Map snap_id -> snap dict. Wird fuer Detail-Display gebraucht."""
out = {}
try:
raw = doc.Strings.GetValue("dossier_ausschnitte")
if raw:
data = json.loads(raw)
if isinstance(data, list):
for s in data:
if isinstance(s, dict) and s.get("id"):
out[s["id"]] = s
except Exception:
pass
return out
def _slim_snaps(doc):
"""Schlanke Liste von Snapshots fuer Frontend-Dropdown."""
out = []
try:
raw = doc.Strings.GetValue("dossier_ausschnitte")
if not raw: return []
data = json.loads(raw)
if not isinstance(data, list): return []
for s in data:
if isinstance(s, dict) and s.get("id"):
out.append({
"id": s.get("id"),
"name": s.get("name"),
"folder": s.get("folder", ""),
"scale": s.get("scale", ""),
})
except Exception:
pass
return out
# --- Bridge -----------------------------------------------------------------
class LayoutsBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "layouts")
def _on_ready(self):
self._send_state()
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
p = data.get("payload") or {}
if not isinstance(p, dict): p = {}
if t == "READY": self._on_ready()
elif t == "LIST": self._send_state()
elif t == "NEW_LAYOUT": self._new_layout(p)
elif t == "DELETE_LAYOUT": self._delete_layout(p.get("id"))
elif t == "RENAME_LAYOUT": self._rename_layout(p.get("id"), p.get("name"))
elif t == "SET_PAGE_SIZE": self._set_page_size(p)
elif t == "ACTIVATE_LAYOUT": self._activate_layout(p.get("id"))
elif t == "EXPORT_PDF": self._export_pdf(p)
elif t == "ADD_DETAIL": self._add_detail(p)
elif t == "DELETE_DETAIL": self._delete_detail(p.get("pageId"), p.get("detailId"))
elif t == "BIND_AUSSCHNITT": self._bind_ausschnitt(p)
elif t == "SYNC_DETAIL": self._sync_detail(p.get("pageId"), p.get("detailId"))
elif t == "SYNC_LAYOUT": self._sync_layout(p.get("id"))
# Ordner-Management
elif t == "ADD_FOLDER": self._add_folder(p.get("name"))
elif t == "REMOVE_FOLDER": self._remove_folder(p.get("name"))
elif t == "SET_FOLDER": self._set_folder(p.get("id"), p.get("folder") or "")
# --- State-Snapshot -----------------------------------------------------
def _send_state(self):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None:
self.send("STATE", {"layouts": [], "snapshots": [], "details": {}, "folders": []})
return
layouts = _list_layouts(doc)
snaps = _slim_snaps(doc)
snap_lookup = _snap_lookup(doc)
# Ordner: explizite Liste + alle in Layouts referenzierten
explicit_folders = _load_folder_list(doc)
for l in layouts:
f = l.get("folder")
if f and f not in explicit_folders:
explicit_folders.append(f)
# Pro Layout die Details mitgeben
details = {}
for v in doc.Views:
if isinstance(v, Rhino.Display.RhinoPageView):
try:
pid = str(v.MainViewport.Id)
details[pid] = [_detail_dict(d, snap_lookup) for d in v.GetDetailViews()]
except Exception as ex:
print("[LAYOUTS] details for page:", ex)
self.send("STATE", {
"layouts": layouts,
"snapshots": snaps,
"details": details,
"folders": explicit_folders,
})
# --- Ordner -------------------------------------------------------------
def _add_folder(self, name):
if not name: return
name = name.strip()
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
folders = _load_folder_list(doc)
if name not in folders:
folders.append(name)
_save_folder_list(doc, folders)
self._send_state()
def _remove_folder(self, name):
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
folders = [f for f in _load_folder_list(doc) if f != name]
_save_folder_list(doc, folders)
# Layouts aus diesem Ordner herausnehmen (zurueck auf Root)
m = _load_folder_map(doc)
m = {k: v for k, v in m.items() if v != name}
_save_folder_map(doc, m)
self._send_state()
def _set_folder(self, page_id, folder):
if not page_id: return
doc = Rhino.RhinoDoc.ActiveDoc
m = _load_folder_map(doc)
if folder:
m[page_id] = folder
# Sicherstellen dass der Ordner-Name in der expliziten Liste ist
folders = _load_folder_list(doc)
if folder not in folders:
folders.append(folder)
_save_folder_list(doc, folders)
else:
if page_id in m: del m[page_id]
_save_folder_map(doc, m)
self._send_state()
# --- Layouts ------------------------------------------------------------
def _resolve_size(self, doc, p):
"""Bestimmt (w, h) in Page-Units aus Payload — Format-Name ODER
customWidth/customHeight in mm."""
fmt = p.get("format")
if fmt == "custom":
try:
wmm = float(p.get("customWidth"))
hmm = float(p.get("customHeight"))
except Exception:
return None
if wmm <= 0 or hmm <= 0: return None
f = _mm_to_page(doc)
return (wmm * f, hmm * f)
if fmt in PAPER_SIZES_MM:
return _page_size_in_doc(doc, fmt, bool(p.get("landscape", True)))
return None
def _new_layout(self, p):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
name = (p.get("name") or "").strip()
size = self._resolve_size(doc, p)
if size is None:
print("[LAYOUTS] ungueltige Groesse:", p); return
w, h = size
if not name:
name = "Layout {}".format(len(_list_layouts(doc)) + 1)
try:
page = doc.Views.AddPageView(name, w, h)
if page is None:
print("[LAYOUTS] AddPageView fehlgeschlagen"); return
print("[LAYOUTS] '{}' angelegt ({}x{})".format(name, w, h))
except Exception as ex:
print("[LAYOUTS] AddPageView Fehler:", ex)
self._send_state()
def _set_page_size(self, p):
"""Aendert die Groesse einer bestehenden Layout-Seite via
RhinoPageView.SetPageSize (Rhino 8). Faellt zurueck auf Property-
Setter, falls die Methode nicht existiert."""
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, p.get("id"))
if page is None: return
size = self._resolve_size(doc, p)
if size is None:
print("[LAYOUTS] ungueltige Groesse fuer set_page_size:", p); return
w, h = size
done = False
# 1) SetPageSize(double, double) — Rhino 8
if hasattr(page, "SetPageSize"):
try:
page.SetPageSize(float(w), float(h))
done = True
print("[LAYOUTS] SetPageSize -> {}x{}".format(w, h))
except Exception as ex:
print("[LAYOUTS] SetPageSize fehlgeschlagen:", ex)
# 2) Fallback: Properties (haengt von Rhino-Version ab)
if not done:
try:
page.PageWidth = float(w)
page.PageHeight = float(h)
done = True
print("[LAYOUTS] PageWidth/Height-Properties -> {}x{}".format(w, h))
except Exception as ex:
print("[LAYOUTS] Property-Setter fehlgeschlagen:", ex)
if not done:
print("[LAYOUTS] Konnte Seiten-Groesse nicht setzen — bitte ueber Rhinos Layout-Dialog aendern")
try: page.Redraw()
except Exception: pass
self._send_state()
def _export_pdf(self, p):
"""Exportiert Layouts als ein gemeinsames PDF. Akzeptiert:
- "ids": Liste von Layout-IDs (Multi-Export)
- "id": einzelne Layout-ID
- sonst alle Layouts.
Save-Dialog via Eto.Forms, Inhalt via FilePdf API mit
EXPLIZITER Pixel-Groesse aus Page-Dimensionen (sonst wird's ein
Mini-Bildchen)."""
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
ids = p.get("ids")
if not ids and p.get("id"): ids = [p.get("id")]
dpi = float(p.get("dpi") or 300)
targets = []
if ids:
for pid in ids:
page = _find_page_by_id(doc, pid)
if page is not None: targets.append(page)
else:
for v in doc.Views:
if isinstance(v, Rhino.Display.RhinoPageView):
targets.append(v)
if not targets:
print("[LAYOUTS] kein Layout zu exportieren"); return
path = self._pick_save_path(doc, targets)
if not path:
print("[LAYOUTS] Export abgebrochen")
return
# PageWidth/PageHeight sind in Page-Units — fuer die Pixel-Berechnung
# rechnen wir in mm um (1 inch = 25.4 mm).
mm_per_pu = _page_to_mm(doc)
# Remember current active view to restore afterwards
prev_view = None
try: prev_view = doc.Views.ActiveView
except Exception: pass
try:
pdf = Rhino.FileIO.FilePdf.Create()
n_added = 0
for page in targets:
try:
# Page muss kurz aktiv sein — sonst rendert ViewCapture
# leer (vor allem auf macOS). Kurzer Idle-Wait gibt dem
# Renderer Zeit, sonst kommt's beim ERSTEN Page-Wechsel
# zu Race-Bedingungen.
try:
page.SetPageAsActive()
doc.Views.ActiveView = page
page.Redraw()
doc.Views.Redraw()
Rhino.RhinoApp.Wait() # einen Idle-Tick verarbeiten
except Exception: pass
# Page-Size in mm -> Pixel @dpi
w_mm = float(page.PageWidth) * mm_per_pu
h_mm = float(page.PageHeight) * mm_per_pu
if w_mm <= 0 or h_mm <= 0:
print("[LAYOUTS] Page '{}' hat ungueltige Groesse: {}x{} mm".format(
page.PageName, w_mm, h_mm))
continue
px_w = max(1, int(round(w_mm / 25.4 * dpi)))
px_h = max(1, int(round(h_mm / 25.4 * dpi)))
# Settings mit expliziter Groesse — die Drei-Argument-
# Variante ist die zuverlaessige fuer PDF-Export.
settings = None
try:
size = System.Drawing.Size(px_w, px_h)
settings = Rhino.Display.ViewCaptureSettings(page, size, dpi)
except Exception as ex:
print("[LAYOUTS] 3-arg Settings:", ex)
if settings is None:
settings = Rhino.Display.ViewCaptureSettings(page, dpi)
# Vector-Output — sonst wird's gerastert und klein
try: settings.RasterMode = False
except Exception: pass
pdf.AddPage(settings)
n_added += 1
print("[LAYOUTS] add page '{}': {}x{}mm -> {}x{}px".format(
page.PageName, w_mm, h_mm, px_w, px_h))
except Exception as ex:
print("[LAYOUTS] add_page '{}': {}".format(page.PageName, ex))
if n_added == 0:
print("[LAYOUTS] Keine Seiten konnten hinzugefuegt werden")
else:
pdf.Write(path)
print("[LAYOUTS] PDF geschrieben: {} ({} Seite(n))".format(path, n_added))
except Exception as ex:
print("[LAYOUTS] PDF-Export fehlgeschlagen:", ex)
finally:
# Vorherige View wieder aktivieren
if prev_view is not None:
try: doc.Views.ActiveView = prev_view
except Exception: pass
def _pick_save_path(self, doc, targets):
"""Eto.Forms SaveFileDialog — Default: Doc-Folder + erster Layout-Name."""
try:
import Eto.Forms as forms
dlg = forms.SaveFileDialog()
dlg.Filters.Add(forms.FileFilter("PDF", ".pdf"))
try: dlg.CurrentFilterIndex = 0
except Exception: pass
# Default-Filename — Layout-Name oder Doc-Name
if len(targets) == 1:
base = targets[0].PageName or "Layout"
else:
base = "Layouts"
if doc.Path:
base = os.path.splitext(os.path.basename(doc.Path))[0] + "_Layouts"
dlg.FileName = "{}.pdf".format(base)
# Default-Folder — neben der .3dm wenn vorhanden
if doc.Path:
try: dlg.Directory = System.Uri(os.path.dirname(doc.Path))
except Exception: pass
try:
import Rhino.UI as RhinoUI
parent = RhinoUI.RhinoEtoApp.MainWindow
except Exception:
parent = None
result = dlg.ShowDialog(parent)
if str(result) != "Ok":
return None
path = dlg.FileName
if path and not path.lower().endswith(".pdf"):
path += ".pdf"
return path
except Exception as ex:
print("[LAYOUTS] SaveFileDialog:", ex)
return None
def _delete_layout(self, page_id):
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, page_id)
if page is None:
print("[LAYOUTS] delete: page not found", page_id); return
name = page.PageName
# Andere View aktivieren — Rhino verweigert oft, die aktive Page zu loeschen
try:
for v in doc.Views:
if v is not page and not isinstance(v, Rhino.Display.RhinoPageView):
doc.Views.ActiveView = v
break
else:
# Nur PageViews da — irgendeine andere aktivieren
for v in doc.Views:
if v is not page:
doc.Views.ActiveView = v
break
except Exception as ex:
print("[LAYOUTS] activate other view:", ex)
done = False
# Methode 1: doc.Views.Remove
try:
r = doc.Views.Remove(page)
print("[LAYOUTS] doc.Views.Remove returned:", r)
# Verify
if _find_page_by_id(doc, page_id) is None:
done = True
except Exception as ex:
print("[LAYOUTS] doc.Views.Remove failed:", ex)
# Methode 2: Close + Delete via RunScript-Fallback
if not done:
try:
Rhino.RhinoApp.RunScript('_-CommandHistory _Hide _Enter', False)
except Exception: pass
try:
# Rhino 8 hat _-Layout _Delete <name>
Rhino.RhinoApp.RunScript('_-Layout _Delete "{}" _Enter'.format(name), False)
if _find_page_by_id(doc, page_id) is None:
done = True
print("[LAYOUTS] geloescht via _-Layout _Delete")
except Exception as ex:
print("[LAYOUTS] _-Layout _Delete failed:", ex)
if not done:
print("[LAYOUTS] Konnte Layout '{}' nicht loeschen — bitte manuell ueber Layout-Tab".format(name))
# Folder-Mapping aufraeumen
try:
m = _load_folder_map(doc)
if page_id in m:
del m[page_id]
_save_folder_map(doc, m)
except Exception: pass
self._send_state()
def _rename_layout(self, page_id, name):
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, page_id)
if page is None or not name: return
try:
page.PageName = name.strip()
except Exception as ex:
print("[LAYOUTS] Rename page:", ex)
self._send_state()
def _activate_layout(self, page_id):
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, page_id)
if page is None: return
try:
page.SetPageAsActive()
doc.Views.ActiveView = page
except Exception as ex:
print("[LAYOUTS] activate page:", ex)
# --- Details ------------------------------------------------------------
def _add_detail(self, p):
"""Neues Detail auf einer Seite anlegen. Optional gleich an einen
Ausschnitt binden (= Snapshot anwenden)."""
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, p.get("pageId"))
if page is None: return
# Bounding-Box auf der Seite: default 80% der Seitenflaeche, zentriert
try:
pw = page.PageWidth
ph = page.PageHeight
margin_x = pw * 0.1
margin_y = ph * 0.1
c0 = rg.Point2d(margin_x, margin_y)
c1 = rg.Point2d(pw - margin_x, ph - margin_y)
# Erlaubt Override per Payload
for k_min, default in (("x", margin_x), ("y", margin_y)):
v = p.get(k_min)
if isinstance(v, (int, float)):
if k_min == "x": c0 = rg.Point2d(float(v), c0.Y)
if k_min == "y": c0 = rg.Point2d(c0.X, float(v))
for k_max, default in (("x2", pw - margin_x), ("y2", ph - margin_y)):
v = p.get(k_max)
if isinstance(v, (int, float)):
if k_max == "x2": c1 = rg.Point2d(float(v), c1.Y)
if k_max == "y2": c1 = rg.Point2d(c1.X, float(v))
proj = Rhino.Display.DefinedViewportProjection.Top
detail = page.AddDetailView("Detail", c0, c1, proj)
if detail is None:
print("[LAYOUTS] AddDetailView gab None"); return
page.Redraw()
# Optional Ausschnitt binden + anwenden
snap_id = p.get("ausschnittId")
if snap_id:
_set_detail_binding(detail, snap_id)
try:
import ausschnitte
ausschnitte.apply_snapshot_to_detail(doc, detail, snap_id)
except Exception as ex:
print("[LAYOUTS] initial apply:", ex)
except Exception as ex:
print("[LAYOUTS] AddDetailView:", ex)
self._send_state()
def _delete_detail(self, page_id, detail_id):
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, page_id)
if page is None: return
detail = _find_detail_by_id(doc, page, detail_id)
if detail is None: return
try:
doc.Objects.Delete(detail.Id, True)
page.Redraw()
except Exception as ex:
print("[LAYOUTS] Delete detail:", ex)
self._send_state()
def _bind_ausschnitt(self, p):
"""Setzt die Binding und wendet den Ausschnitt sofort an (Snapshot-Mode)."""
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, p.get("pageId"))
if page is None: return
detail = _find_detail_by_id(doc, page, p.get("detailId"))
if detail is None: return
snap_id = p.get("ausschnittId") or None
_set_detail_binding(detail, snap_id)
if snap_id:
try:
import ausschnitte
ausschnitte.apply_snapshot_to_detail(doc, detail, snap_id)
except Exception as ex:
print("[LAYOUTS] apply on bind:", ex)
self._send_state()
def _sync_detail(self, page_id, detail_id):
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, page_id)
if page is None: return
detail = _find_detail_by_id(doc, page, detail_id)
if detail is None: return
snap_id = _get_detail_binding(detail)
if not snap_id:
print("[LAYOUTS] sync: kein Binding auf diesem Detail")
return
try:
import ausschnitte
ausschnitte.apply_snapshot_to_detail(doc, detail, snap_id)
except Exception as ex:
print("[LAYOUTS] sync:", ex)
self._send_state()
def _sync_layout(self, page_id):
"""Alle Details der Page mit ihren Bindings re-applien."""
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, page_id)
if page is None: return
try:
import ausschnitte
for d in page.GetDetailViews():
snap_id = _get_detail_binding(d)
if snap_id:
ausschnitte.apply_snapshot_to_detail(doc, d, snap_id)
page.Redraw()
except Exception as ex:
print("[LAYOUTS] sync layout:", ex)
self._send_state()
def _bridge_factory():
b = LayoutsBridge()
sc.sticky["layouts_bridge"] = b
return b
panel_base.register_and_open("layouts", "LAYOUTS", PANEL_GUID_STR,
_bridge_factory, icon_spec=("L", "#7a5fa8"))
+1096
View File
File diff suppressed because it is too large Load Diff
+513
View File
@@ -0,0 +1,513 @@
# ! python3
# -*- coding: utf-8 -*-
"""
oberleiste.py
OBERLEISTE-Panel: horizontale Top-Bar mit Architektur-Kontext-Controls.
Vereint View-Switcher, Display-Mode, Massstab, Print-View und Snap-Toggles.
Re-used massstab-Modul fuer Skala/PlotWeight-Logik die Bridge proxiet alle
Massstab-bezogenen Nachrichten dorthin.
"""
import os
import sys
import Rhino
import scriptcontext as sc
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
import panel_base
import massstab
import overrides
PANEL_GUID_STR = "7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51"
OVERRIDES_PANEL_GUID_STR = "8f2a7b6c-9f3a-4f4d-e6f7-08192a3c4d62"
def _run(cmd):
"""Hilfsfunktion: Rhino-Befehl ausfuehren, mit Logging."""
try:
Rhino.RhinoApp.RunScript(cmd, False)
except Exception as ex:
print("[OBERLEISTE] RunScript-Fehler ({}): {}".format(cmd, ex))
def _get_active_viewport_name():
try:
v = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView
return v.ActiveViewport.Name if v else None
except Exception:
return None
def _list_all_command_names():
"""Enumeriert alle registrierten Rhino-Commands (englische Namen).
Wird einmalig beim Bridge-Start aufgerufen und gecached."""
names = set()
# Variante 1: statische API Rhino.Commands.Command.GetCommandNames
try:
all_names = Rhino.Commands.Command.GetCommandNames(True, True)
for n in all_names:
if n and isinstance(n, str):
names.add(n)
except Exception:
pass
# Variante 2: ueber alle PlugIns iterieren (Fallback)
if not names:
try:
for guid in Rhino.Plugins.PlugIn.GetInstalledPlugIns().Keys:
try:
pi = Rhino.Plugins.PlugIn.Find(guid)
if pi is None: continue
cmds = pi.GetCommands() if hasattr(pi, "GetCommands") else []
for cmd_guid in cmds:
try:
n = Rhino.Commands.Command.GetCommandName(cmd_guid)
if n: names.add(n)
except Exception: pass
except Exception: pass
except Exception: pass
# Variante 3: minimaler Fallback fuer den Fall dass keine API greift
if not names:
for n in ("Line","Polyline","Rectangle","Circle","Arc","Curve","Text","Hatch",
"Move","Copy","Rotate","Scale","Mirror","Offset","Trim","Extend",
"Join","Explode","Fillet","Array","Box","ExtrudeCrv","BooleanUnion",
"BooleanDifference","BooleanIntersection","Cap","Section","Loft",
"Zoom","Pan","Top","Front","Right","Perspective","Undo","Redo",
"Group","Ungroup","Hide","Show","Delete","SelAll","SelNone",
"Properties","Layer","Snap","Ortho","Planar","Save","SaveAs"):
names.add(n)
out = sorted(names)
print("[OBERLEISTE] {} Rhino-Commands fuer Autocomplete enumeriert".format(len(out)))
return out
def _get_command_prompt():
"""Liefert den aktuellen Rhino-Command-Prompt oder leeren String.
Wird gepollt damit OBERLEISTE den Prompt + Optionen anzeigen kann."""
try:
p = Rhino.RhinoApp.CommandPrompt
return p if p is not None else ""
except Exception:
return ""
def _parse_command_options(prompt):
"""Extrahiert Option-Tokens aus einem Rhino-Prompt.
Beispiele:
"Line: First point ( BothSides Bisector Length Vertical Angle )"
"Polyline: Next point of polyline ( Close Helix Mode=Persistent Undo )"
Liefert Liste von dicts: [{name, value (optional), token}].
"""
import re
if not prompt: return []
# Inhalt der letzten Klammer
m = re.search(r"\(([^()]+)\)\s*$", prompt)
if not m: return []
body = m.group(1).strip()
options = []
for tok in body.split():
tok = tok.strip().rstrip(",;:")
if not tok: continue
if "=" in tok:
name, val = tok.split("=", 1)
options.append({"name": name, "value": val, "token": tok})
else:
options.append({"name": tok, "value": None, "token": tok})
return options
def _get_active_display_mode_name():
try:
v = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView
if v is None: return None
dm = v.ActiveViewport.DisplayMode
return dm.LocalName if dm else None
except Exception:
return None
_display_modes_cache = None # gecacht — Liste aendert sich pro Rhino-Session selten
def _list_display_modes():
"""Alle verfuegbaren Display-Modes (LocalName + Id-String).
Gecacht Liste aendert sich nur wenn User Display-Modes ergaenzt/loescht.
Bei Bedarf kann _display_modes_cache von aussen auf None gesetzt werden."""
global _display_modes_cache
if _display_modes_cache is not None:
return _display_modes_cache
out = []
try:
for dm in Rhino.Display.DisplayModeDescription.GetDisplayModes():
try:
out.append({"name": dm.LocalName, "id": str(dm.Id)})
except Exception:
continue
except Exception as ex:
print("[OBERLEISTE] _list_display_modes:", ex)
_display_modes_cache = out
return out
def _set_display_mode(name):
"""Setzt Display-Mode des aktiven Viewports per Name."""
try:
v = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView
if v is None: return False
for dm in Rhino.Display.DisplayModeDescription.GetDisplayModes():
if dm.LocalName == name or dm.EnglishName == name:
v.ActiveViewport.DisplayMode = dm
v.Redraw()
print("[OBERLEISTE] Display-Mode: {}".format(name))
return True
except Exception as ex:
print("[OBERLEISTE] _set_display_mode:", ex)
return False
# --- Snap / Ortho via ModelAidSettings --------------------------------------
def _get_snap_state():
try:
s = Rhino.ApplicationSettings.ModelAidSettings
return {
"ortho": bool(s.Ortho),
"gridSnap": bool(s.GridSnap),
"osnap": bool(s.UseHorizontalDialog) if False else bool(getattr(s, "Osnap", False)) or False,
"planar": bool(getattr(s, "ProjectOsnapsToCPlane", False)),
}
except Exception:
return {"ortho": False, "gridSnap": False, "osnap": False, "planar": False}
def _set_ortho(v):
try:
Rhino.ApplicationSettings.ModelAidSettings.Ortho = bool(v)
except Exception as ex:
print("[OBERLEISTE] _set_ortho:", ex)
def _set_grid_snap(v):
try:
Rhino.ApplicationSettings.ModelAidSettings.GridSnap = bool(v)
except Exception as ex:
print("[OBERLEISTE] _set_grid_snap:", ex)
def _set_osnap_master(v):
"""Master-Toggle fuer Object-Snap (alle aktiven Snaps)."""
try:
s = Rhino.ApplicationSettings.ModelAidSettings
if hasattr(s, "Osnap"):
s.Osnap = bool(v)
elif hasattr(s, "UsePoints"):
# Fallback: einzelne Modi durch
pass
except Exception as ex:
print("[OBERLEISTE] _set_osnap_master:", ex)
# --- Bridge -----------------------------------------------------------------
class OberleisteBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "oberleiste")
self._idle_counter = 0
self._last_prompt = ""
self._last_state_sig = None # Fingerprint des letzten Push — dedupe
self._cached_overrides = None # (enabled, count) — invalidiert bei Toggle/Update
# Command-Liste einmalig laden (kann teuer sein -> cachen)
try:
self._all_commands = _list_all_command_names()
except Exception as ex:
print("[OBERLEISTE] command-enum:", ex)
self._all_commands = []
def _on_ready(self):
# Bootstrap DPI (gemeinsam mit massstab.py)
try: massstab._bootstrap_dpi()
except Exception: pass
# WebView wurde (neu) gemountet — Frontend-State ist leer, also one-shot
# Listen (displayModes, allCommands) neu mitsenden. Sonst zeigt das
# Display-Dropdown nach einem Re-Mount (z.B. Andocken, Layout-Wechsel)
# nur die "—"-Option und wirkt wie ein toter Button.
self._dm_sent = False
self._commands_sent = False
self._send_state(force=True)
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
p = data.get("payload") or {}
if not isinstance(p, dict): p = {}
# --- Lifecycle --------------------------------------------------
if t == "READY":
self._on_ready()
elif t == "REQUEST_STATE":
self._send_state(force=True)
# --- Massstab (delegiert an massstab-Modul) ---------------------
elif t == "SET_SCALE":
doc, vp = massstab._active_vp()
try: ratio = float(p.get("ratio"))
except Exception: return
if ratio > 0 and massstab._apply_scale(doc, vp, ratio):
self._send_state(force=True)
elif t == "ZOOM_EXTENTS":
doc, vp = massstab._active_vp()
massstab._zoom_extents(doc, vp, selected_only=False)
self._send_state(force=True)
elif t == "ZOOM_SELECTION":
doc, vp = massstab._active_vp()
massstab._zoom_extents(doc, vp, selected_only=True)
self._send_state(force=True)
elif t == "SET_LINEWEIGHTS":
doc, _ = massstab._active_vp()
massstab._set_lineweights_enabled(doc, bool(p.get("enabled")))
self._send_state(force=True)
elif t == "SET_DPI":
doc, _ = massstab._active_vp()
massstab._set_dpi(doc, p.get("dpi"), source="manual")
self._send_state(force=True)
elif t == "DETECT_DPI":
massstab._force_redetect_dpi()
self._send_state(force=True)
# --- View-Switcher ----------------------------------------------
elif t == "SET_VIEW":
v = p.get("view")
if v in ("Top", "Front", "Right", "Perspective", "Left", "Back", "Bottom"):
_run("_-{} _Enter".format(v))
self._send_state(force=True)
# --- Display-Mode -----------------------------------------------
elif t == "SET_DISPLAY_MODE":
n = p.get("name")
if n:
_set_display_mode(n)
self._send_state(force=True)
# --- Snap-Toggles -----------------------------------------------
elif t == "TOGGLE_ORTHO":
_set_ortho(bool(p.get("enabled")))
self._send_state(force=True)
elif t == "TOGGLE_GRID_SNAP":
_set_grid_snap(bool(p.get("enabled")))
self._send_state(force=True)
elif t == "TOGGLE_OSNAP":
_set_osnap_master(bool(p.get("enabled")))
self._send_state(force=True)
# --- Graphical Overrides ----------------------------------------
elif t == "TOGGLE_OVERRIDES":
doc = Rhino.RhinoDoc.ActiveDoc
overrides.set_enabled(doc, bool(p.get("enabled")))
self._cached_overrides = None # Cache invalidieren
self._send_state(force=True)
elif t == "SET_OVERRIDES_PRESET":
doc = Rhino.RhinoDoc.ActiveDoc
name = p.get("name") or None
overrides.set_active_preset(doc, name)
self._cached_overrides = None # Cache invalidieren
self._send_state(force=True)
# OVERRIDES-Panel mit-informieren: dort haben sich die Rules
# geaendert (Preset wurde reingeladen).
try:
b = sc.sticky.get("overrides_bridge")
if b is not None:
b._send_state()
except Exception as ex:
print("[OBERLEISTE] notify overrides:", ex)
elif t == "SAVE_OVERRIDES_PRESET":
# Quick-Save direkt aus der Topbar: aktuelle Doc-Rules unter
# gegebenem Namen ablegen und sofort als activePreset markieren.
# Spart dem User den Umweg ueber den grossen OVERRIDES-Editor.
doc = Rhino.RhinoDoc.ActiveDoc
name = (p.get("name") or "").strip()
if not name:
pass
else:
cfg = overrides.load_config(doc)
rules = cfg.get("rules") or []
if overrides.save_preset(name, rules):
overrides.set_active_preset(doc, name)
self._cached_overrides = None
self._send_state(force=True)
try:
b = sc.sticky.get("overrides_bridge")
if b is not None:
b._send_state()
except Exception as ex:
print("[OBERLEISTE] notify overrides:", ex)
elif t == "OPEN_OVERRIDES_PANEL":
try:
import System
import Rhino.UI as RhinoUI
RhinoUI.Panels.OpenPanel(System.Guid(OVERRIDES_PANEL_GUID_STR))
except Exception as ex:
print("[OBERLEISTE] OpenPanel Overrides:", ex)
# --- Command-Line Integration -----------------------------------
elif t == "RUN_COMMAND":
cmd = (p.get("cmd") or "").strip()
if cmd:
# Auto-Praefix mit "_" falls nicht vorhanden, damit auch
# lokalisierte Rhino-Installationen die EN-Namen verstehen.
if not (cmd.startswith("_") or cmd.startswith("'")):
cmd = "_" + cmd
try:
# WICHTIG: Mac Rhinos Command-Bar sammelt parallel
# User-Keystrokes (globaler Keyhook). Wenn unsere React-
# Eingabe tippt landet die da auch. ESC clearen sonst
# haben wir doppelten Text und braucht 2x Enter.
Rhino.RhinoApp.SendKeystrokes("\x1b", False)
Rhino.RhinoApp.RunScript(cmd, False)
except Exception as ex:
print("[OBERLEISTE] RunScript-Fehler:", ex)
elif t == "SEND_KEYS":
text = p.get("text") or ""
append_enter = bool(p.get("enter", True))
try:
# Ebenfalls Buffer zuerst leeren wenn User parallel mitgetippt hat
if text:
Rhino.RhinoApp.SendKeystrokes("\x1b", False)
Rhino.RhinoApp.SendKeystrokes(text, append_enter)
except Exception as ex:
print("[OBERLEISTE] SendKeystrokes-Fehler:", ex)
elif t == "CANCEL_COMMAND":
try:
# Doppel-ESC: einmal um Eingabe-Buffer zu clearen, einmal um
# aktiven Befehl abzubrechen
Rhino.RhinoApp.SendKeystrokes("\x1b", False)
Rhino.RhinoApp.SendKeystrokes("\x1b", False)
except Exception:
pass
elif t == "TOGGLE_RHINO_CMD_LINE":
# Versucht Rhinos eigene Befehlszeile/History zu togglen.
# Mehrere Wege probieren — je nach Version greift einer.
for c in (
"_-CommandPrompt _Hide _Enter",
"_CommandHistory _Toggle _Enter",
"_-Toolbar _Hide _Commands _Enter",
):
try:
Rhino.RhinoApp.RunScript(c, False)
except Exception:
pass
def _send_state(self, force=False):
doc, vp = massstab._active_vp()
info = massstab._compute_scale(doc, vp)
# Massstab-State (Scale, Print-Toggle, DPI)
info["viewMode"] = _get_active_viewport_name()
info["displayMode"] = _get_active_display_mode_name()
# displayModes-Liste nur einmal initial mitsenden — aendert sich kaum
if not getattr(self, "_dm_sent", False):
info["displayModes"] = _list_display_modes()
self._dm_sent = True
# Snap-State
info.update(_get_snap_state())
# Overrides-State — cached, invalidiert bei TOGGLE_OVERRIDES und
# SET_OVERRIDES_PRESET. Bei manuellen Aenderungen via OVERRIDES-Panel
# bleibt der Cache stale bis zum naechsten Toggle — pragmatischer
# Trade-off, weil die beiden Bridges nicht direkt voneinander wissen.
if self._cached_overrides is None:
try:
cfg = overrides.load_config(doc)
presets = [item.get("name") for item in overrides.list_presets() if item.get("name")]
self._cached_overrides = (
bool(cfg.get("enabled")),
len(cfg.get("rules") or []),
cfg.get("activePreset"),
tuple(presets),
)
except Exception:
self._cached_overrides = (False, 0, None, ())
(info["overridesEnabled"],
info["overridesCount"],
info["overridesActivePreset"],
_presets_tuple) = self._cached_overrides
info["overridesPresets"] = list(_presets_tuple)
# Command-Line State
prompt = _get_command_prompt()
info["cmdPrompt"] = prompt
info["cmdOptions"] = _parse_command_options(prompt)
# Command-Autocomplete-Liste — nur einmal initial schicken (gross)
if not getattr(self, "_commands_sent", False):
info["allCommands"] = self._all_commands
self._commands_sent = True
force = True # Erste Push immer feuern
# Diff-Check: wenn weder Daten noch force, gar nichts schicken
# (dedupe Idle-Ticks ohne Aenderung — spart WebView-ExecuteScript Roundtrip)
sig = (
info.get("scale"),
info.get("appliedScale"),
info.get("parallel"),
info.get("viewMode"),
info.get("displayMode"),
info.get("ortho"), info.get("gridSnap"), info.get("osnap"),
info.get("showLineweights"),
info["overridesEnabled"], info["overridesCount"],
info.get("overridesActivePreset"),
tuple(info.get("overridesPresets") or ()),
prompt,
)
if not force and sig == self._last_state_sig:
return
self._last_state_sig = sig
self.send("STATE", info)
def tick_idle(self):
# Command-Prompt aendert sich oft schnell -> separater Pfad: wenn sich
# der Prompt seit letztem Tick geaendert hat, sofort pushen (ungedrosselt).
cur_prompt = _get_command_prompt()
if cur_prompt != self._last_prompt:
self._last_prompt = cur_prompt
self._send_state(force=True)
self._idle_counter = 0
return
# Sonst: normaler throttle fuer den restlichen State
self._idle_counter += 1
if self._idle_counter < massstab._IDLE_THROTTLE:
return
self._idle_counter = 0
self._send_state(force=False)
# --- Listener-Hookup --------------------------------------------------------
def _install_listeners(bridge):
flag = "oberleiste_listeners"
sc.sticky["oberleiste_bridge"] = bridge
if sc.sticky.get(flag):
return
def on_idle(s, e):
b = sc.sticky.get("oberleiste_bridge")
if b is not None:
try: b.tick_idle()
except Exception: pass
def on_view_change(*args):
b = sc.sticky.get("oberleiste_bridge")
if b is not None:
try: b._send_state(force=True)
except Exception: pass
Rhino.RhinoApp.Idle += on_idle
Rhino.RhinoDoc.ActiveDocumentChanged += on_view_change
sc.sticky[flag] = True
print("[OBERLEISTE] Listener aktiv")
def _bridge_factory():
b = OberleisteBridge()
_install_listeners(b)
return b
panel_base.register_and_open("oberleiste", "OBERLEISTE", PANEL_GUID_STR, _bridge_factory,
icon_spec=("O", "#2f5d54"))
+718
View File
@@ -0,0 +1,718 @@
# ! python3
# -*- coding: utf-8 -*-
"""
overrides.py
Engine fuer regelbasierte grafische Overrides (ArchiCAD Graphical Overrides /
Vectorworks Datenvisualisierung).
Datenmodell (gespeichert als JSON in doc.Strings["dossier_overrides"]):
{
"enabled": true,
"rules": [
{
"id": "rule_abc",
"name": "Bestand grau",
"enabled": true,
"condition": {
"type": "layer_name" | "user_string" | "object_name",
"operator": "equals" | "contains" | "starts_with" | "not_equals",
"value": "WAND_BESTAND",
"key": "brandschutz" # nur fuer user_string
},
"actions": {
"color": "#888888" or null,
"lineweight": 0.25 or null,
"linetype": "Dashed" or null
}
},
... (oberste Regel hat hoechste Prioritaet)
]
}
Verhalten:
- Mehrere Regeln matchen additiv: Actions aller passenden Regeln werden
kombiniert. Bei Konflikt fuer die selbe Property gewinnt die in der
Liste WEITER OBEN stehende Regel.
- Originalwerte werden in UserStrings pro Objekt gesichert -> reversibel.
- Engine wird via apply_all(doc) / restore_all(doc) gesteuert.
"""
import os
import sys
import json
import Rhino
import System
import System.Drawing as Drawing
import scriptcontext as sc
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
_STORE_KEY = "dossier_overrides"
# Globale Presets (cross-doc) — Datei im User-Home
_PRESETS_DIR = os.path.expanduser("~/Library/Application Support/RhinoPanel")
_PRESETS_PATH = os.path.join(_PRESETS_DIR, "override_presets.json")
# UserString-Keys fuer Original-Backups (pro Objekt)
_ORIG_COLOR_SRC = "dossier_or_csrc"
_ORIG_COLOR = "dossier_or_color"
_ORIG_LW_SRC = "dossier_or_lwsrc"
_ORIG_LW = "dossier_or_lw"
_ORIG_LT_SRC = "dossier_or_ltsrc"
_ORIG_LT = "dossier_or_lt"
_OVERRIDDEN = "dossier_or_done" # "1" wenn Object aktuell overridden ist
# Hatch-Override: Originalwerte werden auf dem Hatch-Objekt selbst gespeichert.
# Link Curve -> Hatch nutzt den FILL_KEY den gestaltung.py setzt.
_GEST_FILL_KEY = "ebenen_fill_hatch_id" # auf Curve
_ORIG_HP = "dossier_or_hatch_pidx" # auf Hatch — original PatternIndex
_ORIG_HS = "dossier_or_hatch_scale" # auf Hatch — original PatternScale
_HATCH_OVERRIDDEN = "dossier_or_hatch_done" # "1" wenn Hatch aktuell overridden
_FROM_LAYER = Rhino.DocObjects.ObjectColorSource.ColorFromLayer
_FROM_OBJECT = Rhino.DocObjects.ObjectColorSource.ColorFromObject
_LW_FROM_LAY = Rhino.DocObjects.ObjectPlotWeightSource.PlotWeightFromLayer
_LW_FROM_OBJ = Rhino.DocObjects.ObjectPlotWeightSource.PlotWeightFromObject
_LT_FROM_LAY = Rhino.DocObjects.ObjectLinetypeSource.LinetypeFromLayer
_LT_FROM_OBJ = Rhino.DocObjects.ObjectLinetypeSource.LinetypeFromObject
# --- Daten lesen/schreiben --------------------------------------------------
def load_config(doc):
if doc is None:
return {"enabled": False, "rules": []}
try:
raw = doc.Strings.GetValue(_STORE_KEY)
if not raw:
return {"enabled": False, "rules": []}
data = json.loads(raw)
if not isinstance(data, dict):
return {"enabled": False, "rules": []}
data.setdefault("enabled", False)
data.setdefault("rules", [])
return data
except Exception as ex:
print("[OVERRIDES] load_config:", ex)
return {"enabled": False, "rules": []}
def save_config(doc, cfg):
if doc is None: return
try:
doc.Strings.SetString(_STORE_KEY, json.dumps(cfg, ensure_ascii=False))
except Exception as ex:
print("[OVERRIDES] save_config:", ex)
# --- Presets (global, cross-doc) -------------------------------------------
def _read_presets_file():
try:
if os.path.isfile(_PRESETS_PATH):
with open(_PRESETS_PATH, "rb") as f:
data = json.loads(f.read().decode("utf-8"))
if isinstance(data, list):
return data
# Migration: alte dict-Form -> list
if isinstance(data, dict) and "presets" in data:
return data.get("presets") or []
except Exception as ex:
print("[OVERRIDES] read_presets:", ex)
return []
def _write_presets_file(presets):
try:
if not os.path.isdir(_PRESETS_DIR):
os.makedirs(_PRESETS_DIR)
with open(_PRESETS_PATH, "wb") as f:
f.write(json.dumps(presets or [], ensure_ascii=False, indent=2).encode("utf-8"))
return True
except Exception as ex:
print("[OVERRIDES] write_presets:", ex)
return False
def list_presets():
"""Liefert Liste von {name, ruleCount}."""
out = []
for p in _read_presets_file():
if not isinstance(p, dict): continue
out.append({
"name": p.get("name", "(ohne Name)"),
"ruleCount": len(p.get("rules") or []),
})
return out
def save_preset(name, rules):
"""Speichert/ueberschreibt Preset mit gegebenem Namen."""
if not name or not isinstance(name, str): return False
name = name.strip()
if not name: return False
presets = _read_presets_file()
# Existierendes Preset mit gleichem Namen ersetzen
for i, p in enumerate(presets):
if isinstance(p, dict) and p.get("name") == name:
presets[i] = {"name": name, "rules": rules or []}
return _write_presets_file(presets)
# Sonst anhaengen
presets.append({"name": name, "rules": rules or []})
return _write_presets_file(presets)
def load_preset(name):
"""Liefert die Rules-Liste eines Presets oder None."""
for p in _read_presets_file():
if isinstance(p, dict) and p.get("name") == name:
# Deep-Copy via JSON damit der Aufrufer keine Datei-Daten teilt
return json.loads(json.dumps(p.get("rules") or []))
return None
def delete_preset(name):
presets = _read_presets_file()
new = [p for p in presets if not (isinstance(p, dict) and p.get("name") == name)]
if len(new) == len(presets): return False
return _write_presets_file(new)
def set_active_preset(doc, name):
"""Aktiviert ein gespeichertes Preset: kopiert dessen Rules ins Doc-Config
und markiert es als activePreset. Wenn name leer/None: aktives Preset
geclear-t, Rules bleiben unveraendert (User waehlt "kein Preset"). Bei
aktivem enabled-Flag wird sofort neu angewendet. True bei Erfolg."""
if doc is None: return False
cfg = load_config(doc)
if name:
rules = load_preset(name)
if rules is None:
return False
cfg["rules"] = rules
cfg["activePreset"] = name
else:
cfg["activePreset"] = None
save_config(doc, cfg)
if cfg.get("enabled"):
# Erst restore (alte Overrides zuruecknehmen), dann apply mit neuen Rules.
restore_all(doc)
apply_all(doc)
return True
def get_active_preset(doc):
"""Aktuell aktives Preset-Namen oder None."""
if doc is None: return None
return load_config(doc).get("activePreset")
# --- Helpers ----------------------------------------------------------------
def _color_to_hex(c):
if c is None: return None
try:
return "#{:02x}{:02x}{:02x}".format(int(c.R), int(c.G), int(c.B))
except Exception:
return None
def _hex_to_color(h):
if not isinstance(h, str): return Drawing.Color.FromArgb(136, 136, 136)
h = h.strip()
if h.startswith("#"): h = h[1:]
if len(h) != 6:
return Drawing.Color.FromArgb(136, 136, 136)
try:
return Drawing.Color.FromArgb(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
except Exception:
return Drawing.Color.FromArgb(136, 136, 136)
def _layer_name_for(doc, obj):
try:
idx = obj.Attributes.LayerIndex
if 0 <= idx < doc.Layers.Count:
return doc.Layers[idx].Name or ""
except Exception:
pass
return ""
def _layer_full_path_for(doc, obj):
try:
idx = obj.Attributes.LayerIndex
if 0 <= idx < doc.Layers.Count:
return doc.Layers[idx].FullPath or ""
except Exception:
pass
return ""
def _user_string_for(obj, key):
try:
v = obj.Attributes.GetUserString(key)
return v if v is not None else ""
except Exception:
return ""
def _object_name_for(obj):
try:
return obj.Attributes.Name or ""
except Exception:
return ""
def _compare(actual, op, expected):
actual = actual if actual is not None else ""
expected = expected if expected is not None else ""
if op == "equals": return str(actual) == str(expected)
if op == "not_equals": return str(actual) != str(expected)
if op == "contains": return str(expected) in str(actual)
if op == "starts_with": return str(actual).startswith(str(expected))
if op == "ends_with": return str(actual).endswith(str(expected))
return False
def _match_leaf(doc, obj, condition):
"""Evaluates a single leaf condition (layer_name / user_string / object_name)."""
if not isinstance(condition, dict): return False
t = condition.get("type")
op = condition.get("operator") or "equals"
v = condition.get("value")
if t == "layer_name":
return _compare(_layer_name_for(doc, obj), op, v) or \
_compare(_layer_full_path_for(doc, obj), op, v)
if t == "user_string":
return _compare(_user_string_for(obj, condition.get("key", "")), op, v)
if t == "object_name":
return _compare(_object_name_for(obj), op, v)
return False
def _match_rule(doc, obj, rule):
"""Evaluates rule. Unterstuetzt zwei Formate:
- Legacy: rule.condition = {single leaf}
- Neu: rule.conditions = [leaf, leaf, ...] + rule.conditionsLogic = "and" | "or"
"""
# Neue Form (Liste)
conds = rule.get("conditions")
if isinstance(conds, list) and conds:
logic = (rule.get("conditionsLogic") or "and").lower()
if logic == "or":
for c in conds:
if _match_leaf(doc, obj, c):
return True
return False
# default: and
for c in conds:
if not _match_leaf(doc, obj, c):
return False
return True
# Legacy single condition
return _match_leaf(doc, obj, rule.get("condition") or {})
# --- Apply / Restore --------------------------------------------------------
def _backup_original(attrs):
"""Sichert originale Attribute in UserStrings (nur beim ersten Mal)."""
if attrs.GetUserString(_OVERRIDDEN) == "1":
return # bereits gesichert
try:
attrs.SetUserString(_ORIG_COLOR_SRC, str(int(attrs.ColorSource)))
c_hex = _color_to_hex(attrs.ObjectColor) or "#888888"
attrs.SetUserString(_ORIG_COLOR, c_hex)
attrs.SetUserString(_ORIG_LW_SRC, str(int(attrs.PlotWeightSource)))
attrs.SetUserString(_ORIG_LW, "{:.6f}".format(float(attrs.PlotWeight or 0)))
attrs.SetUserString(_ORIG_LT_SRC, str(int(attrs.LinetypeSource)))
attrs.SetUserString(_ORIG_LT, str(int(attrs.LinetypeIndex)))
attrs.SetUserString(_OVERRIDDEN, "1")
except Exception as ex:
print("[OVERRIDES] _backup_original:", ex)
def _restore_original(doc, obj):
"""Stellt urspruengliche Attribute aus UserStrings wieder her.
Beinhaltet auch das Restoring eines ggf. ueberschriebenen Hatches."""
a = obj.Attributes
# Hatch separat zuruecksetzen — kann auch ohne Curve-Override
# passiert sein (z.B. wenn Override nur den Pattern aendert)
_restore_hatch(doc, obj)
if a.GetUserString(_OVERRIDDEN) != "1":
return False
try:
new_a = a.Duplicate()
cs = a.GetUserString(_ORIG_COLOR_SRC)
if cs:
new_a.ColorSource = Rhino.DocObjects.ObjectColorSource(int(cs))
c = a.GetUserString(_ORIG_COLOR)
if c:
new_a.ObjectColor = _hex_to_color(c)
lws = a.GetUserString(_ORIG_LW_SRC)
if lws:
new_a.PlotWeightSource = Rhino.DocObjects.ObjectPlotWeightSource(int(lws))
lw = a.GetUserString(_ORIG_LW)
if lw:
try: new_a.PlotWeight = float(lw)
except Exception: pass
lts = a.GetUserString(_ORIG_LT_SRC)
if lts:
new_a.LinetypeSource = Rhino.DocObjects.ObjectLinetypeSource(int(lts))
lt = a.GetUserString(_ORIG_LT)
if lt:
try: new_a.LinetypeIndex = int(lt)
except Exception: pass
# Backup-Marker entfernen
for k in (_ORIG_COLOR_SRC, _ORIG_COLOR, _ORIG_LW_SRC, _ORIG_LW,
_ORIG_LT_SRC, _ORIG_LT, _OVERRIDDEN):
new_a.SetUserString(k, "")
doc.Objects.ModifyAttributes(obj, new_a, True)
return True
except Exception as ex:
print("[OVERRIDES] _restore_original:", ex)
return False
def _compose_overrides(doc, obj, rules):
"""Sammelt Actions aller matchenden Regeln. Bei Konflikten gewinnt die
Regel die WEITER OBEN in der Liste steht (= niedrigerer Index)."""
composed = {}
for rule in rules:
if not rule.get("enabled", True): continue
if not _match_rule(doc, obj, rule): continue
for prop, val in (rule.get("actions") or {}).items():
if val is None or val == "": continue
if prop not in composed:
composed[prop] = val
return composed
def _find_linked_hatch(doc, curve_obj):
"""Findet den via gestaltung verlinkten Hatch zur Curve (oder None)."""
try:
hid_s = curve_obj.Attributes.GetUserString(_GEST_FILL_KEY)
if not hid_s: return None
h = doc.Objects.FindId(System.Guid(hid_s))
if h is None or h.IsDeleted: return None
return h
except Exception:
return None
def _apply_hatch_override(doc, curve_obj, pattern_name, scale_val):
"""Modifiziert den verlinkten Hatch der Curve. Original wird auf dem
Hatch in UserStrings gesichert. Liefert True bei Aenderung.
Wenn keine Hatch existiert: stiller No-op (User soll erst via Gestaltung
eine Basis-Hatch anlegen Overrides modifizieren, erzeugen nicht)."""
h = _find_linked_hatch(doc, curve_obj)
if h is None: return False
try:
hg = h.Geometry
ha = h.Attributes
# Backup einmalig sichern
if ha.GetUserString(_HATCH_OVERRIDDEN) != "1":
try:
ha.SetUserString(_ORIG_HP, str(int(hg.PatternIndex)))
ha.SetUserString(_ORIG_HS, "{:.6f}".format(float(hg.PatternScale)))
ha.SetUserString(_HATCH_OVERRIDDEN, "1")
doc.Objects.ModifyAttributes(h, ha, True)
except Exception as ex:
print("[OVERRIDES] hatch backup:", ex)
# Pattern wechseln (Geometrie neu erzeugen — PatternIndex ist read-only)
new_pidx = hg.PatternIndex
if pattern_name:
try:
idx = doc.HatchPatterns.Find(pattern_name, True)
if idx >= 0:
new_pidx = idx
except Exception: pass
new_scale = float(scale_val) if scale_val else float(hg.PatternScale)
try:
# Hatch-Geometrie neu instanzieren (PatternIndex/Scale aendern direkt)
new_hg = hg.Duplicate()
try:
new_hg.PatternIndex = new_pidx
except Exception: pass
try:
new_hg.PatternScale = new_scale
except Exception: pass
doc.Objects.Replace(h.Id, new_hg)
return True
except Exception as ex:
print("[OVERRIDES] hatch replace:", ex)
return False
except Exception as ex:
print("[OVERRIDES] _apply_hatch_override:", ex)
return False
def _restore_hatch(doc, curve_obj):
"""Stellt Hatch-Pattern und -Scale aus dem Backup wieder her."""
h = _find_linked_hatch(doc, curve_obj)
if h is None: return False
a = h.Attributes
if a.GetUserString(_HATCH_OVERRIDDEN) != "1": return False
try:
orig_pidx_s = a.GetUserString(_ORIG_HP)
orig_scale_s = a.GetUserString(_ORIG_HS)
hg = h.Geometry.Duplicate()
if orig_pidx_s:
try: hg.PatternIndex = int(orig_pidx_s)
except Exception: pass
if orig_scale_s:
try: hg.PatternScale = float(orig_scale_s)
except Exception: pass
doc.Objects.Replace(h.Id, hg)
# Backup-Marker entfernen
h2 = doc.Objects.FindId(h.Id)
if h2 is not None:
new_a = h2.Attributes.Duplicate()
for k in (_ORIG_HP, _ORIG_HS, _HATCH_OVERRIDDEN):
new_a.SetUserString(k, "")
doc.Objects.ModifyAttributes(h2, new_a, True)
return True
except Exception as ex:
print("[OVERRIDES] _restore_hatch:", ex)
return False
def _apply_to_object(doc, obj, overrides):
"""Setzt die Override-Werte am Objekt. Sichert vorher Originale."""
if not overrides: return False
a = obj.Attributes
_backup_original(a)
new_a = a.Duplicate()
changed = False
if "color" in overrides:
col = _hex_to_color(overrides["color"])
new_a.ColorSource = _FROM_OBJECT
new_a.ObjectColor = col
# Plot-Color mitspiegeln (sonst druckt's wieder in Layerfarbe)
try:
new_a.PlotColorSource = Rhino.DocObjects.ObjectPlotColorSource.PlotColorFromObject
new_a.PlotColor = col
except Exception: pass
changed = True
if "lineweight" in overrides:
try:
new_a.PlotWeightSource = _LW_FROM_OBJ
new_a.PlotWeight = float(overrides["lineweight"])
changed = True
except Exception: pass
if "linetype" in overrides:
try:
idx = doc.Linetypes.Find(overrides["linetype"], True)
if idx >= 0:
new_a.LinetypeSource = _LT_FROM_OBJ
new_a.LinetypeIndex = idx
changed = True
except Exception: pass
if changed:
try:
doc.Objects.ModifyAttributes(obj, new_a, True)
except Exception as ex:
print("[OVERRIDES] apply ModifyAttributes:", ex)
# Hatch-Override (separater Pfad, modifiziert das verlinkte Hatch)
if "hatchPattern" in overrides or "hatchScale" in overrides:
if _apply_hatch_override(doc, obj,
overrides.get("hatchPattern"),
overrides.get("hatchScale")):
changed = True
return changed
def apply_all(doc):
"""Wendet alle aktiven Regeln auf alle Objekte im Doc an.
Objekte die NICHT (mehr) matchen werden auf Originale zurueckgesetzt."""
if doc is None: return 0, 0
cfg = load_config(doc)
if not cfg.get("enabled"): return 0, 0
rules = cfg.get("rules") or []
if not rules: return 0, 0
n_applied = 0
n_restored = 0
_set_applying(True)
try:
for obj in doc.Objects:
if obj is None or obj.IsDeleted: continue
ovs = _compose_overrides(doc, obj, rules)
if ovs:
if _apply_to_object(doc, obj, ovs):
n_applied += 1
else:
# Kein Match aber war evtl. vorher overridden -> restore
if obj.Attributes.GetUserString(_OVERRIDDEN) == "1":
if _restore_original(doc, obj):
n_restored += 1
try: doc.Views.Redraw()
except Exception: pass
print("[OVERRIDES] apply_all: {} applied, {} restored".format(n_applied, n_restored))
except Exception as ex:
print("[OVERRIDES] apply_all:", ex)
finally:
_set_applying(False)
return n_applied, n_restored
def restore_all(doc):
"""Stellt alle Originale wieder her (Overrides aus)."""
if doc is None: return 0
n = 0
_set_applying(True)
try:
for obj in doc.Objects:
if obj is None or obj.IsDeleted: continue
had_attr_override = (obj.Attributes.GetUserString(_OVERRIDDEN) == "1")
# _restore_original kuemmert sich auch um den verlinkten Hatch —
# auch wenn die Curve selbst keinen Attribut-Override hatte.
if had_attr_override:
if _restore_original(doc, obj):
n += 1
else:
# Vielleicht nur Hatch-Override
if _restore_hatch(doc, obj):
n += 1
try: doc.Views.Redraw()
except Exception: pass
print("[OVERRIDES] restore_all: {} Objekte".format(n))
except Exception as ex:
print("[OVERRIDES] restore_all:", ex)
finally:
_set_applying(False)
return n
def set_enabled(doc, enabled):
"""Master-Toggle: an -> apply_all, aus -> restore_all + Config-Flag setzen."""
cfg = load_config(doc)
cfg["enabled"] = bool(enabled)
save_config(doc, cfg)
if enabled:
apply_all(doc)
else:
restore_all(doc)
def update_rules(doc, rules, enabled=None):
"""Schreibt eine neue Regel-Liste. Wenn enabled vorher an war, wird
nach dem Speichern apply_all (mit Restore-cleanup) ausgefuehrt.
Manuelle Aenderungen an den Rules clearen den activePreset sonst
behauptet das Topbar-Dropdown weiter, das alte Preset sei aktiv obwohl
die Rules davon driften (Variante C: Preset ist read-only Snapshot)."""
cfg = load_config(doc)
if enabled is not None:
cfg["enabled"] = bool(enabled)
cfg["rules"] = rules or []
cfg["activePreset"] = None
save_config(doc, cfg)
if cfg.get("enabled"):
# Erst alles zuruecksetzen, dann neu anwenden — sonst koennten alte
# Overrides "kleben" wenn die neue Regelmenge sie nicht mehr enthaelt.
restore_all(doc)
apply_all(doc)
# --- Live-Update via Doc-Events --------------------------------------------
def _is_applying():
return bool(sc.sticky.get("overrides_applying"))
def _set_applying(v):
sc.sticky["overrides_applying"] = bool(v)
def _apply_to_single_object(doc, obj):
"""Re-evaluate Overrides fuer ein einzelnes Objekt. Aufgerufen von den
Event-Handlern bei neu/geaenderten Objekten."""
if doc is None or obj is None: return
cfg = load_config(doc)
if not cfg.get("enabled"): return
rules = cfg.get("rules") or []
if not rules:
# Engine aus oder keine Regeln -> wenn vorher overridden, restore
try:
if obj.Attributes.GetUserString(_OVERRIDDEN) == "1":
_restore_original(doc, obj)
except Exception: pass
return
try:
ovs = _compose_overrides(doc, obj, rules)
if ovs:
_apply_to_object(doc, obj, ovs)
elif obj.Attributes.GetUserString(_OVERRIDDEN) == "1":
_restore_original(doc, obj)
except Exception as ex:
print("[OVERRIDES] live single-apply:", ex)
def install_listeners():
"""Hookt einmalig Rhino-Events fuer Live-Update.
Idempotent via sticky-Flag."""
if sc.sticky.get("overrides_listeners"):
return
def on_add(s, e):
if _is_applying(): return
try:
doc = getattr(e, "TheDoc", None) or Rhino.RhinoDoc.ActiveDoc
obj = getattr(e, "TheObject", None)
if not obj or not doc: return
_set_applying(True)
try:
_apply_to_single_object(doc, obj)
finally:
_set_applying(False)
except Exception as ex:
print("[OVERRIDES] on_add:", ex)
_set_applying(False)
def on_replace(s, e):
# Wird auch von ModifyAttributes gefeuert -> Guard
if _is_applying(): return
try:
doc = getattr(e, "TheDoc", None) or Rhino.RhinoDoc.ActiveDoc
obj = getattr(e, "NewRhinoObject", None) or getattr(e, "TheObject", None)
if not obj or not doc: return
_set_applying(True)
try:
_apply_to_single_object(doc, obj)
finally:
_set_applying(False)
except Exception as ex:
print("[OVERRIDES] on_replace:", ex)
_set_applying(False)
def on_layer_table(s, e):
# Layer geaendert (Name, Properties, ...) — Regeln mit layer_name
# koennten andere Matches haben. Vollstaendiges Reapply.
if _is_applying(): return
try:
doc = getattr(e, "Document", None) or Rhino.RhinoDoc.ActiveDoc
cfg = load_config(doc)
if not cfg.get("enabled"): return
_set_applying(True)
try:
restore_all(doc)
apply_all(doc)
finally:
_set_applying(False)
except Exception as ex:
print("[OVERRIDES] on_layer_table:", ex)
_set_applying(False)
try:
Rhino.RhinoDoc.AddRhinoObject += on_add
Rhino.RhinoDoc.ReplaceRhinoObject += on_replace
Rhino.RhinoDoc.LayerTableEvent += on_layer_table
except Exception as ex:
print("[OVERRIDES] install_listeners:", ex)
return
sc.sticky["overrides_listeners"] = True
print("[OVERRIDES] Live-Update Listener aktiv (Add/Replace/LayerTable)")
+226
View File
@@ -0,0 +1,226 @@
# ! python3
# -*- coding: utf-8 -*-
"""
overrides_panel.py
OVERRIDES-Panel: Rule-Editor fuer grafische Overrides.
Liest/schreibt rhino/overrides.py-Engine.
"""
import os
import sys
import uuid
import Rhino
import scriptcontext as sc
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
import panel_base
import overrides
PANEL_GUID_STR = "8f2a7b6c-9f3a-4f4d-e6f7-08192a3c4d62"
def _list_layer_names(doc):
out = []
try:
for layer in doc.Layers:
if layer is None or layer.IsDeleted: continue
try:
out.append({"name": layer.Name, "fullPath": layer.FullPath or layer.Name})
except Exception:
pass
except Exception:
pass
return out
def _list_linetypes(doc):
out = []
try:
for lt in doc.Linetypes:
try:
if lt.Name and not lt.IsDeleted:
out.append(lt.Name)
except Exception:
pass
except Exception:
pass
return out
def _list_hatch_patterns(doc):
out = []
try:
for i in range(doc.HatchPatterns.Count):
try:
hp = doc.HatchPatterns[i]
if hp and hp.Name:
out.append(hp.Name)
except Exception:
pass
except Exception:
pass
return out
def _payload(doc):
cfg = overrides.load_config(doc)
return {
"enabled": bool(cfg.get("enabled")),
"rules": cfg.get("rules") or [],
"layers": _list_layer_names(doc),
"linetypes": _list_linetypes(doc),
"hatchPatterns": _list_hatch_patterns(doc),
"presets": overrides.list_presets(),
"activePreset": cfg.get("activePreset"),
}
class OverridesBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "overrides")
def _on_ready(self):
self._send_state()
def _send_state(self):
doc = Rhino.RhinoDoc.ActiveDoc
self.send("STATE", _payload(doc))
# Oberleiste mit-informieren: deren Topbar zeigt
# Toggle + Preset-Dropdown, das vom selben State abhaengt.
# Cache invalidieren, dann force-send, sonst sieht die Topbar
# neue Rules/Presets erst beim naechsten Toggle.
try:
b = sc.sticky.get("oberleiste_bridge")
if b is not None:
b._cached_overrides = None
b._send_state(force=True)
except Exception as ex:
print("[OVERRIDES] notify oberleiste:", ex)
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
p = data.get("payload") or {}
if not isinstance(p, dict): p = {}
doc = Rhino.RhinoDoc.ActiveDoc
if t == "READY" or t == "REQUEST_STATE":
self._on_ready()
elif t == "SET_ENABLED":
overrides.set_enabled(doc, bool(p.get("enabled")))
self._send_state()
elif t == "ADD_RULE":
cfg = overrides.load_config(doc)
rule = p.get("rule") or {}
if not rule.get("id"):
rule["id"] = "rule_" + uuid.uuid4().hex[:8]
rule.setdefault("name", "Neue Regel")
rule.setdefault("enabled", True)
rule.setdefault("condition", {"type": "layer_name", "operator": "equals", "value": ""})
rule.setdefault("actions", {})
# Neue Regel oben einfuegen (= hoechste Prioritaet)
rules = cfg.get("rules") or []
rules.insert(0, rule)
overrides.update_rules(doc, rules, cfg.get("enabled"))
self._send_state()
elif t == "UPDATE_RULE":
cfg = overrides.load_config(doc)
rid = p.get("id")
patch = p.get("rule") or {}
rules = cfg.get("rules") or []
for i, r in enumerate(rules):
if r.get("id") == rid:
rules[i] = patch
break
overrides.update_rules(doc, rules, cfg.get("enabled"))
self._send_state()
elif t == "DELETE_RULE":
cfg = overrides.load_config(doc)
rid = p.get("id")
rules = [r for r in (cfg.get("rules") or []) if r.get("id") != rid]
overrides.update_rules(doc, rules, cfg.get("enabled"))
self._send_state()
elif t == "REORDER_RULES":
cfg = overrides.load_config(doc)
order = p.get("order") or []
by_id = {r.get("id"): r for r in (cfg.get("rules") or [])}
new_rules = [by_id[i] for i in order if i in by_id]
# Verbleibende (falls Liste inkonsistent) hinten anhaengen
for r in (cfg.get("rules") or []):
if r not in new_rules:
new_rules.append(r)
overrides.update_rules(doc, new_rules, cfg.get("enabled"))
self._send_state()
elif t == "DUPLICATE_RULE":
cfg = overrides.load_config(doc)
rid = p.get("id")
rules = cfg.get("rules") or []
for i, r in enumerate(rules):
if r.get("id") == rid:
import json
clone = json.loads(json.dumps(r, ensure_ascii=False))
clone["id"] = "rule_" + uuid.uuid4().hex[:8]
clone["name"] = (r.get("name", "Regel") + " Kopie")
rules.insert(i + 1, clone)
break
overrides.update_rules(doc, rules, cfg.get("enabled"))
self._send_state()
elif t == "REAPPLY":
if overrides.load_config(doc).get("enabled"):
overrides.restore_all(doc)
overrides.apply_all(doc)
self._send_state()
elif t == "CLEAR_RULES":
# Alle Regeln entfernen und activePreset clearen — wird vom
# Topbar/Kombination-Dropdown beim Wechsel auf "— neu / keine —"
# gefeuert, damit der Editor wirklich leer ist.
cfg = overrides.load_config(doc)
overrides.update_rules(doc, [], cfg.get("enabled"))
self._send_state()
# --- Presets (cross-doc) ----------------------------------------
elif t == "SAVE_PRESET":
name = (p.get("name") or "").strip()
if name:
cfg = overrides.load_config(doc)
overrides.save_preset(name, cfg.get("rules") or [])
self._send_state()
elif t == "LOAD_PRESET":
name = (p.get("name") or "").strip()
mode = p.get("mode") or "replace" # 'replace' oder 'append'
if mode == "replace":
# set_active_preset macht alles richtig: Rules ersetzen,
# activePreset = name, ggf. neu anwenden.
overrides.set_active_preset(doc, name)
else:
# Append-Mode: bestehende + Preset-Rules. activePreset wird
# in update_rules auf None gesetzt — passt, weil's eine
# Mischung ist, kein einzelnes Preset mehr.
rules = overrides.load_preset(name)
if rules is not None:
cfg = overrides.load_config(doc)
new_rules = list(cfg.get("rules") or []) + list(rules)
overrides.update_rules(doc, new_rules, cfg.get("enabled"))
self._send_state()
elif t == "DELETE_PRESET":
name = (p.get("name") or "").strip()
if name:
overrides.delete_preset(name)
self._send_state()
def _bridge_factory():
b = OverridesBridge()
try: overrides.install_listeners()
except Exception as ex: print("[OVERRIDES] install_listeners:", ex)
# Bridge im sticky ablegen, damit andere Panels (z.B. Oberleiste) sie
# bei Cross-Panel-Updates erreichen koennen.
sc.sticky["overrides_bridge"] = b
return b
panel_base.register_and_open("overrides", "OVERRIDES", PANEL_GUID_STR, _bridge_factory,
icon_spec=("V", "#b5621e"),
min_size=(720, 560))
+499
View File
@@ -0,0 +1,499 @@
# ! python3
# -*- coding: utf-8 -*-
"""
panel_base.py
Geteilte Infrastruktur fuer dockbare Rhino-Panels mit React-WebView.
Wird von rhinopanel.py (EBENEN) und gestaltung.py (GESTALTUNG) verwendet.
"""
import os
import re
import json
import Rhino
import Rhino.UI as RhinoUI
import Eto.Forms as forms
import Eto.Drawing as drawing
import System
import scriptcontext as sc
_HERE = os.path.dirname(os.path.abspath(__file__))
_DIST = os.path.join(_HERE, "..", "dist", "index.html")
# --- Legacy-Migration: traite_* / pause_* -> dossier_* ----------------------
#
# Historisch hatte das Plugin nacheinander die Praefixe "traite_" und "pause_"
# bevor es zu "dossier_" wurde. doc.Strings, Layer-UserStrings und
# Object-UserStrings werden einmalig pro Doc nach "dossier_*" kopiert.
# Idempotent — bestehende dossier_*-Werte werden nicht ueberschrieben.
_LEGACY_DOC_KEYS = (
"zeichnungsebenen", "ebenen", "active_id", "active_code",
"ausschnitte", "ausschnitt_folders", "layer_presets",
"user_scale", "dpi",
"show_lineweights", "plotweight_orig", "hatch_scale_orig",
"clipping_plane",
)
_LEGACY_LAYER_USER_KEYS = ("id", "code")
_LEGACY_OBJECT_USER_KEYS = ("clipping_plane", "plotweight_orig", "hatch_scale_orig")
_LEGACY_PREFIXES = ("traite_", "pause_")
_MIGRATE_FLAG = "dossier_migrated_v2" # neuer Flag — laeuft auch auf Docs die nur traite->pause hatten
def migrate_to_dossier(doc):
"""Migriert einmalig pro Document alle traite_*- und pause_*-Keys zu
dossier_*. No-op wenn schon migriert (per doc.Strings-Flag erkannt)."""
if doc is None:
return
try:
if doc.Strings.GetValue(_MIGRATE_FLAG):
return
except Exception:
return
moved_ds = 0
# 1) doc.Strings
try:
for suffix in _LEGACY_DOC_KEYS:
new = "dossier_" + suffix
try:
if doc.Strings.GetValue(new):
continue # Dossier-Variante vorhanden -> nicht ueberschreiben
for prefix in _LEGACY_PREFIXES:
old_v = doc.Strings.GetValue(prefix + suffix)
if old_v:
doc.Strings.SetString(new, old_v)
moved_ds += 1
break
except Exception:
pass
except Exception as ex:
print("[DOSSIER] migrate doc.Strings:", ex)
# 2) Layer-UserStrings
moved_layer = 0
try:
for layer in doc.Layers:
if layer is None or layer.IsDeleted:
continue
for suffix in _LEGACY_LAYER_USER_KEYS:
try:
if layer.GetUserString("dossier_" + suffix):
continue
for prefix in _LEGACY_PREFIXES:
v = layer.GetUserString(prefix + suffix)
if v:
layer.SetUserString("dossier_" + suffix, v)
moved_layer += 1
break
except Exception:
pass
except Exception as ex:
print("[DOSSIER] migrate layers:", ex)
# 3) Object-UserStrings
moved_obj = 0
try:
for obj in doc.Objects:
if obj is None or obj.IsDeleted:
continue
attrs = obj.Attributes
need_apply = False
a2 = None
for suffix in _LEGACY_OBJECT_USER_KEYS:
try:
if attrs.GetUserString("dossier_" + suffix):
continue
for prefix in _LEGACY_PREFIXES:
v = attrs.GetUserString(prefix + suffix)
if v:
if a2 is None:
a2 = attrs.Duplicate()
a2.SetUserString("dossier_" + suffix, v)
need_apply = True
moved_obj += 1
break
except Exception:
pass
if need_apply and a2 is not None:
try:
doc.Objects.ModifyAttributes(obj, a2, True)
except Exception:
pass
except Exception as ex:
print("[DOSSIER] migrate objects:", ex)
# Flag setzen damit die Migration nicht erneut laeuft
try:
doc.Strings.SetString(_MIGRATE_FLAG, "1")
except Exception:
pass
if moved_ds or moved_layer or moved_obj:
print("[DOSSIER] Migration: doc.Strings={} Layer-UserStrings={} Object-UserStrings={}".format(
moved_ds, moved_layer, moved_obj))
# --- Bridge -----------------------------------------------------------------
class BaseBridge(object):
"""
Basis-Bridge mit Chunk-Zusammenbau.
Subklassen ueberschreiben handle(data) und optional _on_ready().
"""
def __init__(self, mode):
self._wv = None
self._mode = mode
self._chunks = {}
self._chunk_total = 0
def set_webview(self, wv):
self._wv = wv
def handle_raw(self, raw_str):
if not raw_str:
return
try:
data = json.loads(raw_str)
except Exception as ex:
print("[{}] JSON-Fehler: {}".format(self._mode.upper(), ex))
return
if not isinstance(data, dict):
return
if "_chunk" in data:
c = data["_chunk"]
self._chunks[int(c["i"])] = data["d"]
self._chunk_total = int(c["n"])
if len(self._chunks) == self._chunk_total:
full = "".join(self._chunks[k] for k in sorted(self._chunks.keys()))
self._chunks = {}
self._chunk_total = 0
try:
self.handle(json.loads(full))
except Exception as ex:
import traceback
print("[{}] Chunk-Reassembly: {}".format(self._mode.upper(), ex))
print("[{}] Traceback:\n{}".format(self._mode.upper(), traceback.format_exc()))
else:
self.handle(data)
def handle(self, data):
"""Override. Default behandelt nur READY."""
if not isinstance(data, dict):
return
if data.get("type") == "READY":
self._on_ready()
def _on_ready(self):
"""Subklasse sendet hier den initialen State."""
pass
def send(self, msg_type, payload=None):
if self._wv is None:
return
# ensure_ascii=False umgeht Rhinos buggy json/encoder.py
# (s.decode('utf-8') auf .NET-Strings mit Umlauten -> CP1252-Codec-Fehler)
data = json.dumps({"type": msg_type, "payload": payload or {}}, ensure_ascii=False)
try:
self._wv.ExecuteScript(
"if(window.onRhinoMessage)window.onRhinoMessage({});".format(data)
)
except Exception:
pass
# --- HTML laden -------------------------------------------------------------
def load_inline(wv, mode):
"""Laedt dist/index.html inline und injiziert window.PANEL_MODE."""
if not os.path.exists(_DIST):
print("[{}] dist nicht gefunden".format(mode.upper()))
return
dist_dir = os.path.dirname(_DIST)
with open(_DIST, "rb") as f:
html = f.read().decode("utf-8")
mode_script = '<script>window.PANEL_MODE="{}";</script>'.format(mode)
if "</head>" in html:
html = html.replace("</head>", mode_script + "</head>")
else:
html = mode_script + html
def inline_css(m):
p = os.path.join(dist_dir, m.group(1).lstrip("./").replace("/", os.sep))
with open(p, "rb") as f2:
return u"<style>" + f2.read().decode("utf-8") + u"</style>"
def inline_js(m):
p = os.path.join(dist_dir, m.group(1).lstrip("./").replace("/", os.sep))
with open(p, "rb") as f2:
content = f2.read().decode("utf-8")
return (u'<script>document.addEventListener("DOMContentLoaded",function(){'
+ content + u'});</script>')
html = re.sub(r'<link[^>]+href="(\./assets/[^"]+\.css)"[^>]*/?>', inline_css, html)
html = re.sub(r'<script[^>]+src="(\./assets/[^"]+\.js)"[^>]*></script>', inline_js, html)
wv.LoadHtml(html)
def attach_webview(panel, bridge, mode):
wv = forms.WebView()
bridge.set_webview(wv)
panel.Content = wv
def on_title(s, e):
title = e.Title or ""
if not title.startswith("RHINOMSG::"):
return
try:
bridge.handle_raw(title[10:])
except Exception as ex:
print("[{}] Message-Fehler: {}".format(mode.upper(), ex))
finally:
try:
wv.ExecuteScript("document.title='{}';".format(mode.upper()))
except Exception:
pass
def on_loaded(s, e):
try:
wv.ExecuteScript("window.RHINO_MODE=true;")
except Exception:
pass
def on_idle(s, e):
Rhino.RhinoApp.Idle -= on_idle
try:
load_inline(wv, mode)
except Exception as ex:
print("[{}] Inline-Fehler: {}".format(mode.upper(), ex))
wv.DocumentTitleChanged += on_title
wv.DocumentLoaded += on_loaded
Rhino.RhinoApp.Idle += on_idle
# --- Dynamic .NET Type ------------------------------------------------------
def create_dockable_type(guid_str, type_name, assembly_name):
"""Baut einen echten CLR-Typ mit [Guid] fuer Rhinos RegisterPanel."""
import clr
import System.Reflection as SR
import System.Reflection.Emit as SRE
panel_base = clr.GetClrType(forms.Panel)
action_t = clr.GetClrType(System.Action[forms.Panel])
asm = SRE.AssemblyBuilder.DefineDynamicAssembly(
SR.AssemblyName(assembly_name),
SRE.AssemblyBuilderAccess.Run
)
mod = asm.DefineDynamicModule(assembly_name)
tb = mod.DefineType(
type_name,
SR.TypeAttributes.Public | SR.TypeAttributes.Class,
panel_base
)
guid_attr_t = clr.GetClrType(System.Runtime.InteropServices.GuidAttribute)
guid_ctor = guid_attr_t.GetConstructor(
System.Array[System.Type]([clr.GetClrType(System.String)])
)
tb.SetCustomAttribute(SRE.CustomAttributeBuilder(
guid_ctor, System.Array[System.Object]([guid_str])
))
cb_field = tb.DefineField(
"_callback", action_t,
SR.FieldAttributes.Public | SR.FieldAttributes.Static
)
ctor = tb.DefineConstructor(
SR.MethodAttributes.Public,
SR.CallingConventions.Standard,
System.Type.EmptyTypes
)
il = ctor.GetILGenerator()
lbl = il.DefineLabel()
base_ctor = panel_base.GetConstructor(System.Type.EmptyTypes)
il.Emit(SRE.OpCodes.Ldarg_0)
il.Emit(SRE.OpCodes.Call, base_ctor)
il.Emit(SRE.OpCodes.Ldsfld, cb_field)
il.Emit(SRE.OpCodes.Brfalse_S, lbl)
il.Emit(SRE.OpCodes.Ldsfld, cb_field)
il.Emit(SRE.OpCodes.Ldarg_0)
il.Emit(SRE.OpCodes.Callvirt, action_t.GetMethod("Invoke"))
il.MarkLabel(lbl)
il.Emit(SRE.OpCodes.Ret)
return tb.CreateType()
def _hex_rgb(h):
h = (h or "888888").lstrip("#")
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
_ICON_CACHE_DIR = os.path.expanduser("~/Library/Application Support/RhinoPanel/icons")
def make_panel_icon(letter, bg_hex):
"""Erzeugt ein Icon (32x32) mit farbigem Quadrat + Buchstabe.
Schreibt es als PNG-Datei auf Disk und laedt es via Eto.Drawing.Icon(path)
das ist der zuverlaessigste Weg auf Mac Rhino.
"""
try:
size = 32 # 32x32 fuer Retina (wird auf 16pt skaliert dargestellt)
bmp = drawing.Bitmap(size, size, drawing.PixelFormat.Format32bppRgba)
g = drawing.Graphics(bmp)
try:
try: g.AntiAlias = True
except Exception: pass
r, gg, bl = _hex_rgb(bg_hex)
bg = drawing.Color.FromArgb(r, gg, bl, 255)
g.FillRectangle(bg, 0, 0, size, size)
try:
font = drawing.Font(drawing.FontFamilies.Sans, 18, drawing.FontStyle.Bold)
except Exception:
font = drawing.Font("Helvetica", 18, drawing.FontStyle.Bold)
try:
text_size = g.MeasureString(font, letter)
tx = (size - text_size.Width) / 2
ty = (size - text_size.Height) / 2
except Exception:
tx, ty = size * 0.18, size * 0.12
g.DrawText(font, drawing.Colors.White, float(tx), float(ty), letter)
finally:
g.Dispose()
# PNG auf Disk schreiben — zuverlaessig fuer Mac Eto.Drawing.Icon
try:
if not os.path.isdir(_ICON_CACHE_DIR):
os.makedirs(_ICON_CACHE_DIR)
safe = re.sub(r"[^A-Za-z0-9]", "_", letter)
path = os.path.join(_ICON_CACHE_DIR, "icon_{}_{}.png".format(
safe, bg_hex.lstrip("#")))
bmp.Save(path, drawing.ImageFormat.Png)
except Exception as ex:
print("[panel_base] Icon-Save:", ex)
path = None
# 1. Versuch: Icon aus Datei-Pfad
if path and os.path.isfile(path):
try:
return drawing.Icon(path)
except Exception as ex:
print("[panel_base] Icon(path) fehlgeschlagen:", ex)
# 2. Versuch: Icon(scale, bitmap)
try:
return drawing.Icon(1.0, bmp)
except Exception: pass
# 3. Versuch: Icon(bitmap)
try:
return drawing.Icon(bmp)
except Exception: pass
# 4. Fallback: einfach das Bitmap zurueck (Rhino akzeptiert ggf. das auch)
return bmp
except Exception as ex:
print("[panel_base] Icon-Erstellung fehlgeschlagen:", ex)
return None
def find_plugin():
try:
installed = Rhino.PlugIns.PlugIn.GetInstalledPlugIns()
for guid in installed.Keys:
name = str(installed[guid])
if any(k in name for k in ["RhinoCode", "Scripting", "Python", "Script"]):
p = Rhino.PlugIns.PlugIn.Find(guid)
if p is not None:
return p
except Exception as ex:
print("[panel_base] Plugin-Suche:", ex)
return None
def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, min_size=None):
"""
Registriert (falls noetig) und oeffnet ein Panel.
bridge_factory: callable() -> BaseBridge Subklasse
icon_spec: (letter, hex_bg) zum Generieren eines Tab-Icons, oder None.
min_size: (width, height) als Tuple Panel-MinimumSize beim Erstellen.
Bei docked Panels wirkt's als Hint, bei float Panels als
tatsaechliche Startgroesse.
"""
sticky_reg = "panel_registered_" + mode
sticky_guid = "panel_guid_" + mode
if not sc.sticky.get(sticky_reg):
plugin = find_plugin()
if plugin is None:
print("[{}] Plugin nicht gefunden".format(mode.upper()))
return
try:
type_name = "DynPanel_" + mode
asm_name = "RhinoPanelDyn_" + mode
dyn_type = create_dockable_type(guid_str, type_name, asm_name)
def on_created(panel):
# MinimumSize setzen damit Rhino dem Panel bei Auto-Dock /
# Float genug Platz gibt (sonst spawned es schmal).
if min_size is not None:
try:
panel.MinimumSize = drawing.Size(int(min_size[0]), int(min_size[1]))
except Exception as ex:
print("[{}] MinimumSize konnte nicht gesetzt werden: {}".format(mode.upper(), ex))
# Auf einigen Eto-Versionen gibt es zusaetzlich Size/ClientSize
for attr in ("Size", "ClientSize"):
try:
if hasattr(panel, attr):
setattr(panel, attr, drawing.Size(int(min_size[0]), int(min_size[1])))
except Exception: pass
bridge = bridge_factory()
attach_webview(panel, bridge, mode)
dyn_type.GetField("_callback").SetValue(
None, System.Action[forms.Panel](on_created)
)
icon = None
if icon_spec:
try:
icon = make_panel_icon(icon_spec[0], icon_spec[1])
except Exception as ex:
print("[{}] Icon-Erstellung uebersprungen: {}".format(mode.upper(), ex))
icon = None
registered = False
registered_with_icon = False
# Erst mit Icon versuchen, dann stillschweigend ohne (Mac Rhino-Panels
# akzeptieren auf manchen Versionen nur System.Drawing.Icon, das auf
# Mac nicht verfuegbar ist - die Registrierung ohne Icon ist OK).
attempts = [(icon, True)] if icon is not None else []
attempts.append((None, False))
for arg, with_icon in attempts:
try:
RhinoUI.Panels.RegisterPanel(plugin, dyn_type, caption, arg)
registered = True
registered_with_icon = with_icon
if with_icon:
print("[{}] Panel mit Icon registriert ({})".format(
mode.upper(), type(arg).__name__))
break
except Exception as ex:
if with_icon:
print("[{}] RegisterPanel mit Icon fehlgeschlagen: {}".format(
mode.upper(), ex))
else:
print("[{}] RegisterPanel fehlgeschlagen: {}".format(
mode.upper(), ex))
if registered and not registered_with_icon and icon is not None:
print("[{}] Panel ohne Icon registriert (Fallback)".format(mode.upper()))
if not registered:
return
sc.sticky[sticky_reg] = True
sc.sticky[sticky_guid] = System.Guid(guid_str)
print("[{}] Panel registriert".format(mode.upper()))
except Exception as ex:
print("[{}] Registrierung fehlgeschlagen: {}".format(mode.upper(), ex))
return
try:
guid = sc.sticky.get(sticky_guid, System.Guid(guid_str))
RhinoUI.Panels.OpenPanel(guid)
print("[{}] Panel geoeffnet".format(mode.upper()))
except Exception as ex:
print("[{}] OpenPanel fehlgeschlagen: {}".format(mode.upper(), ex))
+798
View File
@@ -0,0 +1,798 @@
# ! python3
# -*- coding: utf-8 -*-
"""
rhinopanel.py
Oeffnet das EBENEN-Panel (Zeichnungsebenen + globale Ebenen).
"""
import os
import sys
import json
import Rhino
import scriptcontext as sc
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
import panel_base
import layer_builder
PANEL_GUID_STR = "3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718"
# Loop-Guard fuer Layer-Events (verhindert Endlos-Schleife bei eigenen Aenderungen)
def _is_processing():
return bool(sc.sticky.get("ebenen_processing_layer", False))
def _set_processing(v):
sc.sticky["ebenen_processing_layer"] = bool(v)
def _hatch_pattern_names(doc):
"""Liefert alle Hatch-Pattern-Namen aus doc.HatchPatterns als Liste."""
out = []
try:
for i in range(doc.HatchPatterns.Count):
try:
hp = doc.HatchPatterns[i]
if hp is None or hp.IsDeleted: continue
if hp.Name: out.append(hp.Name)
except Exception:
continue
except Exception:
pass
if not out: out = ["Solid"]
return out
class EbenenBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "ebenen")
def _on_ready(self):
doc = Rhino.RhinoDoc.ActiveDoc
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
e_raw = doc.Strings.GetValue("dossier_ebenen")
if z_raw or e_raw:
try:
z = json.loads(z_raw) if z_raw else None
e = json.loads(e_raw) if e_raw else None
if z and e:
layer_builder.build_layers(doc, z, e)
layer_builder.cleanup_default_layers(doc)
self._ensure_active_sublayer()
self.send("STATE_SYNC", {
"zeichnungsebenen": z,
"ebenen": e,
"hatchPatterns": _hatch_pattern_names(doc),
})
except Exception as ex:
print("[EBENEN] State-Sync:", ex)
else:
self.send("FIRST_RUN", {"hatchPatterns": _hatch_pattern_names(doc)})
def handle(self, data):
if not isinstance(data, dict):
return
t = data.get("type", "")
p = data.get("payload") or {}
if not isinstance(p, dict):
p = {}
doc = Rhino.RhinoDoc.ActiveDoc
if t == "READY":
self._on_ready()
elif t == "APPLY":
self._apply(p.get("zeichnungsebenen") or [], p.get("ebenen") or [])
elif t == "LAYER_STYLE":
layer_builder.update_layer_style(doc, p["code"], p.get("color"), p.get("lw"))
if p.get("color") is not None:
self._update_ebene_field(p["code"], "color", p["color"])
if p.get("lw") is not None:
self._update_ebene_field(p["code"], "lw", p["lw"])
elif t == "SET_ACTIVE":
self._set_active_zeichnungsebene(p)
elif t == "SET_ACTIVE_LAYER":
code = p.get("code", "")
if code:
doc.Strings.SetString("dossier_active_code", code)
self._set_active_sublayer(code)
elif t == "DELETE_EBENE":
layer_builder.delete_ebene(doc, p.get("code", ""), p.get("moveTo"))
self._remove_ebene_from_state(p.get("code", ""))
elif t == "MOVE_SELECTION_TO_LAYER":
self._move_selection_to_layer(p.get("code", ""))
elif t == "SET_VISIBILITY":
self._apply_visibility(p)
# --- Ebenen-Kombinationen (geteilter Store mit Ausschnitten) -------
elif t == "GET_COMBINATION":
self._send_combination()
elif t == "APPLY_COMBINATION":
self._apply_combination(p)
self._send_combination()
elif t == "SAVE_PRESET":
self._save_preset(p.get("name") or "", p.get("layers") or [])
self._send_combination()
elif t == "SAVE_CURRENT_AS_PRESET":
self._save_current_as_preset(p.get("name") or "")
self._send_combination()
elif t == "DELETE_PRESET":
self._delete_preset(p.get("name") or "")
self._send_combination()
# ---- Helpers ----
def _apply(self, zeichnungsebenen, ebenen):
print("[EBENEN] _apply START z={} e={}".format(
len(zeichnungsebenen) if zeichnungsebenen else 0,
len(ebenen) if ebenen else 0))
doc = Rhino.RhinoDoc.ActiveDoc
# Vor dem Schreiben: alten Fill-Stand snapshotten, damit wir hinterher
# entscheiden koennen ob refresh_layer_fills sich lohnt.
def _fill_signature(e_list):
out = {}
if not isinstance(e_list, list): return out
for e in e_list:
if not isinstance(e, dict): continue
f = e.get("fill")
if not isinstance(f, dict): continue
if f.get("pattern") in (None, "None"): continue
# lw kann None sein -> als Sentinel ein eindeutiger Wert
lw_raw = f.get("lw")
try:
lw_sig = round(float(lw_raw), 6) if lw_raw is not None else None
except Exception:
lw_sig = None
out[e.get("code")] = (
f.get("pattern"),
f.get("source", "layer"),
(f.get("color") or "").lower(),
round(float(f.get("scale") or 1.0), 6),
round(float(f.get("rotation") or 0.0), 6),
lw_sig,
)
return out
old_e_raw = doc.Strings.GetValue("dossier_ebenen")
old_sig = {}
if old_e_raw:
try: old_sig = _fill_signature(json.loads(old_e_raw))
except Exception: old_sig = {}
new_sig = _fill_signature(ebenen)
fill_changed = (old_sig != new_sig)
_set_processing(True)
try:
print("[EBENEN] _apply: build_layers ...")
layer_builder.build_layers(doc, zeichnungsebenen, ebenen)
print("[EBENEN] _apply: json.dumps ...")
# WICHTIG: ensure_ascii=False umgeht einen Bug in Rhinos eigener
# json/encoder.py die bei ASCII-escape s.decode('utf-8') aufruft
# und dabei mit 0xC4 (Umlaut) in den CP1252-Decoder lauft.
z_json = json.dumps(zeichnungsebenen, ensure_ascii=False)
e_json = json.dumps(ebenen, ensure_ascii=False)
print("[EBENEN] _apply: SetString ...")
doc.Strings.SetString("dossier_zeichnungsebenen", z_json)
doc.Strings.SetString("dossier_ebenen", e_json)
# Smart-Elemente (Waende) regenerieren — Geschoss-Hoehen/OKFF
# haben sich evtl. geaendert, gebundene Waende muessen neu
# extrudiert werden. Best-effort, faengt jeden Fehler ab.
try:
elem_bridge = sc.sticky.get("elemente_bridge")
if elem_bridge is not None:
elem_bridge._regenerate_all()
except Exception as _ex:
print("[EBENEN] elemente regen:", _ex)
n_with_fill = sum(1 for e in ebenen if isinstance(e, dict)
and isinstance(e.get("fill"), dict)
and e["fill"].get("pattern") not in (None, "None"))
print("[EBENEN] dossier_ebenen gespeichert: {} Ebenen, davon {} mit fill, JSON-len={}".format(
len(ebenen), n_with_fill, len(e_json)))
re_read = doc.Strings.GetValue("dossier_ebenen")
print("[EBENEN] dossier_ebenen verifiziert: len={}".format(len(re_read) if re_read else 0))
print("[EBENEN] _apply: cleanup_default_layers ...")
layer_builder.cleanup_default_layers(doc)
print("[EBENEN] _apply: ensure_active_sublayer ...")
self._ensure_active_sublayer()
# Existierende 'Nach Ebene'-Hatches an neue Pattern/Skala/Drehung
# angleichen — ABER nur wenn die Fill-Signatur sich tatsaechlich
# geaendert hat (nicht bei reinen Name/Farb-Aenderungen, die das
# Settings-Dialog auch triggern koennte).
try:
import gestaltung
if fill_changed:
gestaltung.refresh_layer_fills(doc)
else:
print("[EBENEN] _apply: fill-Signatur unveraendert -> kein Hatch-Refresh")
# Plot-Color Repair laeuft immer (no-op falls schon synchron)
gestaltung.repair_plot_colors(doc)
except Exception as ex:
print("[EBENEN] gestaltung sync:", ex)
finally:
_set_processing(False)
print("[EBENEN] _apply: update_clipping ...")
self._update_clipping()
print("[EBENEN] _apply: send APPLY_OK")
self.send("APPLY_OK", {})
print("[EBENEN] _apply: DONE")
def _ensure_active_sublayer(self):
"""Setzt den aktiven Rhino-Layer auf den DOSSIER-Sublayer (Fallback: erste Z + 20_WAENDE)."""
doc = Rhino.RhinoDoc.ActiveDoc
z_id = doc.Strings.GetValue("dossier_active_id")
code = doc.Strings.GetValue("dossier_active_code") or "20"
if not z_id:
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
if z_raw:
try:
z_list = json.loads(z_raw)
if z_list:
z_id = z_list[0].get("id", "")
if z_id:
doc.Strings.SetString("dossier_active_id", z_id)
except Exception:
pass
if z_id and code:
layer_builder.set_active_sublayer(doc, z_id, code)
def _apply_visibility(self, p):
doc = Rhino.RhinoDoc.ActiveDoc
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
e_raw = doc.Strings.GetValue("dossier_ebenen")
if not z_raw or not e_raw:
return
try:
z_full = json.loads(z_raw) or []
e_full = json.loads(e_raw) or []
except Exception:
return
payload_z = p.get("zeichnungsebenen") or []
payload_e = p.get("ebenen") or []
z_state = {z["id"]: z for z in payload_z if isinstance(z, dict) and z.get("id")}
e_state = {e["code"]: e for e in payload_e if isinstance(e, dict) and e.get("code")}
merged_z = []
for z in z_full:
if not isinstance(z, dict): continue
m = dict(z)
s = z_state.get(z.get("id"))
if s is not None:
m["visible"] = s.get("visible", True)
merged_z.append(m)
merged_e = []
for e in e_full:
if not isinstance(e, dict): continue
m = dict(e)
s = e_state.get(e.get("code"))
if s is not None:
m["visible"] = s.get("visible", True)
m["locked"] = s.get("locked", False)
merged_e.append(m)
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(merged_z, ensure_ascii=False))
doc.Strings.SetString("dossier_ebenen", json.dumps(merged_e, ensure_ascii=False))
active_z = p.get("activeZ") or {}
if not isinstance(active_z, dict): active_z = {}
layer_builder.apply_visibility(
doc, merged_z, merged_e,
active_z.get("id"),
p.get("activeCode"),
p.get("zMode") or "active",
p.get("eMode") or "all",
)
def _set_active_zeichnungsebene(self, z):
doc = Rhino.RhinoDoc.ActiveDoc
z_id = z.get("id", "")
doc.Strings.SetString("dossier_active_id", z_id)
# Clipping ggf. mitziehen
self._update_clipping(active_z=z)
# Elemente-Panel informieren: das aktive Geschoss hat gewechselt,
# neue Elemente sollen jetzt automatisch dort verlinkt werden.
try:
eb = sc.sticky.get("elemente_bridge")
if eb is not None: eb._send_state()
except Exception: pass
if not (z.get("isGeschoss") and z.get("okff") is not None):
return
okff = float(z["okff"])
updated = 0
for view in doc.Views:
try:
vp = view.ActiveViewport
cp = vp.ConstructionPlane()
plane = cp.Plane if hasattr(cp, "Plane") else cp
# Nur Views deren CPlane horizontal liegt (Normal in +/-Z) -
# also Top/Plan-Style. Right/Front/Perspective haben vertikale
# CPlanes; ein Z-Shift waere dort optisch verwirrend.
if abs(plane.Normal.Z) < 0.99:
continue
new_plane = Rhino.Geometry.Plane(
Rhino.Geometry.Point3d(plane.Origin.X, plane.Origin.Y, okff),
plane.XAxis, plane.YAxis,
)
vp.SetConstructionPlane(new_plane)
view.Redraw()
updated += 1
except Exception as ex:
print("[EBENEN] CPlane fehler ({}): {}".format(vp.Name if vp else "?", ex))
print("[EBENEN] CPlane Z={} auf {} Top-Style View(s) gesetzt".format(okff, updated))
def _update_clipping(self, active_z=None):
"""Clipping-Plane folgt aktivem Geschoss — nur wenn dessen hasClipping=True."""
doc = Rhino.RhinoDoc.ActiveDoc
if active_z is None:
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
active_id = doc.Strings.GetValue("dossier_active_id")
if z_raw and active_id:
try:
z_list = json.loads(z_raw)
active_z = next((z for z in z_list if z.get("id") == active_id), None)
except Exception:
active_z = None
enabled = bool(active_z and active_z.get("hasClipping"))
_set_processing(True)
try:
layer_builder.update_clipping_plane(doc, active_z, enabled)
finally:
_set_processing(False)
def _move_selection_to_layer(self, code):
if not code:
return
doc = Rhino.RhinoDoc.ActiveDoc
z_id = doc.Strings.GetValue("dossier_active_id")
if not z_id:
print("[EBENEN] Keine aktive Zeichnungsebene")
return
parent_idx = layer_builder._find_top_by_id(doc, z_id)
if parent_idx < 0:
print("[EBENEN] Parent fuer aktive Zeichnungsebene nicht gefunden")
return
parent_id = doc.Layers[parent_idx].Id
sub_idx = layer_builder._find_sublayer_by_code(doc, parent_id, code)
if sub_idx < 0:
print("[EBENEN] Sublayer {} unter {} nicht gefunden".format(code, doc.Layers[parent_idx].Name))
return
objs = list(doc.Objects.GetSelectedObjects(False, False))
moved = 0
for obj in objs:
attrs = obj.Attributes.Duplicate()
attrs.LayerIndex = sub_idx
if doc.Objects.ModifyAttributes(obj, attrs, True):
moved += 1
doc.Views.Redraw()
print("[EBENEN] {} Objekt(e) auf {} verschoben".format(moved, doc.Layers[sub_idx].FullPath))
def _set_active_sublayer(self, code):
if not code:
return
doc = Rhino.RhinoDoc.ActiveDoc
z_id = doc.Strings.GetValue("dossier_active_id")
if not z_id:
# Fallback: erste Zeichnungsebene aus persistiertem State
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
if z_raw:
try:
z_list = json.loads(z_raw)
if z_list:
z_id = z_list[0].get("id", "")
if z_id:
doc.Strings.SetString("dossier_active_id", z_id)
except Exception:
pass
if z_id:
layer_builder.set_active_sublayer(doc, z_id, code)
else:
print("[EBENEN] Aktive Zeichnungsebene unbekannt — Layer wird nicht gesetzt")
def _remove_ebene_from_state(self, code):
doc = Rhino.RhinoDoc.ActiveDoc
raw = doc.Strings.GetValue("dossier_ebenen")
if not raw:
return
try:
ebenen = [e for e in json.loads(raw) if e.get("code") != code]
doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen, ensure_ascii=False))
except Exception as ex:
print("[EBENEN] remove:", ex)
def _update_ebene_field(self, code, field, value):
doc = Rhino.RhinoDoc.ActiveDoc
raw = doc.Strings.GetValue("dossier_ebenen")
if not raw:
return
try:
ebenen = json.loads(raw)
for e in ebenen:
if e.get("code") == code:
e[field] = value
break
doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen, ensure_ascii=False))
except Exception as ex:
print("[EBENEN] update:", ex)
# ---- Ebenen-Kombinationen / Presets (geteilt mit AUSSCHNITTE) --------
_PRESETS_KEY = "dossier_layer_presets"
def _load_presets(self, doc):
raw = doc.Strings.GetValue(self._PRESETS_KEY)
if not raw: return []
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except Exception:
return []
def _store_presets(self, doc, presets):
try:
doc.Strings.SetString(self._PRESETS_KEY,
json.dumps(presets, ensure_ascii=False))
except Exception as ex:
print("[EBENEN] _store_presets:", ex)
def _send_combination(self):
"""Schickt aktuelles Layer-State + alle Presets ans Frontend."""
doc = Rhino.RhinoDoc.ActiveDoc
layers_out = []
try:
for layer in doc.Layers:
if layer is None or layer.IsDeleted: continue
lid = str(layer.Id)
try:
fp = layer.FullPath or layer.Name
except Exception:
fp = layer.Name or ""
try:
col = "#%02x%02x%02x" % (layer.Color.R, layer.Color.G, layer.Color.B)
except Exception:
col = "#888888"
layers_out.append({
"id": lid,
"name": layer.Name,
"fullPath": fp,
"color": col,
"visible": bool(layer.IsVisible),
"locked": bool(layer.IsLocked),
})
layers_out.sort(key=lambda x: x["fullPath"])
except Exception as ex:
print("[EBENEN] _send_combination layers:", ex)
try:
presets = self._load_presets(doc)
except Exception:
presets = []
self.send("COMBINATION_DATA", {
"layers": layers_out,
"presets": presets,
})
def _apply_combination(self, payload):
"""Wendet Preset an. payload kann sein:
- Liste [{id, visible, locked}, ...] (alt / AUSSCHNITTE-Dialog)
- Dict { layers, dossierEbenen?, dossierZeichnungsebenen? } (neu)
Eye-State-Pfad (bevorzugt): aktualisiert dossier_ebenen und
dossier_zeichnungsebenen direkt, pusht STATE_SYNC. React triggert
dann SET_VISIBILITY und apply_visibility setzt doc.Layer korrekt
unter Beruecksichtigung von z_mode/e_mode.
Layer-ID-Pfad (Fallback): setzt doc.Layer.IsVisible direkt.
"""
doc = Rhino.RhinoDoc.ActiveDoc
# Payload normalisieren
if isinstance(payload, dict):
layer_states = payload.get("layers") or []
pe_states = payload.get("dossierEbenen")
pz_states = payload.get("dossierZeichnungsebenen")
else:
layer_states = payload or []
pe_states = None
pz_states = None
# --- Eye-State-Pfad (wenn vorhanden) ---
if pe_states is not None or pz_states is not None:
try:
e_raw = doc.Strings.GetValue("dossier_ebenen") or "[]"
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") or "[]"
e_list = json.loads(e_raw) or []
z_list = json.loads(z_raw) or []
if pe_states is not None:
by_code = {x.get("code"): x for x in pe_states if isinstance(x, dict) and x.get("code")}
for e in e_list:
if not isinstance(e, dict): continue
s = by_code.get(e.get("code"))
if s is None: continue
e["visible"] = bool(s.get("visible", True))
e["locked"] = bool(s.get("locked", False))
if pz_states is not None:
by_id = {x.get("id"): x for x in pz_states if isinstance(x, dict) and x.get("id")}
for z in z_list:
if not isinstance(z, dict): continue
s = by_id.get(z.get("id"))
if s is None: continue
z["visible"] = bool(s.get("visible", True))
doc.Strings.SetString("dossier_ebenen", json.dumps(e_list, ensure_ascii=False))
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(z_list, ensure_ascii=False))
# STATE_SYNC pushen — React's visibilityKey aendert sich,
# applyVisibility fires, backend apply_visibility setzt doc.Layer
# state korrekt unter z_mode/e_mode-Beachtung.
self.send("STATE_SYNC", {
"zeichnungsebenen": z_list,
"ebenen": e_list,
})
try: doc.Views.Redraw()
except Exception: pass
print("[EBENEN] Eye-State-Preset angewandt: {} Ebenen, {} Zeichnungsebenen".format(
len(pe_states or []), len(pz_states or [])))
return
except Exception as ex:
print("[EBENEN] _apply_combination eye-state:", ex)
# Fall through zum Layer-ID-Pfad als Fallback
# --- Layer-ID-Pfad (alt / AUSSCHNITTE) ---
by_id = {}
for layer in doc.Layers:
if not layer.IsDeleted:
by_id[str(layer.Id)] = layer
n = 0
# Erst: doc.Layer Visibility setzen
_set_processing(True)
try:
for ls in (layer_states or []):
layer = by_id.get(ls.get("id"))
if layer is None: continue
try:
want_vis = bool(ls.get("visible", True))
want_lck = bool(ls.get("locked", False))
if layer.IsVisible != want_vis:
layer.IsVisible = want_vis
if layer.IsLocked != want_lck:
layer.IsLocked = want_lck
n += 1
except Exception: pass
finally:
_set_processing(False)
# Dann: dossier_ebenen/dossier_zeichnungsebenen Eye-State synchronisieren.
# Map: doc.Layer.Id -> {visible, locked}
state_by_id = {ls.get("id"): ls for ls in (layer_states or []) if ls.get("id")}
try:
e_raw = doc.Strings.GetValue("dossier_ebenen")
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
ebenen_list = json.loads(e_raw) if e_raw else []
z_list = json.loads(z_raw) if z_raw else []
# Sublayer -> dossier_code mapping via Rhino-Layer UserString
code_by_layer_id = {}
zid_by_layer_id = {}
for layer in doc.Layers:
if layer is None or layer.IsDeleted: continue
c = layer.GetUserString("dossier_code")
i = layer.GetUserString("dossier_id")
if c: code_by_layer_id[str(layer.Id)] = c
if i: zid_by_layer_id[str(layer.Id)] = i
# Pro Dossier-Ebene: wenn mind. ein matchender Sublayer im preset war,
# sync visible/locked.
updated_e = False
for e in ebenen_list:
if not isinstance(e, dict): continue
code = e.get("code")
if not code: continue
# Suche eine Layer-Id mit diesem code, deren state im preset ist
for lid, c in code_by_layer_id.items():
if c != code: continue
s = state_by_id.get(lid)
if s is None: continue
new_vis = bool(s.get("visible", True))
new_lck = bool(s.get("locked", False))
if e.get("visible", True) != new_vis:
e["visible"] = new_vis
updated_e = True
if (e.get("locked", False)) != new_lck:
e["locked"] = new_lck
updated_e = True
break
updated_z = False
for z in z_list:
if not isinstance(z, dict): continue
zid = z.get("id")
if not zid: continue
for lid, z_uid in zid_by_layer_id.items():
if z_uid != zid: continue
s = state_by_id.get(lid)
if s is None: continue
new_vis = bool(s.get("visible", True))
if z.get("visible", True) != new_vis:
z["visible"] = new_vis
updated_z = True
break
if updated_e:
doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen_list, ensure_ascii=False))
if updated_z:
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(z_list, ensure_ascii=False))
# STATE_SYNC ans React-Panel pushen damit Eye-Icons matchen
if updated_e or updated_z:
try:
self.send("STATE_SYNC", {
"zeichnungsebenen": z_list,
"ebenen": ebenen_list,
})
except Exception as ex:
print("[EBENEN] STATE_SYNC push:", ex)
except Exception as ex:
print("[EBENEN] _apply_combination sync:", ex)
try: doc.Views.Redraw()
except Exception: pass
print("[EBENEN] Kombination angewandt: {} Layer".format(n))
def _save_preset(self, name, layers):
name = (name or "").strip()
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
presets = self._load_presets(doc)
clean = []
for ls in (layers or []):
lid = ls.get("id")
if not lid: continue
clean.append({
"id": lid,
"visible": bool(ls.get("visible", True)),
"locked": bool(ls.get("locked", False)),
})
existing = next((p for p in presets if p.get("name") == name), None)
if existing is not None:
existing["layers"] = clean
else:
presets.append({"name": name, "layers": clean})
self._store_presets(doc, presets)
print("[EBENEN] Kombination '{}' gespeichert ({} Layer)".format(name, len(clean)))
def _save_current_as_preset(self, name):
"""Speichert die aktuellen Eye-States (dossier_ebenen + dossier_zeichnungs-
ebenen) als Preset NICHT die berechneten doc.Layer.IsVisible-Werte.
Sonst wuerde der z_mode/e_mode-Override (z.B. 'active' nur 1 Layer
sichtbar) ins Preset einbacken und beim Apply nicht wieder restorbar
sein.
layers (doc.Layer-Liste) wird parallel mitgespeichert fuer Kompat
mit AUSSCHNITTE (das vom doc.Layer-State liest)."""
name = (name or "").strip()
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
# 1) doc.Layer state (Kompat mit AUSSCHNITTE)
layers = []
try:
for layer in doc.Layers:
if layer is None or layer.IsDeleted: continue
layers.append({
"id": str(layer.Id),
"visible": bool(layer.IsVisible),
"locked": bool(layer.IsLocked),
})
except Exception as ex:
print("[EBENEN] _save_current_as_preset enum:", ex)
# 2) Eye-States aus dossier_ebenen / dossier_zeichnungsebenen
pe_state = []
pz_state = []
try:
e_raw = doc.Strings.GetValue("dossier_ebenen")
if e_raw:
for e in (json.loads(e_raw) or []):
if isinstance(e, dict) and e.get("code"):
pe_state.append({
"code": e["code"],
"visible": bool(e.get("visible", True)),
"locked": bool(e.get("locked", False)),
})
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
if z_raw:
for z in (json.loads(z_raw) or []):
if isinstance(z, dict) and z.get("id"):
pz_state.append({
"id": z["id"],
"visible": bool(z.get("visible", True)),
})
except Exception as ex:
print("[EBENEN] _save_current_as_preset eye-states:", ex)
presets = self._load_presets(doc)
new_data = {
"name": name,
"layers": layers,
"dossierEbenen": pe_state,
"dossierZeichnungsebenen": pz_state,
}
existing = next((p for p in presets if p.get("name") == name), None)
if existing is not None:
existing.update(new_data)
else:
presets.append(new_data)
self._store_presets(doc, presets)
print("[EBENEN] '{}' gespeichert: {} Layer + {} Ebenen Eye-State".format(
name, len(layers), len(pe_state)))
def _delete_preset(self, name):
name = (name or "").strip()
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
presets = [p for p in self._load_presets(doc) if p.get("name") != name]
self._store_presets(doc, presets)
print("[EBENEN] Kombination '{}' geloescht".format(name))
def _ebenen_bridge_factory():
bridge = EbenenBridge()
_install_layer_listener(bridge)
return bridge
def _install_layer_listener(bridge):
"""Reagiert auf externe Aenderungen in Rhinos Layer-Tabelle (Rename, Delete)."""
if sc.sticky.get("ebenen_layer_listener"):
sc.sticky["ebenen_bridge_ref"] = bridge
return
sc.sticky["ebenen_bridge_ref"] = bridge
def on_layer_event(sender, args):
if _is_processing():
return
try:
doc = args.Document
evt = args.EventType
# Nur Modify-Events interessieren uns (Rename, Color etc.)
if evt != Rhino.DocObjects.Tables.LayerTableEventType.Modified:
return
idx = args.LayerIndex
if idx < 0 or idx >= doc.Layers.Count:
return
layer = doc.Layers[idx]
dossier_id = layer.GetUserString("dossier_id")
dossier_code = layer.GetUserString("dossier_code")
if not (dossier_id or dossier_code):
return
updated = False
if dossier_id:
raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
if raw:
try:
z_list = json.loads(raw)
for z in z_list:
if z.get("id") == dossier_id and z.get("name") != layer.Name:
z["name"] = layer.Name
updated = True
break
if updated:
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(z_list, ensure_ascii=False))
except Exception:
pass
elif dossier_code:
raw = doc.Strings.GetValue("dossier_ebenen")
if raw:
try:
e_list = json.loads(raw)
# Layer-Name ist "CC_NAME" — wir extrahieren NAME
if "_" in layer.Name:
new_name = layer.Name.split("_", 1)[1]
for e in e_list:
if e.get("code") == dossier_code and e.get("name") != new_name:
e["name"] = new_name
updated = True
break
if updated:
doc.Strings.SetString("dossier_ebenen", json.dumps(e_list, ensure_ascii=False))
except Exception:
pass
if updated:
b = sc.sticky.get("ebenen_bridge_ref")
if b is not None:
try:
b._on_ready() # sendet aktualisiertes STATE_SYNC
except Exception:
pass
except Exception as ex:
print("[EBENEN] Layer-Event:", ex)
Rhino.RhinoDoc.LayerTableEvent += on_layer_event
sc.sticky["ebenen_layer_listener"] = True
print("[EBENEN] Layer-Listener aktiv")
panel_base.register_and_open("ebenen", "EBENEN", PANEL_GUID_STR, _ebenen_bridge_factory,
icon_spec=("E", "#3a6fa8"))
+136
View File
@@ -0,0 +1,136 @@
# ! python3
# -*- coding: utf-8 -*-
"""
startup.py
Laedt DOSSIER-Panels beim Rhino-Start. Liest pro geoeffnetem Dokument eine
`dossier.project.json` (neben der `.3dm` abgelegt vom Dossier-Launcher) und
aktiviert nur die dort gelisteten Module. Fehlt die Datei alle bekannten
Module laden (Backwards-Compat fuer Setups ohne Launcher).
"""
import os
import sys
import json
import Rhino
import scriptcontext as sc
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
# Pfad zur Custom-UI (Toolbars/Sidebar) — wird einmal pro Session geladen
_UI_FILE = os.path.join(_HERE, "DOSSIERUI.rhw")
# Map: Modul-ID (aus dossier.project.json) -> Python-Modulname (Datei in rhino/).
# Muss synchron sein mit launcher/modules.json. Wenn neue Module dazukommen,
# beide Stellen pflegen.
_MODULE_TO_PY = {
"ebenen": "rhinopanel",
"oberleiste": "oberleiste",
"ausschnitte": "ausschnitte",
"gestaltung": "gestaltung",
"werkzeuge": "werkzeuge",
"overrides": "overrides_panel",
"dimensionen": "dimensionen",
"layouts": "layouts",
"elemente": "elemente",
}
_ALL_MODULES = list(_MODULE_TO_PY.keys())
def _read_project_config():
"""Liest dossier.project.json aus dem Ordner des aktiven Docs. Rueckgabe:
dict oder None. None heisst keine Config" -> Fallback alle Module."""
try:
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None or not getattr(doc, "Path", None):
return None
doc_dir = os.path.dirname(doc.Path)
if not doc_dir:
return None
config_path = os.path.join(doc_dir, "dossier.project.json")
if not os.path.isfile(config_path):
return None
with open(config_path, "rb") as f:
data = json.loads(f.read().decode("utf-8"))
return data if isinstance(data, dict) else None
except Exception as ex:
print("[STARTUP] Project-Config lesen:", ex)
return None
def _migrate_active_doc(*_):
"""Migriert Legacy-Keys (traite_*, pause_*) -> dossier_* fuer das aktive Doc."""
try:
import panel_base
panel_base.migrate_to_dossier(Rhino.RhinoDoc.ActiveDoc)
except Exception as ex:
print("[STARTUP] Migration:", ex)
def _on_doc_opened(sender, e):
"""Greift bei jedem geoeffneten Doc nach Rhino-Start. Migration ist
idempotent (Flag in doc.Strings)."""
try:
doc = e.Document if hasattr(e, "Document") else Rhino.RhinoDoc.ActiveDoc
import panel_base
panel_base.migrate_to_dossier(doc)
except Exception as ex:
print("[STARTUP] _on_doc_opened:", ex)
def _hint_dossier_ui():
"""Mac Rhino 8 kann Window-Layout-Dateien nicht via Skript laden — der
Dialog ueber Window-Menue nutzt interne API ohne Command-Echo. Wir
geben nur einen Hinweis-Pfad aus, damit der User DOSSIERUI.rhw einmal
manuell laden kann. Rhino merkt sich die Anordnung dann persistent."""
if not os.path.isfile(_UI_FILE):
return
print("[STARTUP] DOSSIERUI gefunden: {}".format(_UI_FILE))
print("[STARTUP] Einmalig laden: Window -> Window Layout -> Open -> obige Datei")
print("[STARTUP] Anordnung bleibt danach ueber Rhino-Restarts erhalten.")
def _load_all(sender, e):
"""Wird beim ersten Idle ausgefuehrt — entkoppelt sich danach selbst."""
try:
Rhino.RhinoApp.Idle -= _load_all
except Exception:
pass
print("[STARTUP] Lade DOSSIER-Panels...")
# Migration einmal fuer das beim Start aktive Doc
_migrate_active_doc()
# Und Listener fuer spaeter geoeffnete Docs registrieren
try:
Rhino.RhinoDoc.EndOpenDocument += _on_doc_opened
except Exception as ex:
print("[STARTUP] EndOpenDocument-Hook:", ex)
# Projekt-Config bestimmt, welche Module geladen werden. Ohne Config
# (kein Launcher benutzt, oder Datei nicht da) laedt der Host alles.
config = _read_project_config()
if config and isinstance(config.get("modules"), list):
enabled_ids = [m for m in config["modules"] if m in _MODULE_TO_PY]
unknown = [m for m in config["modules"] if m not in _MODULE_TO_PY]
print("[STARTUP] Projekt: '{}'".format(config.get("name") or "?"))
print("[STARTUP] Aktivierte Module: {}".format(", ".join(enabled_ids) or "(keine)"))
if unknown:
print("[STARTUP] Unbekannte Modul-IDs in Config: {}".format(unknown))
else:
enabled_ids = _ALL_MODULES
print("[STARTUP] Keine dossier.project.json — alle Module laden")
# massstab.py wird als Library mitgeladen (von oberleiste/ausschnitte/...)
# und braucht hier nicht mehr als eigenstaendiges Panel zu erscheinen.
for mod_id in enabled_ids:
py_name = _MODULE_TO_PY[mod_id]
try:
__import__(py_name)
print("[STARTUP] {} ({}) OK".format(mod_id, py_name))
except Exception as ex:
print("[STARTUP] {} ({}) FEHLER: {}".format(mod_id, py_name, ex))
# DOSSIERUI Window-Layout — Hinweis fuer manuelles Laden
_hint_dossier_ui()
print("[STARTUP] Fertig")
Rhino.RhinoApp.Idle += _load_all
print("[STARTUP] geplant - laedt sobald Rhino idle ist")
+58
View File
@@ -0,0 +1,58 @@
# ! python3
# -*- coding: utf-8 -*-
"""
werkzeuge.py
WERKZEUGE-Panel: Architektur-orientierte Toolbar als React-WebView.
Feuert Rhino-Befehle via RunScript bei Button-Klick.
"""
import os
import sys
import Rhino
import scriptcontext as sc
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
import panel_base
PANEL_GUID_STR = "6d9f5040-7e1f-4f2b-c4d5-f6071829304a"
class WerkzeugeBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "werkzeuge")
def _on_ready(self):
# Keine initialen Daten noetig — Toolbar ist statisch
pass
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
p = data.get("payload") or {}
if not isinstance(p, dict): p = {}
if t == "READY":
self._on_ready()
elif t == "RUN":
cmd = p.get("cmd")
if cmd and isinstance(cmd, str):
# Whitelist: alles muss mit "_" beginnen (Rhino-Befehl) und
# darf keine Zeilenumbrueche oder Semikolons enthalten.
cmd = cmd.strip()
if cmd.startswith("_") and "\n" not in cmd and ";" not in cmd:
try:
Rhino.RhinoApp.RunScript(cmd, False)
print("[WERKZEUGE] {}".format(cmd))
except Exception as ex:
print("[WERKZEUGE] RunScript-Fehler:", ex)
else:
print("[WERKZEUGE] Befehl ignoriert (kein '_' Praefix oder unsicher):", cmd)
def _bridge_factory():
return WerkzeugeBridge()
panel_base.register_and_open("werkzeuge", "WERKZEUGE", PANEL_GUID_STR, _bridge_factory,
icon_spec=("W", "#3a6fa8"))