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:
@@ -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 & Rendering" selected_item="d9ac0269-811b-47d1-aa33-777986b13715" can_be_empty="True" display_style="BitmapAndText">
|
||||
<name>
|
||||
<locale_1033>Display & 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>表示 & レンダリング</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>显示 & 渲染</locale_2052>
|
||||
<locale_1028>顯示 & 彩現</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><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"><clipPath id="a"><path clip-rule="evenodd" d="m0 0h36v36h-36z"/></clipPath><g clip-path="url(#a)" fill-opacity=".996078" transform="matrix(1.3333333 0 0 -1.3333333 0 48)"><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"/><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"/></g></svg></light_svg>
|
||||
<dark_svg><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">
|
||||
<g clip-path="url(#a)" fill-opacity=".996078" transform="matrix(1.3333333 0 0 -1.3333333 0 48)">
|
||||
<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" />
|
||||
<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" />
|
||||
</g>
|
||||
<clipPath id="a">
|
||||
<path clip-rule="evenodd" d="m0 0h36v36h-36z" fill="" stroke-width="" x_rma_id="0" />
|
||||
</clipPath>
|
||||
<g clip-path="url(#a)" fill-opacity=".996078" transform="matrix(1.3333333 0 0 -1.3333333 0 48)">
|
||||
<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" />
|
||||
<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" />
|
||||
</g>
|
||||
</svg></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>
|
||||
@@ -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
|
||||
```
|
||||
@@ -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"))
|
||||
@@ -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.")
|
||||
@@ -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")
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+1635
File diff suppressed because it is too large
Load Diff
@@ -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("============================================")
|
||||
@@ -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()
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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"))
|
||||
@@ -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)")
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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"))
|
||||
@@ -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")
|
||||
@@ -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"))
|
||||
Reference in New Issue
Block a user