From 61b4cde488f75e00788fdbeaf0018f3864a8bbfe Mon Sep 17 00:00:00 2001 From: SAM-BIM-AI Date: Mon, 28 Sep 2026 18:50:23 +0200 Subject: [PATCH 1/6] feat(gh-icons): apply SAM GH icon design system to all 9 SAM_Multitasker Grasshopper objects Propagates the SAM Grasshopper icon redesign (SAM-BIM/SAM#166) to SAM_Multitasker: - design/grasshopper-icons: shared SAM-BIM icon kit (icons.py/render.py/ sam_classify.py vendored verbatim from SAM#166 @ cf4d924a, hash-checked; SAM-BIM extension glyphs v1; repo_rules.py), manifest (source of truth), SVG sources, 24x24 PNGs, contact sheet, Rhino 8 icon load test. - 9/9 objects wired through the project's existing icon mechanism; only the icon token in each Icon getter changes. No GUID/API/behaviour change. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 03c2df7ebb1aa0d230a9c8c552ce3ee486c995f8) --- .../Classes/GooMultitaskerInput.cs | 2 +- .../Classes/GooMultitaskerOutput.cs | 2 +- .../Classes/GooMultitaskerVariable.cs | 2 +- .../Classes/GooScript.cs | 2 +- .../Component/SAMMultitaskerDefaultScripts.cs | 2 +- .../SAMMultitaskerMultitaskerInput.cs | 2 +- .../SAMMultitaskerMultitaskerMode.cs | 2 +- .../SAMMultitaskerMultitaskerVariable.cs | 2 +- .../Component/SAMMultitaskerRun.cs | 2 +- .../Properties/Resources.Designer.cs | 90 ++ .../Properties/Resources.resx | 27 + .../Resources/Icons/SAM_GH_Result.png | Bin 0 -> 334 bytes .../Resources/Icons/SAM_GH_Script.png | Bin 0 -> 480 bytes .../Icons/SAM_GH_ScriptLibraryGet.png | Bin 0 -> 892 bytes .../Resources/Icons/SAM_GH_Settings.png | Bin 0 -> 335 bytes .../Resources/Icons/SAM_GH_SettingsCreate.png | Bin 0 -> 657 bytes .../Resources/Icons/SAM_GH_TasksRun.png | Bin 0 -> 787 bytes .../Resources/Icons/SAM_GH_TasksValue.png | Bin 0 -> 795 bytes .../Resources/Icons/SAM_GH_Value.png | Bin 0 -> 415 bytes .../Resources/Icons/SAM_GH_ValueCreate.png | Bin 0 -> 692 bytes design/grasshopper-icons/.gitignore | 3 + .../grasshopper-icons/ICON_DESIGN_SYSTEM.md | 163 ++++ .../ICON_DESIGN_SYSTEM_EXT.md | 80 ++ design/grasshopper-icons/manifest.csv | 10 + design/grasshopper-icons/manifest.json | 254 ++++++ design/grasshopper-icons/manifest_raw.json | 155 ++++ design/grasshopper-icons/png/24/result.png | Bin 0 -> 334 bytes design/grasshopper-icons/png/24/script.png | Bin 0 -> 480 bytes .../png/24/script_library_get.png | Bin 0 -> 892 bytes design/grasshopper-icons/png/24/settings.png | Bin 0 -> 335 bytes .../png/24/settings_create.png | Bin 0 -> 657 bytes design/grasshopper-icons/png/24/tasks_run.png | Bin 0 -> 787 bytes .../grasshopper-icons/png/24/tasks_value.png | Bin 0 -> 795 bytes design/grasshopper-icons/png/24/value.png | Bin 0 -> 415 bytes .../grasshopper-icons/png/24/value_create.png | Bin 0 -> 692 bytes design/grasshopper-icons/review/REVIEW.md | 7 + .../review/contact_sheet.html | 12 + .../review/contact_sheet.png | Bin 0 -> 63159 bytes design/grasshopper-icons/svg/result.svg | 1 + design/grasshopper-icons/svg/script.svg | 1 + .../svg/script_library_get.svg | 1 + design/grasshopper-icons/svg/settings.svg | 1 + .../grasshopper-icons/svg/settings_create.svg | 1 + design/grasshopper-icons/svg/tasks_run.svg | 1 + design/grasshopper-icons/svg/tasks_value.svg | 1 + design/grasshopper-icons/svg/value.svg | 1 + design/grasshopper-icons/svg/value_create.svg | 1 + .../tests/GhIconTest/GhIconTest.csproj | 21 + .../tests/GhIconTest/IconTests.cs | 98 +++ .../tests/GhIconTest/README.md | 22 + .../GhIconTest/Rhino.Testing.Configs.xml | 5 + design/grasshopper-icons/tools/build.py | 145 ++++ .../tools/check_assemblies.py | 74 ++ .../grasshopper-icons/tools/check_source.py | 97 +++ design/grasshopper-icons/tools/classify.py | 176 ++++ design/grasshopper-icons/tools/icons.py | 782 ++++++++++++++++++ design/grasshopper-icons/tools/icons_ext.py | 223 +++++ design/grasshopper-icons/tools/integrate.py | 208 +++++ design/grasshopper-icons/tools/inventory.py | 147 ++++ design/grasshopper-icons/tools/render.py | 132 +++ design/grasshopper-icons/tools/repo_rules.py | 15 + .../grasshopper-icons/tools/sam_classify.py | 422 ++++++++++ 62 files changed, 3386 insertions(+), 9 deletions(-) create mode 100644 Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_Result.png create mode 100644 Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_Script.png create mode 100644 Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_ScriptLibraryGet.png create mode 100644 Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_Settings.png create mode 100644 Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_SettingsCreate.png create mode 100644 Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_TasksRun.png create mode 100644 Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_TasksValue.png create mode 100644 Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_Value.png create mode 100644 Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_ValueCreate.png create mode 100644 design/grasshopper-icons/.gitignore create mode 100644 design/grasshopper-icons/ICON_DESIGN_SYSTEM.md create mode 100644 design/grasshopper-icons/ICON_DESIGN_SYSTEM_EXT.md create mode 100644 design/grasshopper-icons/manifest.csv create mode 100644 design/grasshopper-icons/manifest.json create mode 100644 design/grasshopper-icons/manifest_raw.json create mode 100644 design/grasshopper-icons/png/24/result.png create mode 100644 design/grasshopper-icons/png/24/script.png create mode 100644 design/grasshopper-icons/png/24/script_library_get.png create mode 100644 design/grasshopper-icons/png/24/settings.png create mode 100644 design/grasshopper-icons/png/24/settings_create.png create mode 100644 design/grasshopper-icons/png/24/tasks_run.png create mode 100644 design/grasshopper-icons/png/24/tasks_value.png create mode 100644 design/grasshopper-icons/png/24/value.png create mode 100644 design/grasshopper-icons/png/24/value_create.png create mode 100644 design/grasshopper-icons/review/REVIEW.md create mode 100644 design/grasshopper-icons/review/contact_sheet.html create mode 100644 design/grasshopper-icons/review/contact_sheet.png create mode 100644 design/grasshopper-icons/svg/result.svg create mode 100644 design/grasshopper-icons/svg/script.svg create mode 100644 design/grasshopper-icons/svg/script_library_get.svg create mode 100644 design/grasshopper-icons/svg/settings.svg create mode 100644 design/grasshopper-icons/svg/settings_create.svg create mode 100644 design/grasshopper-icons/svg/tasks_run.svg create mode 100644 design/grasshopper-icons/svg/tasks_value.svg create mode 100644 design/grasshopper-icons/svg/value.svg create mode 100644 design/grasshopper-icons/svg/value_create.svg create mode 100644 design/grasshopper-icons/tests/GhIconTest/GhIconTest.csproj create mode 100644 design/grasshopper-icons/tests/GhIconTest/IconTests.cs create mode 100644 design/grasshopper-icons/tests/GhIconTest/README.md create mode 100644 design/grasshopper-icons/tests/GhIconTest/Rhino.Testing.Configs.xml create mode 100644 design/grasshopper-icons/tools/build.py create mode 100644 design/grasshopper-icons/tools/check_assemblies.py create mode 100644 design/grasshopper-icons/tools/check_source.py create mode 100644 design/grasshopper-icons/tools/classify.py create mode 100644 design/grasshopper-icons/tools/icons.py create mode 100644 design/grasshopper-icons/tools/icons_ext.py create mode 100644 design/grasshopper-icons/tools/integrate.py create mode 100644 design/grasshopper-icons/tools/inventory.py create mode 100644 design/grasshopper-icons/tools/render.py create mode 100644 design/grasshopper-icons/tools/repo_rules.py create mode 100644 design/grasshopper-icons/tools/sam_classify.py diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerInput.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerInput.cs index 27f214e..2cfdf87 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerInput.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerInput.cs @@ -73,7 +73,7 @@ public class GooMultitaskerInputParam : GH_PersistentParam { public override Guid ComponentGuid => new Guid("fdac916f-3e4f-4df6-8029-ea950823b4e8"); - protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_Small)); + protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_GH_Value)); public GooMultitaskerInputParam() : base("MultitaskerInput", "MultitaskerInput", "SAM Multitasker MultitaskerInput", "Params", "SAM") diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerOutput.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerOutput.cs index c53d249..5c951e4 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerOutput.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerOutput.cs @@ -41,7 +41,7 @@ public class GooMultitaskerOutputParam : GH_PersistentParam new Guid("cfc14cb0-df0a-4774-858a-7a652551b3bb"); - protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_Small)); + protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_GH_Result)); public GooMultitaskerOutputParam() : base("MultitaskerOutput", "MultitaskerOutput", "SAM Multitasker MultitaskerOutput", "Params", "SAM") diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerVariable.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerVariable.cs index fbbb32b..bf67699 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerVariable.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerVariable.cs @@ -73,7 +73,7 @@ public class GooMultitaskerVariableParam : GH_PersistentParam new Guid("e2a46e73-b05f-427a-9c51-f6340f9fd98f"); - protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_Small)); + protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_GH_Settings)); public GooMultitaskerVariableParam() : base("MultitaskerVariable", "MultitaskerVariable", "SAM Multitasker MultitaskerVariable", "Params", "SAM") diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooScript.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooScript.cs index 210ff2a..c69fa9e 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooScript.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooScript.cs @@ -68,7 +68,7 @@ public class GooScriptParam : GH_PersistentParam { public override Guid ComponentGuid => new Guid("400a16f6-a61d-4848-a9d6-6f0ade098ba3"); - protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_Small)); + protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_GH_Script)); public GooScriptParam() : base("Script", "Script", "SAM Script", "Params", "SAM") diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerDefaultScripts.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerDefaultScripts.cs index 0d8bdd9..036487c 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerDefaultScripts.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerDefaultScripts.cs @@ -24,7 +24,7 @@ public class SAMMultitaskerDefaultScripts : GH_SAMVariableOutputParameterCompone /// /// Provides an Icon for the component. /// - protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_Small)); + protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_GH_ScriptLibraryGet)); public override GH_Exposure Exposure => GH_Exposure.secondary; diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerInput.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerInput.cs index 8470f15..c923ba5 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerInput.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerInput.cs @@ -25,7 +25,7 @@ public class SAMMultitaskerMultitaskerInput : GH_SAMVariableOutputParameterCompo /// /// Provides an Icon for the component. /// - protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_Small)); + protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_GH_ValueCreate)); public override GH_Exposure Exposure => GH_Exposure.secondary; diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerMode.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerMode.cs index 1c5e0d2..8fe2b45 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerMode.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerMode.cs @@ -21,7 +21,7 @@ public class SAMMultitaskerMultitaskerMode : GH_SAMEnumComponent /// Provides an Icon for the component. /// - protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_Small)); + protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_GH_TasksValue)); /// /// About SAM Enum Component diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerVariable.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerVariable.cs index eecca9b..8a0f845 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerVariable.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerVariable.cs @@ -25,7 +25,7 @@ public class SAMMultitaskerMultitaskerVariable : GH_SAMVariableOutputParameterCo /// /// Provides an Icon for the component. /// - protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_Small)); + protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_GH_SettingsCreate)); public override GH_Exposure Exposure => GH_Exposure.secondary; diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerRun.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerRun.cs index 73788d3..7cce162 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerRun.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerRun.cs @@ -28,7 +28,7 @@ public class SAMMultitaskerRun : GH_SAMVariableOutputParameterComponent /// /// Provides an Icon for the component. /// - protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_Small)); + protected override Bitmap Icon => new Bitmap(new MemoryStream(Resources.SAM_GH_TasksRun)); public override GH_Exposure Exposure => GH_Exposure.secondary; diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Properties/Resources.Designer.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Properties/Resources.Designer.cs index 4bf4152..fca7708 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Properties/Resources.Designer.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Properties/Resources.Designer.cs @@ -79,5 +79,95 @@ internal static byte[] SAM_Small { return ((byte[])(obj)); } } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] SAM_GH_Result { + get { + object obj = ResourceManager.GetObject("SAM_GH_Result", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] SAM_GH_Script { + get { + object obj = ResourceManager.GetObject("SAM_GH_Script", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] SAM_GH_ScriptLibraryGet { + get { + object obj = ResourceManager.GetObject("SAM_GH_ScriptLibraryGet", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] SAM_GH_Settings { + get { + object obj = ResourceManager.GetObject("SAM_GH_Settings", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] SAM_GH_SettingsCreate { + get { + object obj = ResourceManager.GetObject("SAM_GH_SettingsCreate", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] SAM_GH_TasksRun { + get { + object obj = ResourceManager.GetObject("SAM_GH_TasksRun", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] SAM_GH_TasksValue { + get { + object obj = ResourceManager.GetObject("SAM_GH_TasksValue", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] SAM_GH_Value { + get { + object obj = ResourceManager.GetObject("SAM_GH_Value", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] SAM_GH_ValueCreate { + get { + object obj = ResourceManager.GetObject("SAM_GH_ValueCreate", resourceCulture); + return ((byte[])(obj)); + } + } } } diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Properties/Resources.resx b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Properties/Resources.resx index 6df4d6d..ee1aee5 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Properties/Resources.resx +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Properties/Resources.resx @@ -124,4 +124,31 @@ ..\Resources\SAM_Small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\Icons\SAM_GH_Result.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\Icons\SAM_GH_Script.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\Icons\SAM_GH_ScriptLibraryGet.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\Icons\SAM_GH_Settings.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\Icons\SAM_GH_SettingsCreate.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\Icons\SAM_GH_TasksRun.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\Icons\SAM_GH_TasksValue.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\Icons\SAM_GH_Value.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\Icons\SAM_GH_ValueCreate.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_Result.png b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_Result.png new file mode 100644 index 0000000000000000000000000000000000000000..5ecc83b93ab145ae5c33962eb59e2963beff333c GIT binary patch literal 334 zcmV-U0kQsxP)LDcpf`Z~{6w0u5o2BCzqA zM;D;*PmE0gOt-VM`|Z59yFkYw4b}tzHa?9BVA&c#RnJ9aw%S1OyjVQ;i|S32ws#M< zuy?$Tr~6l2o!$1zYP}|ao)H9_1}3N$ptOT93=c`hE3wIB-NTduNXEmR{wx{~kSufD zT|cC`+BCEj(1xLv5`EXu-)ZvC08~{;M!6%> z7YCOTB)qygRgG0u3%_esodY-(kuNiRBkJTpfaHa>c3K3`EXyV&{f~cM2*B0J`M#e3 gxUtq+{KwymcPD?1N+rS{(EtDd07*qoM6N<$g1P98)Bpeg literal 0 HcmV?d00001 diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_Script.png b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_Script.png new file mode 100644 index 0000000000000000000000000000000000000000..0234a728986475901dc878c00163b6b4787f1654 GIT binary patch literal 480 zcmV<60U!Q}P)F|9LGQJDZ7rc(~K=ftRiAoNF?pv!tfA;Aj%?m@zh21;6d54A{_yeP=*&Z;{rI6p5MyW1XYb}oTmcs$H(sbWt9M>lnUfsL;(DJK5qa= zm3JD51om7RsZtt<)Y^6Bp`+7c>E#rIBey~;auvXAZ=?N6jE9eg8K1rv3ZMyy8iw&0 zz}YR5+t?;K)JK1+n?!G%jqDcwv79GGc%HXduLf26`RzB0(;4=5Tv9{VnOPVMFGZC) za6JG@`{jlJ&9#Obcen)jO#l)-aR%=8{`D}@(tIv}0%EppyCOm(ldP1& zahw>iUt8p|oX&b95)r?)Mb7|7z(=K&Cn5w~Wf95N1N`}}ZQD+ZNMG3B^@T3fxK04? W!nH~Y@6j9p00006%i6<6!)>vPd)R8|s`latcr za$N`1vN!Pgd_B6ZAI@g8)^aW!4hOxxy>xYT;qiFTG_4Lu7jUJN^K?4>+UV%0HIYb= zNF;E%TsIX^6ovl&esZ~7N!$cbS66qJs;UhD`Fy_GFbqp97DLlC0EUN$iwVum&A8p} z;yRT|;qiFTb-lEJ-EKcBgy<>SSC>oJ-Q8U>K}rcgV`C%HXteA@D!>zkq>9>32!g?2 zMdSdA5TXhouzfQPUPsvkDJ7XqreZ@T0MiDQ$6sm(VCcXIk#DEhZO{~erNtaadp=;% z$no5fojkZ>%lZlcT#fwB>rbB~F_&cbi|uT>r(t~sZ1pufd7uNwts9vbpW?Tx*H%R| zGc!X~RaF+yOTNL&#~;Ia+eXg57v{UOKQK7*43-*m@!;uGm-uMt!c7G_J3G@>tMwp| zyFp-SF~`rBW1Kk^=BrO8Y1-uE*Ps4aL6AviOA^Q9an6sOm*!h)C2*+O<=JI6nL|Q| z(*VM5x2FNC5Tc?|6lO+MRYIW?6Ad~4$RUjSVCB|>oa zM*=?oKPD8er4q~~<6BMZ(d*B*@8PM3x;Z}nF8-#?H+~gs3AfET;dM5(Z@)WOBp*EY zHuv7XEO4jGgWKku5Nj2XQnp>33Xfix3R7#bGBkE_1^&*SH}V0WzirJo5JG$*r97r( zl6(6;JyzVWF_69EO4*_t`iaQwl~`o~S0{%Cemsefo SMvKz`0000^GOB+ h2czkI6pRAe000h(Y4DKbH-G>D002ovPDHLkV1iQqhS2~3 literal 0 HcmV?d00001 diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_SettingsCreate.png b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_SettingsCreate.png new file mode 100644 index 0000000000000000000000000000000000000000..1cab885ad58cf0782a0a07f2e7fa35c968d9759a GIT binary patch literal 657 zcmV;C0&e|@P)-kcUX6Qq;Rnb~yUF1~Z`Irp4%zwZM7yGdI) zEQIiRJf4H9s{S-h({d8ya+urAIUfLENRl)Hz~G$sIVn(bSouX+VOJVk_sI1ZcB3?) zBC36_^0<|0(=^}xE0zHO=R61?n6*ubRqsDqTz>pgWybgXVH6);eVqaDan1F}FpS4J zjflAz+wfxfPdsCcvUaeUan8@Vi(K6emn-Fp171wOPvS+_l>N)1pswpr3PQJ%G9D5_ zPopmJuzpr0pPZ6*Xg?M`x4WyXPM5` z&S#%8V22{XyG_pE8-0@uKwHlIj_Kxnb!(UgM-vnoH)zqaY0EG^p^eD_RM3cx*<&xnZVLH}^PrM-6o6~zjOfSGR=?w_r~sk+jejIoZqe^?@_ rS+MLLEt;IwqRH6>%kCkfn)Lo}7)KDU#_@@800000NkvXXu0mjf^V>Oe literal 0 HcmV?d00001 diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_TasksRun.png b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_TasksRun.png new file mode 100644 index 0000000000000000000000000000000000000000..2aa0733e9f8667c10b7be7531a095f3f52e2e971 GIT binary patch literal 787 zcmV+u1MK{XP)e8u;^@9%s6 zd=Ie8CT*NTh&GZZ0|O)}TPzkw0dOmT5CE8_`J--x29xLaX+Z&hj4%INsTfym+kT?! zdO}szUeEK=uIv6&6#(G)k<)v$)=NJGzNl<}(q4@%{c4)#FC-%*kA=hGqtR&en&UXX zf9)^t_6$M*K=RMOW{Ruf8buEZcspDA2;gq1RC=WA`aw<8dOgoeJC3uw*^ujm5SK~r z4+4<<+p?_30GusX5R1k3s}VW9=K6<&0<@?y55&eT%W?pObzMKPy1F{MDL|5>j>OTr z_=W3%`SGl8KK^F5c*8W!49PMPmoFnyr*v6ARVIvV0Z5)%BJ^ zmgO$PFh1C}{h-)z;QFm&@gs|4T(xsYs<#$*!)hi=O9EQItw!W8=K0X}2Uv8v7FP z&88>{dU|>e8it{^wY9-@T>wBN5^3)5?;j<32f%G<{iFR{w^zG=?Cjn^sdzoH{Ly*8 z)>$f*-VF~Azv%4jL|y&2hsP7Z=&x-Ay3`3x&ecuE@0{NjpzM z@_9>33xyEO=kv5}o0F51OeT{QLa?>9l_a0<1aNL*VuC^l4h#&iqoacxEfYd;YHF%N z^2|=FxR=Rf7>~z4CyvMC%w#eodHcKcn`D#gxBy RP4NH#002ovPDHLkV1m5RciaE~ literal 0 HcmV?d00001 diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_TasksValue.png b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_TasksValue.png new file mode 100644 index 0000000000000000000000000000000000000000..3161c910e9a36206c93f31168d1b250dcdc93d1a GIT binary patch literal 795 zcmV+$1LXXPP)OlTlP1S1$hZ_`352#i{Up=gU}VMdrCQZ0h$ z2ZTU^7Gew{LD(Yk;cO~}{?ScS3G?Q@-i&3>cUq*7!SwvWU1xI_=bZbU^PO|Q3v95l zrBl^)hQD6b0N5+;ARG>ti9w-tKIhhWB}gIPdmuc*Iky1>WLb{Q&CMDA1YnGvsM=9ddAP&# zemuVRzVe-6o>x`%ArWl_a6uHs`<(M008XJT3Wc{wMJs8!DC{p11mO&TGmDf#5QK}0 zqI|O~>t>-X%H>SQSqZ?;yz98Gdz*+35K%LLkFM)pwk+#b!67S(QX3F`BklsiUI2n0 zrZev*IOiz<{^H`|J^6fo;=fcBl!`xgNZ5CmbNwzl>yfQyW=;XeUZZ9bn5 z-QC?4oO7|cxfzj21OU+9-X7C+JxN4Y0Ccj&hjyx@RJyVAV5z56ym~VG-OkLP&1SP7 zdwYAIPESvxv9S?~q5uFm=SU`#sHv%;wzjs!nx%q>9@W>^H$C<)zwXO zbaXTXgF%dqjV<$4S63sQP9q+Vr~KP7;2?BeCjdQc zJ@gRK^}fEob3;QzC@=s0hSKRYbX`YFOUot3Sm(OGL?YTwM0X9t_+%KyCnCB_MBA7A ZzW^QOU0)SWvtIxJ002ovPDHLkV1g28aJB#d literal 0 HcmV?d00001 diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_Value.png b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_Value.png new file mode 100644 index 0000000000000000000000000000000000000000..5d03a7adf3f8905939348f8780cbb231c97825f1 GIT binary patch literal 415 zcmV;Q0bu@#P)7Hk^Cq=cu6%5(Y#*QqlwF*D*d z{O7kn4AbK;@-Z+lun}c}u&7`Kp9ougK(#72A;XCQOels63&e5qvsQ;Tt8=rmF;gWD z;6C8v1m7z^)T)e_; z3>P-OX9)3&WbpF{VYsmQ1DVML$p?&#jQ^Nf7=ObA|NZ~RASETm@b~XuhJSzmQYV@6 z@$q?Z3$U+^?$+dbb?+O)@g=VqSlCz?%p!ysl#B%kr6Pv+@89z>Ffe?<>p@{*fjCY< zwyCkbT09)wEJV4GkOe5I=FT0004>YbJ{~Bi}nBj002ov JPDHLkV1kk>z}El( literal 0 HcmV?d00001 diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_ValueCreate.png b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_ValueCreate.png new file mode 100644 index 0000000000000000000000000000000000000000..ac6fa61acbaa90f40f56b921326318ca378cba4c GIT binary patch literal 692 zcmV;l0!#ggP)7-*kR0ElTlpt@Zb zlzg$h-d|=*MV?M@8^~2>o7G|QH}!gL7LnhU86*`bic%}uECVfl|)6h*0)74c!y#v4)YUd({8=PJo2*m77&Cc+CDsm2GNEZ2 z48wqF7~5l0MNw*PPHCj|#$I`Tav3l0%|j3cRJY`zw91*vMF@w(xd6UwDNt3NZB~~U zICZnao?{czsxZ|98(b6$MefEI6Me%M-^CIu#^2jykERAZyR2;u-CldDT_~|vVxQ$C zM74AY0KA(0gJfZT#@O>MJ>V(I8%cbn6PwzP` z`F#L-GL}w6hlgIwkh^1)+#RDd@o9;IQ_&=T{CdtLq9a@u?d|FvnM?w^#6z+EG)DKM zj|@PkkSY6YM6WbV!kshw;gAIsxg-FZLl%(p zVZ7=HwJ@K(dpn+bULORgG5xZ amj4FzA=`PGONzMw0000`) are not drawn. Variants that differ only by qualifier share one icon **intentionally**. +7. **Badge glyph weight** (from the live Rhino review): line glyphs use a stroke of at least 1.6 px (outline shapes at least 1.3 px). Arrows use filled heads. Transfer badges use one bold solid arrow (↓ import, ↑ export, ⇄ convert) and no base line. Thinner glyphs collapsed to a dot at native size. +8. Review every icon on all three Grasshopper body states: normal grey, **orange warning** (the default for a freshly placed component with unconnected inputs) and a dark skin. The yellow Change disc keeps its white ring, so it still reads on the orange warning body. + +## 7. Collision rules + +* Singular vs plural of the same query/change always differ (the plural modifier). +* Create vs Add always differ (single green object vs neutral + green). +* Get vs Set always differ (colour family **and** mirrored arrow). +* `tools/build.py` fails the review if two different icon ids rasterise to identical pixels. The current result is 0 identical groups. + +## 8. Exceptions + +* `SAM.About` / `SAM.Version`: SAM emblem tile + info mark (identity, not an analytical object). +* `SAM.Test` (`A_SAMAnalytical`): it had no Icon override. It gets `object + validate`. This is the only class that gains a new `Icon` line. +* `AssemblyInfo.Icon` / `AssemblyIcon` (the Grasshopper tab/plugin identity) are **not** component icons and keep the SAM logo. +* Hidden/obsolete components get the same grammar icon as their live equivalent. They appear only in old definitions. + +## 9. Pipeline (deterministic) + +``` +tools/inventory.py -> manifest_raw.json (parse C#: every class with ComponentGuid) +tools/classify.py -> manifest.json / .csv (rules + explicit OVERRIDES table) +tools/build.py -> svg/ png/24/ review/batch_*.png SAM_GH_ICON_LIBRARY.png (+ identical-pixel check) +tools/integrate.py -> Grasshopper//Resources/Icons/*.png, Resources.resx, Resources.Designer.cs, Icon getters +tools/catalogue.py -> review/_catalogue/ (all glyphs, badges and modifiers) +tools/check_assemblies.py (every required icon embedded at 24x24 in the built DLLs) +tools/live_review_rhino.py (real Rhino 8 / GH canvas capture, light + dark; see file header) +``` + +The rasteriser renders each SVG alone at a fixed page position, so an icon's PNG depends only on its own SVG. Adding icons never changes existing PNGs. + +Requirements: Python 3 with Pillow, and Microsoft Edge (headless) as the SVG rasteriser. +`svg/` is the canonical editable source. The PNGs are generated, never hand-edited. diff --git a/design/grasshopper-icons/ICON_DESIGN_SYSTEM_EXT.md b/design/grasshopper-icons/ICON_DESIGN_SYSTEM_EXT.md new file mode 100644 index 0000000..a548d0d --- /dev/null +++ b/design/grasshopper-icons/ICON_DESIGN_SYSTEM_EXT.md @@ -0,0 +1,80 @@ +# SAM-BIM icon design system: extension v1 + +Status: **frozen v1** (2026-09-28). It extends the SAM Grasshopper icon design system (`ICON_DESIGN_SYSTEM.md`, +vendored verbatim from SAM-BIM/SAM#166 @ `cf4d924a`) to the SAM-BIM plugin repositories. +`tools/icons_ext.py` implements this file and is **byte-identical in every SAM-BIM repository**. + +## What the extension may and may not do + +* It **adds** object glyphs for domains where SAM has no noun. It also adds **one** verb, `run`. +* It does **not** change the grammar (object glyph + SAM-green subject + operation badge), the palette, the 7 badge + families and their colours, any SAM glyph, badge or modifier, or the construction rules (24×24, one 1.1 px outline, + flat fills, no text, bottom-right badge quadrant reserved). +* SAM glyphs are reused wherever the noun already exists: space, panel, construction, shell, model, system, fan, + weather, file, table, result and so on. An object that SAM already draws is never redrawn. +* An icon id that also exists in SAM (e.g. `model_import`, `shell_union_plural`) renders **pixel-identically** to + SAM's icon, because `icons.py` and `render.py` are the same bytes. + +## New object glyphs (v1) + +| Glyph | Motif | Used for | +|---|---|---| +| `mollierChart` | psychrometric chart axes + saturation curve, green area | Mollier/psychrometric chart, diagram, generic Mollier object | +| `mollierPoint` | chart (thin) + green state point | MollierPoint | +| `mollierProcess` | chart (thin) + two green states joined | MollierProcess (generic, undefined, by two points); MollierGroup uses SAM `group` | +| `processHeating` / `processCooling` | chart + bold green arrow → / ← (mirrored pair) | heating / cooling processes | +| `processHumidification` | chart + arrow ↑ | isothermal and steam humidification | +| `processAdiabatic` | chart + arrow ↖ | adiabatic humidification | +| `processRoom` | chart + arrow ↗ | room (load) process | +| `processMixing` | chart + two arrows converging on a state | mixing process | +| `processHeatRecovery` | chart + ⇄ arrows | heat recovery process | +| `processFan` | chart + fan disc + short arrow → | fan process | +| `cellComplex` | isometric solid divided into 2×2×2 cells | Topologic/OCCT CellComplex, Topology | +| `algorithm` | objective curve with the green optimum | GenOpt / solver algorithms | +| `objective` | target | optimisation objective | +| `script` | terminal `>_` (dark body, green prompt) | Python / GenOpt / Excel macro scripts | +| `tasks` | three green lanes running in parallel | Multitasker | +| `sound` | speaker + sound waves | acoustic band values and absorption | +| `view` | drawing sheet: green plan + title block | Revit views and sheets | +| `element` | green cube on a neutral base plate | Revit/BIM element instances and ids | +| `energyCentre` | plant block with a stack | SystemEnergyCentre | +| `plantRoom` | SAM `floor` cutaway room with a green plant unit | SystemPlantRoom | +| `connector` | two green units joined by a pipe | system connectors, connected objects | +| `sunPath` | sun + green direction arrow to the ground | sun direction / sun position | +| `solar` | sun + parallel rays onto a green surface | irradiance, sun exposure, solar simulation | + +Process arrows use the mid subject green `#659335` (SAM's `G_RIGHT`) at 2.2 px. It stays readable on the normal, +orange-warning and dark Grasshopper bodies, where the deep green `#277A45` disappears on dark. +Chart glyphs never take the plural modifier (`classify.py` `NO_PLURAL`): stacked charts do not read at 24 px. + +## New verb (v1) + +| Verb | Family | Glyph | Leading words | +|---|---|---|---| +| `run` | **Evaluate** (existing ink disc, white glyph) | ▶ filled play triangle | Run, Simulate, Execute, Solve, Workflow | + +## Classification (tools/classify.py) + +Rules, in order: `repo_rules.OVERRIDES` → SAM `OVERRIDES` → verbs (repo, SAM, extension) → nouns +(repo, extension, SAM) → params (repo, extension, SAM). Plugin name prefixes (`SAMMollier.`, `Tas.`, `SAMOCCT.`, `Revit.` …) +are stripped the way SAM strips `SAMAnalytical.`. Two conventions apply to interop components: + +* external → SAM (`X.SAMAnalytical`, `FromX`, `Open…`) = **import ↓**; SAM → external (`SAMAnalytical.X`, `ToX`, `Save…`) = **export ↑**. +* The object glyph is the SAM object that is read or written (model, geometry, result…), never a vendor logo. + +`repo_rules.py` holds a repository's explicit decisions (overrides, param nouns). Each entry is one line with a reason when +it is not obvious. + +## Tooling (identical in every repo, except `repo_rules.py`) + +``` +tools/inventory.py -> manifest_raw.json (every non-abstract class declaring ComponentGuid, any project layout) +tools/classify.py -> manifest.json/.csv (source of truth: object, op, modifiers, icon id, resource, origin) +tools/build.py -> svg/ png/24/ review/contact_sheet.png review/REVIEW.md (+ identical-pixel check) +tools/integrate.py -> Resources/Icons/*.png, resx + Designer (or the project's icon holder class), Icon getters +tools/check_source.py -> vendored-file hashes, icon-token-only C# diff, ComponentGuid set unchanged +tools/check_assemblies.py -> built .gha/.dll embed every required 24x24 icon +tests/GhIconTest/ -> real Rhino 8 / Grasshopper: every object loads by GUID with its redesigned icon +``` +Vendored verbatim from SAM#166 (hash-checked by `check_source.py`): `icons.py`, `render.py`, `sam_classify.py` +(SAM's `classify.py`), and `ICON_DESIGN_SYSTEM.md`. Requirements: Python 3 + Pillow, Microsoft Edge (headless rasteriser). diff --git a/design/grasshopper-icons/manifest.csv b/design/grasshopper-icons/manifest.csv new file mode 100644 index 0000000..19ac04d --- /dev/null +++ b/design/grasshopper-icons/manifest.csv @@ -0,0 +1,10 @@ +class,name,nickname,category,subcategory,project,project_dir,source,guid,kind,exposure,obsolete,current_icon,icon_holder,object,op,plural,container,glyph_origin,badge_origin,rule,note,icon_id,resource,icon_file,status +GooMultitaskerOutputParam,MultitaskerOutput,MultitaskerOutput,Params,SAM,SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerOutput.cs,cfc14cb0-df0a-4774-858a-7a652551b3bb,param,,False,SAM_Small,Resources,result,,False,,SAM,,param:MultitaskerOutput,,result,SAM_GH_Result,svg/result.svg,integrated +GooScriptParam,Script,Script,Params,SAM,SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooScript.cs,400a16f6-a61d-4848-a9d6-6f0ade098ba3,param,,False,SAM_Small,Resources,script,,False,,SAM-BIM ext v1,,param:Script,,script,SAM_GH_Script,svg/script.svg,integrated +SAMMultitaskerDefaultScripts,SAMMultitasker.DefaultScripts,SAMMultitasker.DefaultScripts,SAM WIP,Multitasker,SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerDefaultScripts.cs,97d3d237-ec7b-421e-913c-0a692a244980,component,GH_Exposure.secondary,False,SAM_Small,Resources,script,get,False,library,SAM-BIM ext v1,SAM,override,,script_library_get,SAM_GH_ScriptLibraryGet,svg/script_library_get.svg,integrated +GooMultitaskerVariableParam,MultitaskerVariable,MultitaskerVariable,Params,SAM,SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerVariable.cs,e2a46e73-b05f-427a-9c51-f6340f9fd98f,param,,False,SAM_Small,Resources,settings,,False,,SAM,,param:MultitaskerVariable,,settings,SAM_GH_Settings,svg/settings.svg,integrated +SAMMultitaskerMultitaskerVariable,SAMMultitasker.MultitaskerVariable,SAMMultitasker.MultitaskerVariable,SAM WIP,Multitasker,SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerVariable.cs,5f36773d-521b-489c-86e5-25719c8346f0,component,GH_Exposure.secondary,False,SAM_Small,Resources,settings,create,False,,SAM,SAM,override,multitasker variable (varied setting),settings_create,SAM_GH_SettingsCreate,svg/settings_create.svg,integrated +SAMMultitaskerRun,SAMMultitasker.Run,SAMMultitasker.Run,SAM WIP,Multitasker,SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerRun.cs,f2fa4316-2d2c-426c-ab5f-efeb65c72456,component,GH_Exposure.secondary,False,SAM_Small,Resources,tasks,run,False,,SAM-BIM ext v1,SAM-BIM ext v1,override,,tasks_run,SAM_GH_TasksRun,svg/tasks_run.svg,integrated +SAMMultitaskerMultitaskerMode,SAMMultitasker.MultitaskerMode,SAMMultitasker.MultitaskerMode,SAM WIP,Multitasker,SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerMode.cs,1fd108c6-a6bf-478e-9a96-4604040801c6,component,,False,SAM_Small,Resources,tasks,value,False,,SAM-BIM ext v1,SAM,rule,,tasks_value,SAM_GH_TasksValue,svg/tasks_value.svg,integrated +GooMultitaskerInputParam,MultitaskerInput,MultitaskerInput,Params,SAM,SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerInput.cs,fdac916f-3e4f-4df6-8029-ea950823b4e8,param,,False,SAM_Small,Resources,value,,False,,SAM,,param:MultitaskerInput,,value,SAM_GH_Value,svg/value.svg,integrated +SAMMultitaskerMultitaskerInput,SAMMultitasker.MultitaskerInput,SAMMultitasker.MultitaskerInput,SAM WIP,Multitasker,SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker,Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerInput.cs,14c83218-ad03-48b4-8e1f-e24bc371be6b,component,GH_Exposure.secondary,False,SAM_Small,Resources,value,create,False,,SAM,SAM,override,multitasker input,value_create,SAM_GH_ValueCreate,svg/value_create.svg,integrated diff --git a/design/grasshopper-icons/manifest.json b/design/grasshopper-icons/manifest.json new file mode 100644 index 0000000..653f78e --- /dev/null +++ b/design/grasshopper-icons/manifest.json @@ -0,0 +1,254 @@ +[ + { + "class": "GooMultitaskerOutputParam", + "name": "MultitaskerOutput", + "nickname": "MultitaskerOutput", + "category": "Params", + "subcategory": "SAM", + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerOutput.cs", + "guid": "cfc14cb0-df0a-4774-858a-7a652551b3bb", + "kind": "param", + "exposure": null, + "obsolete": false, + "current_icon": "SAM_Small", + "icon_holder": "Resources", + "object": "result", + "op": null, + "plural": false, + "container": null, + "glyph_origin": "SAM", + "badge_origin": null, + "rule": "param:MultitaskerOutput", + "note": null, + "icon_id": "result", + "resource": "SAM_GH_Result", + "icon_file": "svg/result.svg", + "status": "integrated" + }, + { + "class": "GooScriptParam", + "name": "Script", + "nickname": "Script", + "category": "Params", + "subcategory": "SAM", + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooScript.cs", + "guid": "400a16f6-a61d-4848-a9d6-6f0ade098ba3", + "kind": "param", + "exposure": null, + "obsolete": false, + "current_icon": "SAM_Small", + "icon_holder": "Resources", + "object": "script", + "op": null, + "plural": false, + "container": null, + "glyph_origin": "SAM-BIM ext v1", + "badge_origin": null, + "rule": "param:Script", + "note": null, + "icon_id": "script", + "resource": "SAM_GH_Script", + "icon_file": "svg/script.svg", + "status": "integrated" + }, + { + "class": "SAMMultitaskerDefaultScripts", + "name": "SAMMultitasker.DefaultScripts", + "nickname": "SAMMultitasker.DefaultScripts", + "category": "SAM WIP", + "subcategory": "Multitasker", + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerDefaultScripts.cs", + "guid": "97d3d237-ec7b-421e-913c-0a692a244980", + "kind": "component", + "exposure": "GH_Exposure.secondary", + "obsolete": false, + "current_icon": "SAM_Small", + "icon_holder": "Resources", + "object": "script", + "op": "get", + "plural": false, + "container": "library", + "glyph_origin": "SAM-BIM ext v1", + "badge_origin": "SAM", + "rule": "override", + "note": null, + "icon_id": "script_library_get", + "resource": "SAM_GH_ScriptLibraryGet", + "icon_file": "svg/script_library_get.svg", + "status": "integrated" + }, + { + "class": "GooMultitaskerVariableParam", + "name": "MultitaskerVariable", + "nickname": "MultitaskerVariable", + "category": "Params", + "subcategory": "SAM", + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerVariable.cs", + "guid": "e2a46e73-b05f-427a-9c51-f6340f9fd98f", + "kind": "param", + "exposure": null, + "obsolete": false, + "current_icon": "SAM_Small", + "icon_holder": "Resources", + "object": "settings", + "op": null, + "plural": false, + "container": null, + "glyph_origin": "SAM", + "badge_origin": null, + "rule": "param:MultitaskerVariable", + "note": null, + "icon_id": "settings", + "resource": "SAM_GH_Settings", + "icon_file": "svg/settings.svg", + "status": "integrated" + }, + { + "class": "SAMMultitaskerMultitaskerVariable", + "name": "SAMMultitasker.MultitaskerVariable", + "nickname": "SAMMultitasker.MultitaskerVariable", + "category": "SAM WIP", + "subcategory": "Multitasker", + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerVariable.cs", + "guid": "5f36773d-521b-489c-86e5-25719c8346f0", + "kind": "component", + "exposure": "GH_Exposure.secondary", + "obsolete": false, + "current_icon": "SAM_Small", + "icon_holder": "Resources", + "object": "settings", + "op": "create", + "plural": false, + "container": null, + "glyph_origin": "SAM", + "badge_origin": "SAM", + "rule": "override", + "note": "multitasker variable (varied setting)", + "icon_id": "settings_create", + "resource": "SAM_GH_SettingsCreate", + "icon_file": "svg/settings_create.svg", + "status": "integrated" + }, + { + "class": "SAMMultitaskerRun", + "name": "SAMMultitasker.Run", + "nickname": "SAMMultitasker.Run", + "category": "SAM WIP", + "subcategory": "Multitasker", + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerRun.cs", + "guid": "f2fa4316-2d2c-426c-ab5f-efeb65c72456", + "kind": "component", + "exposure": "GH_Exposure.secondary", + "obsolete": false, + "current_icon": "SAM_Small", + "icon_holder": "Resources", + "object": "tasks", + "op": "run", + "plural": false, + "container": null, + "glyph_origin": "SAM-BIM ext v1", + "badge_origin": "SAM-BIM ext v1", + "rule": "override", + "note": null, + "icon_id": "tasks_run", + "resource": "SAM_GH_TasksRun", + "icon_file": "svg/tasks_run.svg", + "status": "integrated" + }, + { + "class": "SAMMultitaskerMultitaskerMode", + "name": "SAMMultitasker.MultitaskerMode", + "nickname": "SAMMultitasker.MultitaskerMode", + "category": "SAM WIP", + "subcategory": "Multitasker", + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerMode.cs", + "guid": "1fd108c6-a6bf-478e-9a96-4604040801c6", + "kind": "component", + "exposure": null, + "obsolete": false, + "current_icon": "SAM_Small", + "icon_holder": "Resources", + "object": "tasks", + "op": "value", + "plural": false, + "container": null, + "glyph_origin": "SAM-BIM ext v1", + "badge_origin": "SAM", + "rule": "rule", + "note": null, + "icon_id": "tasks_value", + "resource": "SAM_GH_TasksValue", + "icon_file": "svg/tasks_value.svg", + "status": "integrated" + }, + { + "class": "GooMultitaskerInputParam", + "name": "MultitaskerInput", + "nickname": "MultitaskerInput", + "category": "Params", + "subcategory": "SAM", + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerInput.cs", + "guid": "fdac916f-3e4f-4df6-8029-ea950823b4e8", + "kind": "param", + "exposure": null, + "obsolete": false, + "current_icon": "SAM_Small", + "icon_holder": "Resources", + "object": "value", + "op": null, + "plural": false, + "container": null, + "glyph_origin": "SAM", + "badge_origin": null, + "rule": "param:MultitaskerInput", + "note": null, + "icon_id": "value", + "resource": "SAM_GH_Value", + "icon_file": "svg/value.svg", + "status": "integrated" + }, + { + "class": "SAMMultitaskerMultitaskerInput", + "name": "SAMMultitasker.MultitaskerInput", + "nickname": "SAMMultitasker.MultitaskerInput", + "category": "SAM WIP", + "subcategory": "Multitasker", + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerInput.cs", + "guid": "14c83218-ad03-48b4-8e1f-e24bc371be6b", + "kind": "component", + "exposure": "GH_Exposure.secondary", + "obsolete": false, + "current_icon": "SAM_Small", + "icon_holder": "Resources", + "object": "value", + "op": "create", + "plural": false, + "container": null, + "glyph_origin": "SAM", + "badge_origin": "SAM", + "rule": "override", + "note": "multitasker input", + "icon_id": "value_create", + "resource": "SAM_GH_ValueCreate", + "icon_file": "svg/value_create.svg", + "status": "integrated" + } +] \ No newline at end of file diff --git a/design/grasshopper-icons/manifest_raw.json b/design/grasshopper-icons/manifest_raw.json new file mode 100644 index 0000000..bacdc0b --- /dev/null +++ b/design/grasshopper-icons/manifest_raw.json @@ -0,0 +1,155 @@ +[ + { + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "class": "GooMultitaskerInputParam", + "base_type": "GH_PersistentParam", + "guid": "fdac916f-3e4f-4df6-8029-ea950823b4e8", + "name": "MultitaskerInput", + "nickname": "MultitaskerInput", + "category": "Params", + "subcategory": "SAM", + "icon_expr": "SAM_Small", + "icon_holder": "Resources", + "icon_via_bytes": true, + "exposure": null, + "obsolete": false, + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerInput.cs" + }, + { + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "class": "GooMultitaskerOutputParam", + "base_type": "GH_PersistentParam", + "guid": "cfc14cb0-df0a-4774-858a-7a652551b3bb", + "name": "MultitaskerOutput", + "nickname": "MultitaskerOutput", + "category": "Params", + "subcategory": "SAM", + "icon_expr": "SAM_Small", + "icon_holder": "Resources", + "icon_via_bytes": true, + "exposure": null, + "obsolete": false, + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerOutput.cs" + }, + { + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "class": "GooMultitaskerVariableParam", + "base_type": "GH_PersistentParam", + "guid": "e2a46e73-b05f-427a-9c51-f6340f9fd98f", + "name": "MultitaskerVariable", + "nickname": "MultitaskerVariable", + "category": "Params", + "subcategory": "SAM", + "icon_expr": "SAM_Small", + "icon_holder": "Resources", + "icon_via_bytes": true, + "exposure": null, + "obsolete": false, + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerVariable.cs" + }, + { + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "class": "GooScriptParam", + "base_type": "GH_PersistentParam", + "guid": "400a16f6-a61d-4848-a9d6-6f0ade098ba3", + "name": "Script", + "nickname": "Script", + "category": "Params", + "subcategory": "SAM", + "icon_expr": "SAM_Small", + "icon_holder": "Resources", + "icon_via_bytes": true, + "exposure": null, + "obsolete": false, + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooScript.cs" + }, + { + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "class": "SAMMultitaskerDefaultScripts", + "base_type": "GH_SAMVariableOutputParameterComponent", + "guid": "97d3d237-ec7b-421e-913c-0a692a244980", + "name": "SAMMultitasker.DefaultScripts", + "nickname": "SAMMultitasker.DefaultScripts", + "category": "SAM WIP", + "subcategory": "Multitasker", + "icon_expr": "SAM_Small", + "icon_holder": "Resources", + "icon_via_bytes": true, + "exposure": "GH_Exposure.secondary", + "obsolete": false, + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerDefaultScripts.cs" + }, + { + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "class": "SAMMultitaskerMultitaskerInput", + "base_type": "GH_SAMVariableOutputParameterComponent", + "guid": "14c83218-ad03-48b4-8e1f-e24bc371be6b", + "name": "SAMMultitasker.MultitaskerInput", + "nickname": "SAMMultitasker.MultitaskerInput", + "category": "SAM WIP", + "subcategory": "Multitasker", + "icon_expr": "SAM_Small", + "icon_holder": "Resources", + "icon_via_bytes": true, + "exposure": "GH_Exposure.secondary", + "obsolete": false, + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerInput.cs" + }, + { + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "class": "SAMMultitaskerMultitaskerMode", + "base_type": "GH_SAMEnumComponent", + "guid": "1fd108c6-a6bf-478e-9a96-4604040801c6", + "name": "SAMMultitasker.MultitaskerMode", + "nickname": "SAMMultitasker.MultitaskerMode", + "category": "SAM WIP", + "subcategory": "Multitasker", + "icon_expr": "SAM_Small", + "icon_holder": "Resources", + "icon_via_bytes": true, + "exposure": null, + "obsolete": false, + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerMode.cs" + }, + { + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "class": "SAMMultitaskerMultitaskerVariable", + "base_type": "GH_SAMVariableOutputParameterComponent", + "guid": "5f36773d-521b-489c-86e5-25719c8346f0", + "name": "SAMMultitasker.MultitaskerVariable", + "nickname": "SAMMultitasker.MultitaskerVariable", + "category": "SAM WIP", + "subcategory": "Multitasker", + "icon_expr": "SAM_Small", + "icon_holder": "Resources", + "icon_via_bytes": true, + "exposure": "GH_Exposure.secondary", + "obsolete": false, + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerVariable.cs" + }, + { + "project": "SAM.Core.Grasshopper.Multitasker", + "project_dir": "Grasshopper/SAM.Core.Grasshopper.Multitasker", + "class": "SAMMultitaskerRun", + "base_type": "GH_SAMVariableOutputParameterComponent", + "guid": "f2fa4316-2d2c-426c-ab5f-efeb65c72456", + "name": "SAMMultitasker.Run", + "nickname": "SAMMultitasker.Run", + "category": "SAM WIP", + "subcategory": "Multitasker", + "icon_expr": "SAM_Small", + "icon_holder": "Resources", + "icon_via_bytes": true, + "exposure": "GH_Exposure.secondary", + "obsolete": false, + "source": "Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerRun.cs" + } +] \ No newline at end of file diff --git a/design/grasshopper-icons/png/24/result.png b/design/grasshopper-icons/png/24/result.png new file mode 100644 index 0000000000000000000000000000000000000000..5ecc83b93ab145ae5c33962eb59e2963beff333c GIT binary patch literal 334 zcmV-U0kQsxP)LDcpf`Z~{6w0u5o2BCzqA zM;D;*PmE0gOt-VM`|Z59yFkYw4b}tzHa?9BVA&c#RnJ9aw%S1OyjVQ;i|S32ws#M< zuy?$Tr~6l2o!$1zYP}|ao)H9_1}3N$ptOT93=c`hE3wIB-NTduNXEmR{wx{~kSufD zT|cC`+BCEj(1xLv5`EXu-)ZvC08~{;M!6%> z7YCOTB)qygRgG0u3%_esodY-(kuNiRBkJTpfaHa>c3K3`EXyV&{f~cM2*B0J`M#e3 gxUtq+{KwymcPD?1N+rS{(EtDd07*qoM6N<$g1P98)Bpeg literal 0 HcmV?d00001 diff --git a/design/grasshopper-icons/png/24/script.png b/design/grasshopper-icons/png/24/script.png new file mode 100644 index 0000000000000000000000000000000000000000..0234a728986475901dc878c00163b6b4787f1654 GIT binary patch literal 480 zcmV<60U!Q}P)F|9LGQJDZ7rc(~K=ftRiAoNF?pv!tfA;Aj%?m@zh21;6d54A{_yeP=*&Z;{rI6p5MyW1XYb}oTmcs$H(sbWt9M>lnUfsL;(DJK5qa= zm3JD51om7RsZtt<)Y^6Bp`+7c>E#rIBey~;auvXAZ=?N6jE9eg8K1rv3ZMyy8iw&0 zz}YR5+t?;K)JK1+n?!G%jqDcwv79GGc%HXduLf26`RzB0(;4=5Tv9{VnOPVMFGZC) za6JG@`{jlJ&9#Obcen)jO#l)-aR%=8{`D}@(tIv}0%EppyCOm(ldP1& zahw>iUt8p|oX&b95)r?)Mb7|7z(=K&Cn5w~Wf95N1N`}}ZQD+ZNMG3B^@T3fxK04? W!nH~Y@6j9p00006%i6<6!)>vPd)R8|s`latcr za$N`1vN!Pgd_B6ZAI@g8)^aW!4hOxxy>xYT;qiFTG_4Lu7jUJN^K?4>+UV%0HIYb= zNF;E%TsIX^6ovl&esZ~7N!$cbS66qJs;UhD`Fy_GFbqp97DLlC0EUN$iwVum&A8p} z;yRT|;qiFTb-lEJ-EKcBgy<>SSC>oJ-Q8U>K}rcgV`C%HXteA@D!>zkq>9>32!g?2 zMdSdA5TXhouzfQPUPsvkDJ7XqreZ@T0MiDQ$6sm(VCcXIk#DEhZO{~erNtaadp=;% z$no5fojkZ>%lZlcT#fwB>rbB~F_&cbi|uT>r(t~sZ1pufd7uNwts9vbpW?Tx*H%R| zGc!X~RaF+yOTNL&#~;Ia+eXg57v{UOKQK7*43-*m@!;uGm-uMt!c7G_J3G@>tMwp| zyFp-SF~`rBW1Kk^=BrO8Y1-uE*Ps4aL6AviOA^Q9an6sOm*!h)C2*+O<=JI6nL|Q| z(*VM5x2FNC5Tc?|6lO+MRYIW?6Ad~4$RUjSVCB|>oa zM*=?oKPD8er4q~~<6BMZ(d*B*@8PM3x;Z}nF8-#?H+~gs3AfET;dM5(Z@)WOBp*EY zHuv7XEO4jGgWKku5Nj2XQnp>33Xfix3R7#bGBkE_1^&*SH}V0WzirJo5JG$*r97r( zl6(6;JyzVWF_69EO4*_t`iaQwl~`o~S0{%Cemsefo SMvKz`0000^GOB+ h2czkI6pRAe000h(Y4DKbH-G>D002ovPDHLkV1iQqhS2~3 literal 0 HcmV?d00001 diff --git a/design/grasshopper-icons/png/24/settings_create.png b/design/grasshopper-icons/png/24/settings_create.png new file mode 100644 index 0000000000000000000000000000000000000000..1cab885ad58cf0782a0a07f2e7fa35c968d9759a GIT binary patch literal 657 zcmV;C0&e|@P)-kcUX6Qq;Rnb~yUF1~Z`Irp4%zwZM7yGdI) zEQIiRJf4H9s{S-h({d8ya+urAIUfLENRl)Hz~G$sIVn(bSouX+VOJVk_sI1ZcB3?) zBC36_^0<|0(=^}xE0zHO=R61?n6*ubRqsDqTz>pgWybgXVH6);eVqaDan1F}FpS4J zjflAz+wfxfPdsCcvUaeUan8@Vi(K6emn-Fp171wOPvS+_l>N)1pswpr3PQJ%G9D5_ zPopmJuzpr0pPZ6*Xg?M`x4WyXPM5` z&S#%8V22{XyG_pE8-0@uKwHlIj_Kxnb!(UgM-vnoH)zqaY0EG^p^eD_RM3cx*<&xnZVLH}^PrM-6o6~zjOfSGR=?w_r~sk+jejIoZqe^?@_ rS+MLLEt;IwqRH6>%kCkfn)Lo}7)KDU#_@@800000NkvXXu0mjf^V>Oe literal 0 HcmV?d00001 diff --git a/design/grasshopper-icons/png/24/tasks_run.png b/design/grasshopper-icons/png/24/tasks_run.png new file mode 100644 index 0000000000000000000000000000000000000000..2aa0733e9f8667c10b7be7531a095f3f52e2e971 GIT binary patch literal 787 zcmV+u1MK{XP)e8u;^@9%s6 zd=Ie8CT*NTh&GZZ0|O)}TPzkw0dOmT5CE8_`J--x29xLaX+Z&hj4%INsTfym+kT?! zdO}szUeEK=uIv6&6#(G)k<)v$)=NJGzNl<}(q4@%{c4)#FC-%*kA=hGqtR&en&UXX zf9)^t_6$M*K=RMOW{Ruf8buEZcspDA2;gq1RC=WA`aw<8dOgoeJC3uw*^ujm5SK~r z4+4<<+p?_30GusX5R1k3s}VW9=K6<&0<@?y55&eT%W?pObzMKPy1F{MDL|5>j>OTr z_=W3%`SGl8KK^F5c*8W!49PMPmoFnyr*v6ARVIvV0Z5)%BJ^ zmgO$PFh1C}{h-)z;QFm&@gs|4T(xsYs<#$*!)hi=O9EQItw!W8=K0X}2Uv8v7FP z&88>{dU|>e8it{^wY9-@T>wBN5^3)5?;j<32f%G<{iFR{w^zG=?Cjn^sdzoH{Ly*8 z)>$f*-VF~Azv%4jL|y&2hsP7Z=&x-Ay3`3x&ecuE@0{NjpzM z@_9>33xyEO=kv5}o0F51OeT{QLa?>9l_a0<1aNL*VuC^l4h#&iqoacxEfYd;YHF%N z^2|=FxR=Rf7>~z4CyvMC%w#eodHcKcn`D#gxBy RP4NH#002ovPDHLkV1m5RciaE~ literal 0 HcmV?d00001 diff --git a/design/grasshopper-icons/png/24/tasks_value.png b/design/grasshopper-icons/png/24/tasks_value.png new file mode 100644 index 0000000000000000000000000000000000000000..3161c910e9a36206c93f31168d1b250dcdc93d1a GIT binary patch literal 795 zcmV+$1LXXPP)OlTlP1S1$hZ_`352#i{Up=gU}VMdrCQZ0h$ z2ZTU^7Gew{LD(Yk;cO~}{?ScS3G?Q@-i&3>cUq*7!SwvWU1xI_=bZbU^PO|Q3v95l zrBl^)hQD6b0N5+;ARG>ti9w-tKIhhWB}gIPdmuc*Iky1>WLb{Q&CMDA1YnGvsM=9ddAP&# zemuVRzVe-6o>x`%ArWl_a6uHs`<(M008XJT3Wc{wMJs8!DC{p11mO&TGmDf#5QK}0 zqI|O~>t>-X%H>SQSqZ?;yz98Gdz*+35K%LLkFM)pwk+#b!67S(QX3F`BklsiUI2n0 zrZev*IOiz<{^H`|J^6fo;=fcBl!`xgNZ5CmbNwzl>yfQyW=;XeUZZ9bn5 z-QC?4oO7|cxfzj21OU+9-X7C+JxN4Y0Ccj&hjyx@RJyVAV5z56ym~VG-OkLP&1SP7 zdwYAIPESvxv9S?~q5uFm=SU`#sHv%;wzjs!nx%q>9@W>^H$C<)zwXO zbaXTXgF%dqjV<$4S63sQP9q+Vr~KP7;2?BeCjdQc zJ@gRK^}fEob3;QzC@=s0hSKRYbX`YFOUot3Sm(OGL?YTwM0X9t_+%KyCnCB_MBA7A ZzW^QOU0)SWvtIxJ002ovPDHLkV1g28aJB#d literal 0 HcmV?d00001 diff --git a/design/grasshopper-icons/png/24/value.png b/design/grasshopper-icons/png/24/value.png new file mode 100644 index 0000000000000000000000000000000000000000..5d03a7adf3f8905939348f8780cbb231c97825f1 GIT binary patch literal 415 zcmV;Q0bu@#P)7Hk^Cq=cu6%5(Y#*QqlwF*D*d z{O7kn4AbK;@-Z+lun}c}u&7`Kp9ougK(#72A;XCQOels63&e5qvsQ;Tt8=rmF;gWD z;6C8v1m7z^)T)e_; z3>P-OX9)3&WbpF{VYsmQ1DVML$p?&#jQ^Nf7=ObA|NZ~RASETm@b~XuhJSzmQYV@6 z@$q?Z3$U+^?$+dbb?+O)@g=VqSlCz?%p!ysl#B%kr6Pv+@89z>Ffe?<>p@{*fjCY< zwyCkbT09)wEJV4GkOe5I=FT0004>YbJ{~Bi}nBj002ov JPDHLkV1kk>z}El( literal 0 HcmV?d00001 diff --git a/design/grasshopper-icons/png/24/value_create.png b/design/grasshopper-icons/png/24/value_create.png new file mode 100644 index 0000000000000000000000000000000000000000..ac6fa61acbaa90f40f56b921326318ca378cba4c GIT binary patch literal 692 zcmV;l0!#ggP)7-*kR0ElTlpt@Zb zlzg$h-d|=*MV?M@8^~2>o7G|QH}!gL7LnhU86*`bic%}uECVfl|)6h*0)74c!y#v4)YUd({8=PJo2*m77&Cc+CDsm2GNEZ2 z48wqF7~5l0MNw*PPHCj|#$I`Tav3l0%|j3cRJY`zw91*vMF@w(xd6UwDNt3NZB~~U zICZnao?{czsxZ|98(b6$MefEI6Me%M-^CIu#^2jykERAZyR2;u-CldDT_~|vVxQ$C zM74AY0KA(0gJfZT#@O>MJ>V(I8%cbn6PwzP` z`F#L-GL}w6hlgIwkh^1)+#RDd@o9;IQ_&=T{CdtLq9a@u?d|FvnM?w^#6z+EG)DKM zj|@PkkSY6YM6WbV!kshw;gAIsxg-FZLl%(p zVZ7=HwJ@K(dpn+bULORgG5xZ amj4FzA=`PGONzMw0000SAM_Multitasker - SAM GH icons

SAM_Multitasker - SAM GH icons

9 Grasshopper objects · 9 distinct icons (4 on new SAM-BIM glyphs, green frame). Native 24 px on GH normal / orange-warning / dark bodies, plus 3x nearest-neighbour for inspection.

script (1)

SAMMultitasker.DefaultScripts
script_library_get · script · get · library · new glyph

settings (1)

SAMMultitasker.MultitaskerVariable
settings_create · settings · create

tasks (2)

SAMMultitasker.Run
tasks_run · tasks · run · new glyph · new verb
SAMMultitasker.MultitaskerMode
tasks_value · tasks · value · new glyph

value (1)

SAMMultitasker.MultitaskerInput
value_create · value · create

Parameters: object glyph only, no badge (4)

MultitaskerOutput (param)
result · result · param
Script (param)
script · script · param · new glyph
MultitaskerVariable (param)
settings · settings · param
MultitaskerInput (param)
value · value · param
\ No newline at end of file diff --git a/design/grasshopper-icons/review/contact_sheet.png b/design/grasshopper-icons/review/contact_sheet.png new file mode 100644 index 0000000000000000000000000000000000000000..6e09c766da3e34d29820a6c6065b4f64f6bad2ce GIT binary patch literal 63159 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

4m_`WXos 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 ROrAR6H&nEg^RHP1{STIcF=GG# literal 0 HcmV?d00001 diff --git a/design/grasshopper-icons/svg/result.svg b/design/grasshopper-icons/svg/result.svg new file mode 100644 index 0000000..d978a6d --- /dev/null +++ b/design/grasshopper-icons/svg/result.svg @@ -0,0 +1 @@ + diff --git a/design/grasshopper-icons/svg/script.svg b/design/grasshopper-icons/svg/script.svg new file mode 100644 index 0000000..c7774d8 --- /dev/null +++ b/design/grasshopper-icons/svg/script.svg @@ -0,0 +1 @@ + diff --git a/design/grasshopper-icons/svg/script_library_get.svg b/design/grasshopper-icons/svg/script_library_get.svg new file mode 100644 index 0000000..93002d2 --- /dev/null +++ b/design/grasshopper-icons/svg/script_library_get.svg @@ -0,0 +1 @@ + diff --git a/design/grasshopper-icons/svg/settings.svg b/design/grasshopper-icons/svg/settings.svg new file mode 100644 index 0000000..de648af --- /dev/null +++ b/design/grasshopper-icons/svg/settings.svg @@ -0,0 +1 @@ + diff --git a/design/grasshopper-icons/svg/settings_create.svg b/design/grasshopper-icons/svg/settings_create.svg new file mode 100644 index 0000000..c23cf4a --- /dev/null +++ b/design/grasshopper-icons/svg/settings_create.svg @@ -0,0 +1 @@ + diff --git a/design/grasshopper-icons/svg/tasks_run.svg b/design/grasshopper-icons/svg/tasks_run.svg new file mode 100644 index 0000000..712adb6 --- /dev/null +++ b/design/grasshopper-icons/svg/tasks_run.svg @@ -0,0 +1 @@ + diff --git a/design/grasshopper-icons/svg/tasks_value.svg b/design/grasshopper-icons/svg/tasks_value.svg new file mode 100644 index 0000000..abc7b9f --- /dev/null +++ b/design/grasshopper-icons/svg/tasks_value.svg @@ -0,0 +1 @@ + diff --git a/design/grasshopper-icons/svg/value.svg b/design/grasshopper-icons/svg/value.svg new file mode 100644 index 0000000..7c7e06d --- /dev/null +++ b/design/grasshopper-icons/svg/value.svg @@ -0,0 +1 @@ + diff --git a/design/grasshopper-icons/svg/value_create.svg b/design/grasshopper-icons/svg/value_create.svg new file mode 100644 index 0000000..915cca4 --- /dev/null +++ b/design/grasshopper-icons/svg/value_create.svg @@ -0,0 +1 @@ + diff --git a/design/grasshopper-icons/tests/GhIconTest/GhIconTest.csproj b/design/grasshopper-icons/tests/GhIconTest/GhIconTest.csproj new file mode 100644 index 0000000..4dbac6f --- /dev/null +++ b/design/grasshopper-icons/tests/GhIconTest/GhIconTest.csproj @@ -0,0 +1,21 @@ + + + net8.0-windows + true + true + disable + True + + + + + + + + + + + + + + diff --git a/design/grasshopper-icons/tests/GhIconTest/IconTests.cs b/design/grasshopper-icons/tests/GhIconTest/IconTests.cs new file mode 100644 index 0000000..4a54218 --- /dev/null +++ b/design/grasshopper-icons/tests/GhIconTest/IconTests.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text.Json; +using NUnit.Framework; + +[SetUpFixture] +public sealed class Setup : Rhino.Testing.Fixtures.RhinoSetupFixture { } + +[TestFixture] +public class IconTests +{ + // SAM_ICON_MANIFESTS = ';'-separated paths to /design/grasshopper-icons/manifest.json + [Test] + public void EveryManifestObject_LoadsInGrasshopper_WithItsRedesignedIcon() + { + var server = Grasshopper.Instances.ComponentServer; + Assert.That(server, Is.Not.Null); + var failures = new List(); + var report = new List(); + foreach (string manifest in (Environment.GetEnvironmentVariable("SAM_ICON_MANIFESTS") ?? "").Split(';', StringSplitOptions.RemoveEmptyEntries)) + { + string repo = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(manifest), "..", "..")); + var rows = JsonDocument.Parse(File.ReadAllText(manifest)).RootElement.EnumerateArray().ToList(); + int ok = 0; int globalMax = 0; int offline = 0; + foreach (var r in rows) + { + string guid = r.GetProperty("guid").GetString(), cls = r.GetProperty("class").GetString(); + string png = Path.Combine(repo, r.GetProperty("project_dir").GetString(), "Resources", "Icons", r.GetProperty("resource").GetString() + ".png"); + var obj = server.EmitObject(new Guid(guid)); + Bitmap icon; + if (obj == null) + { + // Offline fallback (e.g. Rhino.Inside.Revit plugins that plain Rhino cannot load): read the resource the + // Icon getter references straight from the built assembly. GUID -> resource is proven by the source re-parse. + string asmDir = Environment.GetEnvironmentVariable("SAM_ICON_ASSEMBLY_DIR"); + if (string.IsNullOrEmpty(asmDir)) { failures.Add($"{Path.GetFileName(repo)} {cls} {guid}: not loaded by Grasshopper"); continue; } + icon = OfflineIcon(asmDir, r.GetProperty("project").GetString(), r.GetProperty("resource").GetString()); + offline++; + if (icon == null) { failures.Add($"{cls}: resource {r.GetProperty("resource").GetString()} not found offline"); continue; } + } + else + { + string name = r.GetProperty("name").GetString() ?? ""; + if (!name.StartsWith("{") && obj.Name != name) failures.Add($"{cls}: name '{obj.Name}' != source '{name}'"); + string cat = r.GetProperty("category").ValueKind == JsonValueKind.String ? r.GetProperty("category").GetString() : null; + if (cat != null && !cat.StartsWith("{") && obj.Category != cat) failures.Add($"{cls}: category '{obj.Category}' != source '{cat}'"); + icon = obj.Icon_24x24 as Bitmap; + } + if (icon == null) { failures.Add($"{cls}: Icon_24x24 null"); continue; } + if (icon.Width != 24 || icon.Height != 24) { failures.Add($"{cls}: icon {icon.Width}x{icon.Height}"); continue; } + using var expected = new Bitmap(png); + int maxd = 0; + for (int y = 0; y < 24; y++) + for (int x = 0; x < 24; x++) + { + Color a = icon.GetPixel(x, y), b = expected.GetPixel(x, y); + int d = Math.Abs(a.A - b.A); + if (a.A > 0 && b.A > 0) d = Math.Max(d, Math.Max(Math.Abs(a.R - b.R), Math.Max(Math.Abs(a.G - b.G), Math.Abs(a.B - b.B)))); + maxd = Math.Max(maxd, d); + } + string dump = Environment.GetEnvironmentVariable("SAM_ICON_DUMP"); + if (!string.IsNullOrEmpty(dump)) { Directory.CreateDirectory(dump); icon.Save(Path.Combine(dump, cls + ".png")); } + globalMax = Math.Max(globalMax, maxd); + if (maxd > 2) { failures.Add($"{cls}: icon differs from {Path.GetFileName(png)} (max channel diff {maxd})"); continue; } + ok++; + } + report.Add($"{Path.GetFileName(repo)}: {ok}/{rows.Count} objects verified ({rows.Count - offline} loaded in Grasshopper by GUID, {offline} offline from the built assembly) with their redesigned 24x24 icon (max channel diff vs manifest PNG: {globalMax}, premultiplied-alpha rounding; tolerance 2)"); + } + foreach (var l in report) TestContext.Progress.WriteLine(l); + foreach (var f in failures) TestContext.Progress.WriteLine("FAIL " + f); + File.WriteAllLines(Path.Combine(TestContext.CurrentContext.WorkDirectory, "icon_report.txt"), report.Concat(failures.Select(f => "FAIL " + f))); + Assert.That(report, Is.Not.Empty); + Assert.That(failures, Is.Empty); + } + + static readonly Dictionary Loaded = new(); + + static Bitmap OfflineIcon(string dir, string project, string resource) + { + string path = Directory.GetFiles(dir, project + ".dll", SearchOption.AllDirectories).OrderByDescending(File.GetLastWriteTimeUtc).FirstOrDefault(); + if (path == null) return null; + if (!Loaded.TryGetValue(path, out var asm)) Loaded[path] = asm = System.Reflection.Assembly.LoadFrom(path); + foreach (string res in asm.GetManifestResourceNames().Where(n => n.EndsWith(".resources"))) + { + var rm = new System.Resources.ResourceManager(res.Substring(0, res.Length - ".resources".Length), asm); + object o; + try { o = rm.GetObject(resource); } catch { continue; } + if (o is Bitmap b) return b; + if (o is byte[] bytes) return new Bitmap(new MemoryStream(bytes)); + } + foreach (string res in asm.GetManifestResourceNames().Where(n => n.EndsWith("." + resource + ".png"))) + return new Bitmap(asm.GetManifestResourceStream(res)); + return null; + } +} diff --git a/design/grasshopper-icons/tests/GhIconTest/README.md b/design/grasshopper-icons/tests/GhIconTest/README.md new file mode 100644 index 0000000..ef9dd7c --- /dev/null +++ b/design/grasshopper-icons/tests/GhIconTest/README.md @@ -0,0 +1,22 @@ +# GhIconTest: real Rhino 8 / Grasshopper icon load test + +Hosts the installed Rhino 8 + Grasshopper in-process (McNeel `Rhino.Testing`, the same setup as SAM's +`SAM.Core.Grasshopper.Tests`). Grasshopper loads the installed SAM plugins (`%APPDATA%\SAM` via `SAM.ghlink`). +For every manifest row the test: + +* emits the object by its ComponentGuid through `Grasshopper.Instances.ComponentServer` (it must load), +* checks name and category against the source, +* checks `Icon_24x24` is 24×24 and matches the manifest PNG (tolerance: 2 levels per channel, premultiplied-alpha rounding). + +It is not part of any solution. Build the repository first (post-build installs to `%APPDATA%\SAM`), then: + +``` +set SAM_ICON_MANIFESTS=\design\grasshopper-icons\manifest.json +dotnet test design\grasshopper-icons\tests\GhIconTest +``` + +**Offline mode.** Some plugins can't load in plain Rhino; for example the SAM_Revit plugins need Rhino.Inside.Revit. +For those, set `SAM_ICON_ASSEMBLY_DIR=\build`. Any object that Grasshopper can't emit then has its resource read +directly from the built assembly (`ResourceManager`, which resolves no plugin types) and compared with the manifest PNG. +The report counts loaded and offline objects separately. The link from GUID to resource is proven by `tools/integrate.py` +(source re-parse) and `tools/check_source.py`. diff --git a/design/grasshopper-icons/tests/GhIconTest/Rhino.Testing.Configs.xml b/design/grasshopper-icons/tests/GhIconTest/Rhino.Testing.Configs.xml new file mode 100644 index 0000000..e289255 --- /dev/null +++ b/design/grasshopper-icons/tests/GhIconTest/Rhino.Testing.Configs.xml @@ -0,0 +1,5 @@ + + + C:\Program Files\Rhino 8\System + true + diff --git a/design/grasshopper-icons/tools/build.py b/design/grasshopper-icons/tools/build.py new file mode 100644 index 0000000..ccc07b1 --- /dev/null +++ b/design/grasshopper-icons/tools/build.py @@ -0,0 +1,145 @@ +"""Build this repository's SAM GH icons from manifest.json. + + python build.py -> svg/ (canonical), png/24/ (production), review/contact_sheet.png (+.html), + review/REVIEW.md (collisions + shared icons) +Deterministic: output depends only on manifest.json + icons.py + icons_ext.py. +Fails (exit 1) if two different icon ids rasterise to identical pixels. +""" +import collections +import hashlib +import html +import json +import os +import re +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import icons # noqa: E402 +import icons_ext # noqa: E402,F401 (registers the SAM-BIM extension glyphs) +import render # noqa: E402 + +DESIGN = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +REPO_NAME = os.path.basename(os.path.dirname(os.path.dirname(DESIGN))) +SVG = os.path.join(DESIGN, "svg") +PNG = os.path.join(DESIGN, "png", "24") +REVIEW = os.path.join(DESIGN, "review") + +CSS = """ +*{box-sizing:border-box}body{margin:0;background:#fff;color:#111;font:12px/1.3 'Segoe UI',system-ui,sans-serif;padding:20px 24px;width:%dpx} +h1{font-size:18px;margin:0 0 2px;text-transform:uppercase;letter-spacing:.02em}.bar{height:5px;width:120px;background:linear-gradient(90deg,#D32F2F 33%%,#F6C90E 33%% 66%%,#1565C0 66%%);margin:6px 0 10px} +h2{font-size:13px;margin:16px 0 6px;border-bottom:2px solid #111;padding-bottom:3px;text-transform:uppercase;letter-spacing:.03em} +.sub{color:#424242;margin:0 0 8px}.grid{display:grid;grid-template-columns:repeat(%d,1fr);gap:6px} +.c{border:1px solid #BDBDBD;padding:6px;display:flex;gap:8px;align-items:center;min-height:84px}.c.new{border-color:#277A45;border-width:2px} +.n{display:flex;flex-direction:column;gap:3px;align-items:center} +.gh{width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:3px} +.l{background:linear-gradient(#e6e6e6,#c9c9c9);border:1px solid #8a8a8a}.o{background:linear-gradient(#ffc680,#f0962a);border:1px solid #9a5a10} +.d{background:linear-gradient(#4a4a4a,#2e2e2e);border:1px solid #111} +img.px{image-rendering:pixelated}.t{font-size:10.5px;word-break:break-word;line-height:1.25}.t b{font-size:11px}.t i{color:#424242;font-style:normal} +""" + + +def display_name(r): + n = r["name"] or "" + m = re.match(r"^\{typeof\((?:[\w:.]+\.)?(\w+)\)\.Name\}$", n) + return (m.group(1) if m else n) + (" (param)" if r["kind"] == "param" else "") + + +def label(rows): + r = rows[0] + names = "
".join(html.escape(display_name(x)) + (" (hidden/obsolete)" if x["obsolete"] or "hidden" in (x["exposure"] or "") else "") for x in rows) + ext = " · new glyph" if r["glyph_origin"] != "SAM" else "" + ext += " · new verb" if r["badge_origin"] not in (None, "SAM") else "" + return (f"{names}
{r['icon_id']} · {r['object']} · {r['op'] or 'param'}{' · plural' if r['plural'] else ''}" + f"{' · library' if r['container'] else ''}{ext}") + + +def sheet(title, subtitle, groups, out_png, cols=4, big=72, width=1500): + """groups: [(heading, [(png24, label_html, is_new)])]; native 24 px on GH normal / warning / dark bodies + 3x.""" + base = os.path.dirname(os.path.abspath(out_png)) + parts = [f"

{html.escape(title)}

{subtitle}

"] + rows = 0 + for head, items in groups: + parts.append(f"

{html.escape(head)} ({len(items)})

") + for png, lab, new in items: + rel = os.path.relpath(png, base).replace("\\", "/") + tiles = "".join(f"" for k in "lod") + parts.append(f"
{tiles}
" + f"
{lab}
") + parts.append("
") + rows += (len(items) + cols - 1) // cols + doc = (f"{html.escape(title)}" + f"{''.join(parts)}") + html_path = os.path.splitext(out_png)[0] + ".html" + open(html_path, "w", encoding="utf-8", newline="\n").write(doc) + render._shot(os.path.abspath(html_path), os.path.abspath(out_png), width, 90 + len(groups) * 44 + rows * 118, scale=1) + render._trim(out_png) + + +def main(): + rows = json.load(open(os.path.join(DESIGN, "manifest.json"), encoding="utf-8")) + ids = {} + for r in rows: + ids.setdefault(r["icon_id"], r) + os.makedirs(SVG, exist_ok=True) + for fn in os.listdir(SVG): # svg/ mirrors the manifest exactly + if fn.endswith(".svg") and fn[:-4] not in ids: + os.remove(os.path.join(SVG, fn)) + svgs, pngs = [], [] + for iid, r in sorted(ids.items()): + svg = icons.icon_svg(r["object"], r["op"], plural=r["plural"], container=r["container"], comment=f"SAM GH icon {iid}") + p = os.path.join(SVG, iid + ".svg") + open(p, "w", encoding="utf-8", newline="\n").write(svg) + svgs.append(p) + pngs.append(os.path.join(PNG, iid + ".png")) + if os.path.isdir(PNG): + for fn in os.listdir(PNG): + if fn[:-4] not in ids: + os.remove(os.path.join(PNG, fn)) + render.rasterise(svgs, pngs) + + # ---- collision check: identical 24 px pixels for different icon ids + by_hash = collections.defaultdict(list) + for iid in ids: + by_hash[hashlib.sha1(open(os.path.join(PNG, iid + ".png"), "rb").read()).hexdigest()].append(iid) + dup = sorted(v for v in by_hash.values() if len(v) > 1) + + # ---- contact sheet: one tile per icon (objects sharing it listed together), grouped by object family + by_icon = collections.OrderedDict() + for r in sorted(rows, key=lambda r: (r["kind"] == "param", r["object"], r["op"] or "", r["icon_id"], r["name"] or "")): + by_icon.setdefault(r["icon_id"], []).append(r) + groups = collections.OrderedDict() + for iid, rs in by_icon.items(): + head = "Parameters: object glyph only, no badge" if rs[0]["kind"] == "param" else rs[0]["object"] + groups.setdefault(head, []).append((os.path.join(PNG, iid + ".png"), label(rs), + rs[0]["glyph_origin"] != "SAM" or rs[0]["badge_origin"] not in (None, "SAM"))) + os.makedirs(REVIEW, exist_ok=True) + n_new = sum(1 for r in ids.values() if r["glyph_origin"] != "SAM") + sheet(f"{REPO_NAME} - SAM GH icons", f"{len(rows)} Grasshopper objects · {len(ids)} distinct icons ({n_new} on new SAM-BIM glyphs, green frame). " + "Native 24 px on GH normal / orange-warning / dark bodies, plus 3x nearest-neighbour for inspection.", + list(groups.items()), os.path.join(REVIEW, "contact_sheet.png")) + + # ---- review report + shared = [(iid, rs) for iid, rs in by_icon.items() if len(rs) > 1] + lines = [f"# {REPO_NAME} - icon review", "", + f"- Objects: **{len(rows)}** ({sum(r['kind'] == 'component' for r in rows)} components, {sum(r['kind'] == 'param' for r in rows)} params)", + f"- Distinct icons: **{len(ids)}** ({len(ids) - n_new} on SAM glyphs, {n_new} on SAM-BIM extension glyphs)", + f"- Identical-pixel groups (different icon ids): **{len(dup)}** {dup if dup else ''}", + f"- Icons shared by several objects (intentional: qualifier variants / same noun+verb): **{len(shared)}**", ""] + for iid, rs in shared: + lines.append(f" - `{iid}`: " + ", ".join(display_name(r) for r in rs)) + open(os.path.join(REVIEW, "REVIEW.md"), "w", encoding="utf-8", newline="\n").write("\n".join(lines) + "\n") + + for r in rows: + r["status"] = "redesigned" if r["status"] == "classified" else r["status"] + json.dump(rows, open(os.path.join(DESIGN, "manifest.json"), "w", encoding="utf-8"), indent=1) + import csv + with open(os.path.join(DESIGN, "manifest.csv"), "w", newline="", encoding="utf-8") as fh: + w = csv.DictWriter(fh, fieldnames=list(rows[0].keys())) + w.writeheader() + w.writerows(rows) + print(f"{len(rows)} objects, {len(ids)} icons ({n_new} new-glyph), identical-pixel groups: {dup}") + return 1 if dup else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/design/grasshopper-icons/tools/check_assemblies.py b/design/grasshopper-icons/tools/check_assemblies.py new file mode 100644 index 0000000..117b0cb --- /dev/null +++ b/design/grasshopper-icons/tools/check_assemblies.py @@ -0,0 +1,74 @@ +"""Verify the built Grasshopper assemblies embed every SAM_GH_* icon their manifest rows need. + +Format-agnostic (byte[] resx, Bitmap resx via System.Resources.Extensions, or manifest resources): + * every required resource name must occur in the assembly (UTF-16LE resx name table or UTF-8 manifest name), and + * the assembly must embed at least as many 24x24 PNGs as required icons (legacy odd-size PNGs are reported). +Usage: python check_assemblies.py [ ...] (searched recursively; newest .gha/.dll per assembly wins) +""" +import io +import json +import os +import re +import sys + +from PIL import Image + +DESIGN = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +REPO = os.path.dirname(os.path.dirname(DESIGN)) +SIG, IEND = b"\x89PNG\r\n\x1a\n", b"IEND\xaeB`\x82" + + +def pngs(blob): + i = 0 + while True: + i = blob.find(SIG, i) + if i < 0: + return + j = blob.find(IEND, i) + yield Image.open(io.BytesIO(blob[i: j + len(IEND)])) + i = j + + +def assembly_name(project_dir): + pdir = os.path.join(REPO, project_dir) + csproj = [f for f in os.listdir(pdir) if f.endswith(".csproj")][0] + m = re.search(r"([^<]+)", open(os.path.join(pdir, csproj), encoding="utf-8-sig").read()) + return m.group(1).strip() if m else csproj[:-7] + + +def find(dirs, name): + hits = [] + for d in dirs: + for root, subdirs, files in os.walk(d): + subdirs[:] = [x for x in subdirs if x not in ("obj", ".git", "packages")] # skip reference assemblies (obj/*/ref, refint) + hits += [os.path.join(root, f) for f in files if f in (name + ".gha", name + ".dll")] + return max(hits, key=os.path.getmtime) if hits else None + + +def main(dirs): + rows = json.load(open(os.path.join(DESIGN, "manifest.json"), encoding="utf-8")) + need = {} + for r in rows: + need.setdefault(r["project_dir"], set()).add(r["resource"]) + fail = 0 + for pdir, names in sorted(need.items()): + asm = assembly_name(pdir) + path = find(dirs, asm) + if not path: + print(f"{asm}: NOT FOUND in {dirs} -> FAIL") + fail += 1 + continue + blob = open(path, "rb").read() + missing = [n for n in names if n.encode("utf-16-le") not in blob and n.encode("utf-8") not in blob] + imgs = list(pngs(blob)) + bad = [im.size for im in imgs if im.size != (24, 24)] + ok = not missing and sum(im.size == (24, 24) for im in imgs) >= len(names) + fail += not ok + print(f"{asm} ({os.path.relpath(path, REPO) if path.startswith(REPO) else path}): {len(names)} required icons, " + f"names missing {len(missing)}, embedded PNGs {len(imgs)} (legacy non-24x24: {len(bad)}) -> {'OK' if ok else 'FAIL'}") + print("RESULT:", "OK" if not fail else f"FAIL ({fail} assemblies)") + return 1 if fail else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:] or [os.path.join(REPO, "build")])) diff --git a/design/grasshopper-icons/tools/check_source.py b/design/grasshopper-icons/tools/check_source.py new file mode 100644 index 0000000..a10345a --- /dev/null +++ b/design/grasshopper-icons/tools/check_source.py @@ -0,0 +1,97 @@ +"""Guard rails for the icon redesign diff. + + python check_source.py [base] (default base: origin/sow/2026-Q3) + +1. Vendored design-system files are byte-identical to SAM-BIM/SAM#166 @ cf4d924a (SHA-256). +2. Every changed line in a component .cs file is an icon-token swap inside an `Icon` getter + (`.OLD` -> `.SAM_GH_*`); generated Resources.Designer.cs / holder blocks are excluded. +3. The set of ComponentGuid values, names, nicknames, categories and exposures is unchanged vs base. +""" +import hashlib +import os +import re +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +REPO = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) + +VENDORED = { # file -> SHA-256 (LF line endings) of SAM/design/grasshopper-icons/tools/ at SAM#166 head cf4d924a + "icons.py": "5ca1bfee3c0f19e55babff06120eee62c4a7468b141b608e8c386deaa998f0e7", + "render.py": "8a87f66c45e0bb3e04cc613f2b0f28a8cb8df951a7daad95bdec8e3851060a3f", + "sam_classify.py": "317064816c964e2268e9406aee9ca5f7d82c2bdb34da9c3225a344ca2464b864", # SAM classify.py +} +TOKEN = re.compile(r"\b(?:\w+\.)+\w+") # any dotted chain, e.g. Properties.Resources.SAM_Small3 +KEEP_RE = re.compile(r'ComponentGuid|base\(\s*"|GH_Exposure|Category|NickName|new Guid\(|new\("') + + +def git(*args): + return subprocess.run(["git", "-C", REPO, *args], check=True, capture_output=True, text=True, encoding="utf-8").stdout + + +def main(base): + ok = True + for f, h in VENDORED.items(): + got = hashlib.sha256(open(os.path.join(HERE, f), "rb").read().replace(b"\r\n", b"\n")).hexdigest() # autocrlf-safe + print(f"vendored {f}: {'OK' if got == h else 'MODIFIED'}") + ok &= got == h + files = [f for f in git("diff", "--name-only", base, "--", "*.cs").split("\n") if f and not f.startswith("design/")] + swaps, bad = 0, [] + for f in files: + if f.endswith("Resources.Designer.cs") or f.startswith("design/"): + continue + diff = git("diff", "-U0", base, "--", f) + if "// " in diff: # generated holder block: only additions inside the markers + block = re.findall(r"^[-+](?![-+]).*$", diff, re.M) + inside, outside = False, [] + for line in block: + if "// " in line: + inside = True + if not inside and line.strip() not in ("+", "-"): + outside.append(line) + if "// " in line: + inside = False + if outside: + bad.append((f, outside[:4])) + continue + minus = re.findall(r"^-(?!--)(.*)$", diff, re.M) + plus = re.findall(r"^\+(?!\+\+)(.*)$", diff, re.M) + if len(minus) != len(plus): + bad.append((f, "added/removed lines")) + continue + now = open(os.path.join(REPO, f), encoding="utf-8-sig").read() + getters = [m.end() for m in re.finditer(r"Bitmap\s+Icon\b", now)] + + def in_icon_getter(line): # the line sits inside an `Icon` getter (single- or multi-line form) + i = now.find(line.strip()) + return i >= 0 and any(0 <= i - g <= 300 for g in getters) + for a, b in zip(minus, plus): + if ("Icon" not in a and not in_icon_getter(b)) or TOKEN.sub("X", a) != TOKEN.sub("X", b) or "SAM_GH_" not in b or KEEP_RE.search(a): + bad.append((f, a.strip(), b.strip())) + else: + swaps += 1 + print(f"component .cs files changed: {len(files)}; icon-token swaps: {swaps}; non-icon changes: {len(bad)}") + for b in bad[:20]: + print(" NON-ICON CHANGE:", b) + ok &= not bad + + def guids(rev): + out = set() + listing = git("ls-tree", "-r", "--name-only", rev) if rev else git("ls-files") + for f in listing.split("\n"): + if f.endswith(".cs") and not f.startswith("design/"): + try: + t = git("show", f"{rev}:{f}") if rev else open(os.path.join(REPO, f), encoding="utf-8-sig").read() + except (subprocess.CalledProcessError, FileNotFoundError): + continue + out |= {(f, m.group(0)) for m in re.finditer(r'ComponentGuid[^"\n]*"[0-9a-fA-F-]+"', t)} + return out + g0, g1 = guids(base), guids(None) + print(f"ComponentGuid declarations: base {len(g0)}, now {len(g1)} -> {'UNCHANGED' if g0 == g1 else 'CHANGED'}") + ok &= g0 == g1 + print("RESULT:", "OK" if ok else "FAIL") + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "origin/sow/2026-Q3")) diff --git a/design/grasshopper-icons/tools/classify.py b/design/grasshopper-icons/tools/classify.py new file mode 100644 index 0000000..942adfb --- /dev/null +++ b/design/grasshopper-icons/tools/classify.py @@ -0,0 +1,176 @@ +"""Classify every inventoried GH object into (object glyph, operation badge, modifiers). + +Input : ../manifest_raw.json (from inventory.py) +Output: ../manifest.json + ../manifest.csv (the source of truth for this repository's icons) + +Rule order (first hit wins, all deterministic): + 1. repo_rules.OVERRIDES, then SAM's OVERRIDES (sam_classify.py, vendored verbatim from SAM#166) + 2. verbs : SAM VERBS, then EXT_VERBS below + 3. nouns : repo_rules.OBJECTS, then EXT_OBJECTS below (new SAM-BIM glyphs), then SAM OBJECTS + 4. params : repo_rules.PARAM_OBJECTS, EXT_PARAM_OBJECTS, then SAM PARAM_OBJECTS +Qualifiers (`...By`) are not drawn, exactly as in SAM. +""" +import csv +import json +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +DESIGN = os.path.dirname(HERE) +sys.path.insert(0, HERE) +import icons # noqa: E402 +import icons_ext # noqa: E402 +import repo_rules as R # noqa: E402 +import sam_classify as C # noqa: E402 + +# ------------------------------------------------------------------ extra verbs (after SAM's VERBS) +EXT_VERBS = [ + (r"^(Run|Simulate|Execute|Solve|Workflow)", "run"), (r"^Open", "import"), (r"^Print", "export"), + (r"^(Show|Visuali[sz]e)", "display"), (r"^(Delete|Clear)", "remove"), (r"^Query", "get"), (r"^Assign", "set"), + (r"^(Renumber|Rationalise|Clean|Repair)", "modify"), (r"^(Verify|Validate)", "validate"), (r"^Compare", "analyse"), + (r"^(Assemble|Sew)", "merge"), (r"^Tag", "display"), (r"^(Duplicated|Connected|Ordered|Related)", "get"), +] + +# ------------------------------------------------------------------ extra nouns -> SAM-BIM extension glyphs (before SAM's OBJECTS) +EXT_OBJECTS = [ + ("AdiabaticHumidification", "processAdiabatic", None), ("IsothermalHumidification", "processHumidification", None), + ("SteamHumidification", "processHumidification", None), ("HeatingProcess", "processHeating", None), + ("CoolingProcess", "processCooling", None), ("MixingProcess", "processMixing", None), + ("HeatRecoveryProcess", "processHeatRecovery", None), ("FanProcess", "processFan", None), ("RoomProcess", "processRoom", None), + ("UndefinedProcess", "mollierProcess", None), ("MollierProcess", "mollierProcess", None), ("MollierPoint", "mollierPoint", None), + ("MollierGroup", "group", None), ("MollierChart", "mollierChart", None), ("Diagram", "mollierChart", None), + ("Psychrometric", "mollierChart", None), ("Mollier", "mollierChart", None), + ("CellComplex", "cellComplex", None), ("Topology", "cellComplex", None), + ("Algorithm", "algorithm", None), ("GenOpt", "algorithm", None), ("Solver", "algorithm", None), ("Objective", "objective", None), + ("Script", "script", None), ("Python", "script", None), ("Macro", "script", None), ("Multitasker", "tasks", None), + ("Absorption", "sound", None), ("BandValues", "sound", None), ("Acoustic", "sound", None), + ("Sheet", "view", None), ("View", "view", None), ("ElementId", "element", None), ("UniqueId", "element", None), + ("Element", "element", None), + ("EnergyCentre", "energyCentre", None), ("PlantRoom", "plantRoom", None), ("Connector", "connector", None), + ("SunDirection", "sunPath", None), ("SunPath", "sunPath", None), ("Irradiance", "solar", None), ("SunExposure", "solar", None), + ("SunAnalysis", "solar", None), ("SolarSimulation", "solar", None), +] +EXT_PARAM_OBJECTS = { + "MollierPoint": "mollierPoint", "MollierProcess": "mollierProcess", "MollierGroup": "group", + "MollierObject": "mollierChart", "MollierChartObject": "mollierChart", "CellComplex": "cellComplex", "ResolvedCellComplex": "cellComplex", + "Algorithm": "algorithm", "Objective": "objective", "Script": "script", "BandValues": "sound", + "SystemEnergyCentre": "energyCentre", "SystemPlantRoom": "plantRoom", +} +NO_PLURAL = {g for g in icons_ext.EXT_GLYPHS if g.startswith(("mollier", "process"))} # charts do not stack + + +def _param_type(x): + """Type key of a param: from `{typeof(X).Name}` / a literal name, and from the GooParam class name.""" + name = x["name"] or "" + m = re.fullmatch(r"\{typeof\((?:[\w:.]+\.)?(\w+)\)\.Name\}", name) + by_name = m.group(1) if m else name.split(".")[-1] + by_class = re.sub(r"^Goo|Param$", "", x["class"]) + return by_class, by_name + + +def _lookup_param(x): + for key in _param_type(x): + for table in (R.PARAM_OBJECTS, EXT_PARAM_OBJECTS, C.PARAM_OBJECTS): + if key in table: + v = table[key] + obj, container, plural = (v, None, False) if isinstance(v, str) else (v + (False,))[:3] + if table is C.PARAM_OBJECTS and key in C.PARAM_LIBRARY: + container = "library" + return obj, container, bool(plural), key + raise KeyError(f"no param glyph for {x['class']} / {x['name']} (add it to repo_rules.PARAM_OBJECTS)") + + +def _short_candidates(name): + s = C.short(name) + out = [s] + t = re.sub(r"^\w+\.", "", s) # any plugin prefix: SAMMollier., Tas., SAMOCCT., Revit., ... + if t != s: + out.append(t) + return out + + +def classify(x): + name = x["name"] or "" + if x["category"] == "Params" or x["base_type"].startswith(("GH_PersistentParam", "GH_Param")): + obj, container, plural, key = _lookup_param(x) + return dict(kind="param", object=obj, op=None, plural=plural, container=container, rule="param:" + key) + for table, tag in ((R.OVERRIDES, "override"), (C.OVERRIDES, "sam-override")): + if name in table: + obj, op, extra = table[name] + return dict(kind="component", object=obj, op=op, plural=extra == "plural", + container="library" if extra == "library" else None, rule=tag, + note=extra if extra not in (None, "plural", "library") else None) + verbs = list(R.VERBS) + C.VERBS + EXT_VERBS + nouns = list(R.OBJECTS) + EXT_OBJECTS + C.OBJECTS + cands = _short_candidates(name) + s, op = cands[0], None + for cand in cands: + op = next((o for rx, o in verbs if re.match(rx, cand)), None) + if op: + s = cand + break + else: + s = cands[-1] + body = re.split(r"By[A-Z]", s, 1)[0] + obj, container = None, None + for text in (body, s): + for key, o, c in nouns: + if key in text: + obj, container = o, c + break + if obj: + break + if x["base_type"].startswith("GH_SAMEnumComponent") and op is None: + op = "value" + plural = bool(re.search(C.PLURAL_RE + r"(By|$|[A-Z])", body)) and container is None + return dict(kind="component", object=obj, op=op, plural=plural, container=container, rule="rule") + + +def normalise(c): + c = C.normalise(c) # ADD: plurality not drawn + if c["object"] in NO_PLURAL: + c["plural"] = False + return c + + +def origin(c): + g = "SAM-BIM ext v" + icons_ext.EXT_VERSION if c["object"] in icons_ext.EXT_GLYPHS else "SAM" + b = "SAM-BIM ext v" + icons_ext.EXT_VERSION if c["op"] in icons_ext.EXT_BADGES else "SAM" + return g, b + + +def main(): + raw = json.load(open(os.path.join(DESIGN, "manifest_raw.json"), encoding="utf-8")) + out, problems = [], [] + for x in raw: + c = normalise(classify(x)) + if not c["object"] or c["object"] not in icons.G or (c["kind"] == "component" and not c["op"]) or (c["op"] and c["op"] not in icons.BADGES): + problems.append((x["class"], x["name"], c)) + continue + iid = C.icon_id(c) + g_src, b_src = origin(c) + out.append({ + "class": x["class"], "name": x["name"], "nickname": x["nickname"], "category": x["category"], + "subcategory": x["subcategory"], "project": x["project"], "project_dir": x["project_dir"], "source": x["source"], + "guid": x["guid"], "kind": c["kind"], "exposure": x["exposure"], "obsolete": x["obsolete"], + "current_icon": x["icon_expr"], "icon_holder": x["icon_holder"], + "object": c["object"], "op": c["op"], "plural": c["plural"], "container": c["container"], + "glyph_origin": g_src, "badge_origin": b_src if c["op"] else None, + "rule": c["rule"], "note": c.get("note"), "icon_id": iid, "resource": C.resource_name(iid), + "icon_file": f"svg/{iid}.svg", "status": "classified", + }) + for cls, n, c in problems: + print("UNCLASSIFIED", cls, n, c) + out.sort(key=lambda r: (r["project"], r["object"], r["op"] or "", r["name"] or "", r["guid"])) + json.dump(out, open(os.path.join(DESIGN, "manifest.json"), "w", encoding="utf-8"), indent=1) + with open(os.path.join(DESIGN, "manifest.csv"), "w", newline="", encoding="utf-8") as fh: + w = csv.DictWriter(fh, fieldnames=list(out[0].keys()) if out else ["class"]) + w.writeheader() + w.writerows(out) + print(len(out), "classified,", len(problems), "unclassified,", len({r['icon_id'] for r in out}), "distinct icons") + return 1 if problems else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/design/grasshopper-icons/tools/icons.py b/design/grasshopper-icons/tools/icons.py new file mode 100644 index 0000000..cd7d954 --- /dev/null +++ b/design/grasshopper-icons/tools/icons.py @@ -0,0 +1,782 @@ +"""SAM Grasshopper icon grammar: object glyphs + operation badges -> 24x24 SVG. + +Deterministic: the same (object, op, modifiers) always yields byte-identical SVG. +See ../ICON_DESIGN_SYSTEM.md for the rules this file implements. +""" +import math + +# ---------------------------------------------------------------- palette +# Taken from https://sambim.xyz (CSS custom properties + logo pixels, 2026-09-28). +INK = "#111111" # --text / --line +EMBLEM = "#81BC44" # SAM_BIM_logo.png tile green (averaged) +DEEP = "#277A45" # --green +BLUE = "#1565C0" # --blue +YELLOW = "#F6C90E" # --yellow +RED = "#D32F2F" # --red +MUTED = "#424242" # --muted +WHITE = "#FFFFFF" # --bg + +# Derived tones (flat, no gradients). Green = subject, neutral = context. +G_TOP, G_LEFT, G_RIGHT = "#ADD485", EMBLEM, "#659335" +N_TOP, N_LEFT, N_RIGHT = WHITE, "#E3E3E3", "#BDBDBD" # #BDBDBD = site border grey +N_FLAT = "#EDEDED" +SW = 1.1 # the one outline weight (px at 24x24) + + +def tones(subject): + return (G_TOP, G_LEFT, G_RIGHT) if subject else (N_TOP, N_LEFT, N_RIGHT) + + +def f(v): + s = ("%.2f" % v).rstrip("0").rstrip(".") + return "0" if s in ("-0", "") else s + + +def pts(p): + return " ".join(f(x) + "," + f(y) for x, y in p) + + +def poly(p, fill, stroke=INK, sw=SW, extra=""): + return f'' + + +def path(d, fill="none", stroke=INK, sw=SW, extra=""): + return f'' + + +def circle(cx, cy, r, fill, stroke=INK, sw=SW): + s = f' stroke="{stroke}" stroke-width="{f(sw)}"' if stroke else "" + return f'' + + +def rect(x, y, w, h, fill, rx=0, stroke=INK, sw=SW, extra=""): + r = f' rx="{f(rx)}"' if rx else "" + s = f' stroke="{stroke}" stroke-width="{f(sw)}"' if stroke else "" + return f'' + + +# ---------------------------------------------------------------- isometric helpers (2:1 pixel isometric) +def iso(ox, oy): + return lambda x, y, z: (ox + 2 * x - 2 * y, oy + x + y - z) + + +def box(P, x0, y0, z0, dx, dy, dz, t, dashed=False): + x1, y1, z1 = x0 + dx, y0 + dy, z0 + dz + ex = ' stroke-dasharray="1.6 1.1"' if dashed else "" + return "".join([ + poly([P(x0, y0, z1), P(x1, y0, z1), P(x1, y1, z1), P(x0, y1, z1)], t[0], extra=ex), + poly([P(x0, y1, z1), P(x1, y1, z1), P(x1, y1, z0), P(x0, y1, z0)], t[1], extra=ex), + poly([P(x1, y0, z1), P(x1, y1, z1), P(x1, y1, z0), P(x1, y0, z0)], t[2], extra=ex), + ]) + + +def prism(P, plan, z0, z1, t): + """Extruded convex plan polygon (counter-clockwise in plan). Visible sides + top.""" + out = [] + n = len(plan) + sides = [] + for i in range(n): + a, b = plan[i], plan[(i + 1) % n] + ex, ey = b[0] - a[0], b[1] - a[1] + nx, ny = ey, -ex # outward for CCW + if nx + ny > 1e-6: + depth = (a[0] + b[0] + a[1] + b[1]) + sides.append((depth, a, b, t[1] if ny > nx else t[2])) + for _, a, b, tone in sorted(sides, key=lambda s: s[0]): + out.append(poly([P(a[0], a[1], z1), P(b[0], b[1], z1), P(b[0], b[1], z0), P(a[0], a[1], z0)], tone)) + out.append(poly([P(x, y, z1) for x, y in plan], t[0])) + return "".join(out) + + +def hexplan(r, rot=0.0): + return [(r * math.cos(math.radians(rot + 60 * i)), r * math.sin(math.radians(rot + 60 * i))) for i in range(6)] + + +# ---------------------------------------------------------------- object glyphs +# Each returns SVG elements in the 24x24 frame. The bottom-right (≈ x,y > 14) is kept +# light because the operation badge lives there. subject=True -> SAM green. +G = {} + + +def glyph(name): + def deco(fn): + G[name] = fn + return fn + return deco + + +@glyph("space") +def _space(s=True): + return box(iso(10, 10), 0, 0, 0, 4, 4, 8, tones(s)) + + +@glyph("floor") +def _floor(s=True): + # approved exploratory icon: cutaway room corner, floor = subject + return (poly([(9, 8), (9, 2), (1, 6), (1, 12)], "#EFEFEF") + poly([(9, 8), (17, 12), (17, 6), (9, 2)], N_RIGHT) + + poly([(9, 8), (17, 12), (9, 16), (1, 12)], G_LEFT if s else N_TOP)) + + +@glyph("externalSpace") +def _external_space(s=True): + P = iso(10, 10) + return box(P, 0, 0, 0, 4, 4, 8, (N_TOP, N_LEFT, N_RIGHT), dashed=True) + + +@glyph("cluster") +def _cluster(s=True): + P = iso(7, 9) + t = (N_TOP, N_LEFT, N_RIGHT) + return box(P, 0, 0, 0, 6, 3, 7, t) + path( + "M13 5L7 8V15", stroke=DEEP if s else MUTED, sw=2.4) + + +@glyph("model") +def _model(s=True): + P = iso(9, 9) + t = tones(s) + out = [box(P, 0, 0, 0, 4.5, 3.5, 5.5, t)] + # gable roof, ridge along x at y=1.75 + out.append(poly([P(0, 3.5, 5.5), P(4.5, 3.5, 5.5), P(4.5, 1.75, 8.5), P(0, 1.75, 8.5)], t[0])) + out.append(poly([P(4.5, 0, 5.5), P(4.5, 3.5, 5.5), P(4.5, 1.75, 8.5)], t[2])) + return "".join(out) + + +@glyph("case") +def _case(s=True): + return compose_plural(G["model"], s) + + +@glyph("panel") +def _panel(s=True): + return poly([(2, 4), (14, 10), (14, 18), (2, 12)], G_LEFT if s else N_LEFT) + + +@glyph("aperture") +def _aperture(s=True): + return poly([(2, 4), (14, 10), (14, 18), (2, 12)], N_LEFT) + poly( + [(5, 7), (11, 10), (11, 14), (5, 11)], G_LEFT if s else N_TOP) + + +@glyph("openingProperties") +def _opening(s=True): + return (poly([(2, 4), (14, 10), (14, 18), (2, 12)], N_LEFT) + + poly([(5, 7), (11, 10), (11, 14), (5, 11)], MUTED) + + poly([(5, 7), (11, 10), (8, 15.5), (2, 12.5)], G_TOP if s else N_TOP)) + + +@glyph("shade") +def _shade(s=True): + return (poly([(7, 6), (17, 11), (17, 17), (7, 12)], N_LEFT) + + poly([(7, 6), (17, 11), (11, 14), (1, 9)], G_TOP if s else N_TOP)) + + +@glyph("zone") +def _zone(s=True): + P = iso(10, 7) + out = [poly([P(0, 0, 0), P(4.5, 0, 0), P(4.5, 4.5, 0), P(0, 4.5, 0)], G_LEFT if s else N_FLAT, sw=1.3)] + n = (N_TOP, N_LEFT, N_RIGHT) + out.append(box(P, 0.5, 0.5, 0, 1.6, 1.6, 5, n)) + out.append(box(P, 2.4, 2.4, 0, 1.6, 1.6, 5, n)) + return "".join(out) + + +@glyph("level") +def _level(s=True): + return (path("M2 13H20", sw=1.6) + path("M2 6.5H20", sw=1, extra=' stroke-dasharray="2 1.5"') + + poly([(3, 8), (9, 8), (6, 13)], G_LEFT if s else N_TOP)) + + +@glyph("shell") +def _shell(s=True): + return prism(iso(10, 13.5), hexplan(3.2, 15), 0, 8, tones(s)) + + +@glyph("face") +def _face(s=True): + P = iso(10, 7) + plan = [(0, 0), (5, 0), (5, 3), (3, 5), (0, 5)] + return prism(P, plan, 0, 1, tones(s)) + + +@glyph("geometry") +def _geometry(s=True): + h = [(10 + 7 * math.cos(math.radians(60 * i)), 10 + 7 * math.sin(math.radians(60 * i))) for i in range(6)] + out = [poly(h, G_TOP if s else N_FLAT)] + out += [circle(x, y, 1.5, G_RIGHT if s else MUTED, stroke=None) for x, y in h[3:6]] + return "".join(out) + + +@glyph("geometry2D") +def _geometry2d(s=True): + p = [(3, 4), (16, 4), (16, 10), (10, 10), (10, 17), (3, 17)] + return poly(p, G_TOP if s else N_FLAT) + "".join(circle(x, y, 1.4, G_RIGHT if s else MUTED, stroke=None) for x, y in p[:5:2]) + + +@glyph("plane") +def _plane(s=True): + P = iso(10, 9) + return (poly([P(0, 0, 0), P(4, 0, 0), P(4, 4, 0), P(0, 4, 0)], G_TOP if s else N_TOP) + + path("M10 13V3M8 5L10 3L12 5", sw=1.5)) + + +@glyph("transform3D") +def _transform(s=True): + return (path("M9 12V2.5M7.3 4.2L9 2.5L10.7 4.2", sw=1.5) + path("M9 12L17 16", sw=1.5) + + path("M9 12L1.5 15.8", sw=1.5) + circle(9, 12, 2.2, G_LEFT if s else N_TOP)) + + +@glyph("mesh") +def _mesh(s=True): + P = iso(10, 7) + a, b, c, d, m = P(0, 0, 0), P(5, 0, 0), P(5, 5, 0), P(0, 5, 0), P(2.5, 2.5, 0) + t1, t2 = (G_TOP, G_LEFT) if s else (N_TOP, N_LEFT) + return poly([a, b, m], t1) + poly([b, c, m], t2) + poly([c, d, m], t1) + poly([d, a, m], t2) + + +@glyph("polyloop") +def _polyloop(s=True): + p = [(3, 5), (12, 2.5), (17, 9), (11, 16), (4, 13)] + return poly(p, G_TOP if s else "none", stroke=DEEP if s else INK, sw=1.8) + "".join( + circle(x, y, 1.3, INK, stroke=None) for x, y in p) + + +@glyph("segment") +def _segment(s=True): + return path("M3 15L16 4", stroke=DEEP if s else INK, sw=2) + circle(3, 15, 2, WHITE) + circle(16, 4, 2, WHITE) + + +@glyph("points") +def _points(s=True): + fill = G_LEFT if s else N_TOP + return circle(5, 13, 2.4, fill) + circle(11, 4.5, 2.4, fill) + circle(15, 11, 2.4, fill) + + +@glyph("vector") +def _vector(s=True): + return path("M3 16L15 4M8.5 4H15V10.5", stroke=DEEP if s else INK, sw=2.2) + + +@glyph("sectionBox") +def _section_box(s=True): + P = iso(12, 12) + n = (N_TOP, N_LEFT, N_RIGHT) + out = [poly([P(0, 0, 3), P(4, 0, 3), P(4, 4, 3), P(0, 4, 3)], G_LEFT if s else N_TOP)] + out.append(poly([P(0, 4, 3), P(4, 4, 3), P(4, 4, 0), P(0, 4, 0)], n[1])) + out.append(poly([P(4, 0, 3), P(4, 4, 3), P(4, 4, 0), P(4, 0, 0)], n[2])) + out.append(box(P, 0, 0, 7, 4, 4, 3, n)) + return "".join(out) + + +def _slab(P, x0, dx, t, dy=3.5, dz=9): + return box(P, x0, 0, 0, dx, dy, dz, t) + + +@glyph("construction") +def _construction(s=True): + # one wall block, three adjoining layers: neutral | green core | neutral (ink only at seams) + P = iso(8, 11) + n = (N_TOP, N_LEFT, N_RIGHT) + return (box(P, 0, 0, 0, 1.3, 3.5, 9, n) + box(P, 1.3, 0, 0, 2.0, 3.5, 9, tones(s)) + + box(P, 3.3, 0, 0, 1.3, 3.5, 9, n)) + + +@glyph("constructionLayer") +def _construction_layer(s=True): + return _slab(iso(9, 11), 1.6, 1.1, tones(s)) + + +@glyph("materialLayer") +def _material_layer(s=True): + P = iso(9, 11) + h = "".join(f"M{f(P(2.7, y, 8.2)[0])} {f(P(2.7, y, 8.2)[1])}L{f(P(2.7, y - 1.4, 0.8)[0])} {f(P(2.7, y - 1.4, 0.8)[1])}" for y in (2, 3.3)) + return _slab(P, 1.6, 1.1, tones(s)) + path(h, sw=0.9) + + +@glyph("apertureConstruction") +def _aperture_construction(s=True): + # double-glazing unit: rear pane + front pane with SAM-green glass + rear = [(6, 2), (16, 7), (16, 15), (6, 10)] + front = [(2, 5), (12, 10), (12, 18), (2, 13)] + return (poly(rear, N_TOP) + poly(front, N_LEFT) + + poly([(4, 7.6), (10, 10.6), (10, 15.4), (4, 12.4)], G_LEFT if s else N_TOP)) + + +@glyph("material") +def _material(s=True): + # material sample sphere: body + shaded crescent + highlight + return (circle(10, 10, 7.5, G_LEFT if s else N_TOP) + + path("M4.2 13.6A7.5 7.5 0 0 0 17.5 10A7.5 6 0 0 1 4.2 13.6Z", fill=G_RIGHT if s else N_RIGHT, stroke="none") + + circle(10, 10, 7.5, "none") + circle(7.2, 6.8, 1.6, WHITE, stroke=None)) + + +@glyph("transparentMaterial") +def _transparent_material(s=True): + return (circle(10, 10, 7.5, "#F3F8EC" if s else WHITE) + path("M6.5 6.8A4.8 4.8 0 0 1 11 4.8", stroke=DEEP, sw=1.4) + + path("M5.5 14.5L14.5 5.5", stroke=G_LEFT if s else N_RIGHT, sw=1.2, extra=' stroke-dasharray="1.6 1.2"')) + + +@glyph("gasMaterial") +def _gas_material(s=True): + out = [circle(10, 10, 7.5, WHITE).replace("/>", ' stroke-dasharray="2 1.4"/>')] + out += [circle(x, y, 1.3, G_RIGHT if s else MUTED, stroke=None) for x, y in ((7, 7), (12.5, 6.5), (6.5, 12.5), (11, 11), (14, 13))] + return "".join(out) + + +@glyph("profile") +def _profile(s=True): + return (path("M3 13.5H6.5V6H11V10H16.5V16H3Z", fill=G_TOP if s else N_FLAT, stroke="none") + + path("M3 2.5V16H17", sw=1.3) + path("M3 13.5H6.5V6H11V10H16.5", stroke=DEEP if s else INK, sw=1.8)) + + +@glyph("internalCondition") +def _person(s=True): + fill = G_LEFT if s else N_TOP + return circle(10, 5.5, 3.2, fill) + path("M3.5 17.5a6.5 6.5 0 0 1 13 0z", fill=fill) + + +@glyph("degreeOfActivity") +def _activity(s=True): + return _person(s) + path("M1 8.5h2.2M0.8 11.5h2", sw=1.3) + path("M17 8.5h2.2M17.2 11.5h2", sw=1.3) + + +@glyph("lighting") +def _bulb(s=True): + return (path("M10 2.5a5.5 5.5 0 0 1 3.2 10v2.3h-6.4v-2.3A5.5 5.5 0 0 1 10 2.5z", fill=G_TOP if s else N_TOP) + + rect(7, 15.8, 6, 2.2, MUTED, rx=0.6, stroke=None)) + + +@glyph("equipment") +def _equipment(s=True): + return rect(2.5, 3, 15, 10, G_TOP if s else N_TOP, rx=1) + path("M10 13V16.5M6 17H14", sw=1.5) + + +@glyph("infiltration") +def _wind(s=True): + c = DEEP if s else INK + return (path("M2 7H12a2.4 2.4 0 1 0-2.4-2.4", stroke=c, sw=1.8) + path("M2 11H16", stroke=c, sw=1.8) + + path("M2 15H11a2.4 2.4 0 1 1-2.4 2.4", stroke=c, sw=1.8)) + + +@glyph("pollutant") +def _pollutant(s=True): + return (path("M5 15a3.5 3.5 0 0 1 0-7a4.5 4.5 0 0 1 8.6-1.2A3.8 3.8 0 0 1 15 15z", fill=G_TOP if s else N_TOP) + + circle(7.5, 11.5, 1.1, INK, stroke=None) + circle(11, 10, 1.1, INK, stroke=None) + circle(12.5, 13, 1.1, INK, stroke=None)) + + +@glyph("thermometer") +def _thermo(s=True): + return (path("M8 11.5V4a2 2 0 0 1 4 0v7.5a3.8 3.8 0 1 1-4 0z", fill=WHITE) + + path("M10 7.5V13", stroke=G_RIGHT if s else MUTED, sw=2.2) + circle(10, 14.5, 2.1, G_LEFT if s else MUTED, stroke=None) + + path("M13.5 5h2M13.5 8h2", sw=1)) + + +@glyph("fan") +def _fan(s=True): + out = [circle(10, 10, 7.5, G_TOP if s else N_TOP)] + for a in (0, 120, 240): + r = math.radians(a) + c, sn = math.cos(r), math.sin(r) + tip = (10 + 5.8 * c, 10 + 5.8 * sn) + side = (10 + 4.2 * math.cos(r + 0.9), 10 + 4.2 * math.sin(r + 0.9)) + out.append(poly([(10, 10), tip, side], G_RIGHT if s else MUTED, sw=0.8)) + out.append(circle(10, 10, 1.4, WHITE)) + return "".join(out) + + +@glyph("airflow") +def _airflow(s=True): + c = DEEP if s else INK + return "".join(path(f"M2 {y}H14M11 {y - 2.5}L14 {y}L11 {y + 2.5}", stroke=c, sw=1.8) for y in (5, 10, 15)) + + +@glyph("izam") +def _izam(s=True): + P = iso(9, 12) + n = (N_TOP, N_LEFT, N_RIGHT) + return (box(P, 0, 1, 0, 2.2, 2.2, 5, n) + box(P, 3.4, -1.2, 0, 2.2, 2.2, 5, n) + + path("M4 5.2Q9 0.6 14.5 3.2M11.8 1.6L14.5 3.2L12.3 5.2", stroke=DEEP if s else INK, sw=1.7)) + + +@glyph("ahu") +def _ahu(s=True): + P = iso(9, 11) + cx, cy = P(2.5, 2.5, 3) + return box(P, 0, 0, 0, 5, 2.5, 6, tones(s)) + circle(cx, cy, 2.4, WHITE) + path( + f"M{f(cx - 1.3)} {f(cy - 1.3)}L{f(cx + 1.3)} {f(cy + 1.3)}M{f(cx + 1.3)} {f(cy - 1.3)}L{f(cx - 1.3)} {f(cy + 1.3)}", sw=0.9) + + +@glyph("system") +def _system(s=True): + return (rect(2.5, 6, 15, 8, G_TOP if s else N_TOP, rx=1) + path("M2.5 10H0.8M17.5 10H19.2", sw=2) + + circle(10, 10, 2.8, WHITE) + path("M8.2 8.2L11.8 11.8M11.8 8.2L8.2 11.8", sw=1)) + + +@glyph("location") +def _pin(s=True): + return (path("M3 17.5H17", sw=1.3) + path("M10 17C6 12.5 4.2 9.8 4.2 7.2a5.8 5.8 0 0 1 11.6 0C15.8 9.8 14 12.5 10 17z", fill=G_LEFT if s else N_TOP) + + circle(10, 7.2, 2, WHITE)) + + +@glyph("address") +def _address(s=True): + return rect(1.5, 9.5, 12, 8, WHITE, rx=1) + path("M4 12.5h7M4 15h4.5", sw=1.1) + path( + "M13 12C10.5 9.2 9.5 7.6 9.5 6a3.5 3.5 0 0 1 7 0C16.5 7.6 15.5 9.2 13 12z", fill=G_LEFT if s else N_TOP) + circle(13, 6, 1.1, WHITE, stroke=None) + + +def _sun(cx, cy, r, rays=True): + out = "" + if rays: + d = "".join(f"M{f(cx + (r + 1.3) * math.cos(math.radians(a)))} {f(cy + (r + 1.3) * math.sin(math.radians(a)))}" + f"L{f(cx + (r + 3) * math.cos(math.radians(a)))} {f(cy + (r + 3) * math.sin(math.radians(a)))}" for a in range(0, 360, 45)) + out += path(d, sw=1.2) + return out + circle(cx, cy, r, YELLOW) + + +@glyph("weather") +def _weather(s=True): + return _sun(7.5, 7, 3.2) + path("M7 17.5a3.4 3.4 0 0 1 0-6.8a4.4 4.4 0 0 1 8.4-0.8A3.6 3.6 0 0 1 16 17.5z", fill=G_TOP if s else N_TOP) + + +@glyph("weatherData") +def _weather_data(s=True): + return _sun(6.5, 6, 2.6) + path("M2 17.5H18", sw=1.3) + "".join( + rect(x, y, 2.4, 17.5 - y, G_LEFT if s else N_TOP, stroke=INK, sw=0.9) for x, y in ((9, 11), (12.2, 8), (15.4, 12))) + + +@glyph("weatherYear") +def _weather_year(s=True): + return (rect(2.5, 4, 15, 13.5, WHITE, rx=1) + rect(2.5, 4, 15, 3.5, G_LEFT if s else N_LEFT, rx=1) + + "".join(circle(x, y, 0.9, INK, stroke=None) for x in (6, 10, 14) for y in (10.5, 14)) + _sun(15.5, 3.5, 2.2, rays=False)) + + +@glyph("designDay") +def _design_day(s=True): + return (path("M2 16H18", sw=1.3) + path("M3.5 16a6.5 6.5 0 0 1 13 0", stroke=DEEP if s else INK, sw=1.4, extra=' stroke-dasharray="1.8 1.3"') + + _sun(10, 9.5, 2.6, rays=True)) + + +@glyph("clock") +def _clock(s=True): + return circle(10, 10, 7.5, G_TOP if s else N_TOP) + path("M10 5.5V10L13.5 12", sw=1.7) + + +@glyph("calendar") +def _calendar(s=True): + return (rect(2.5, 4, 15, 13.5, WHITE, rx=1) + rect(2.5, 4, 15, 3.5, G_LEFT if s else N_LEFT, rx=1) + + "".join(circle(x, y, 0.9, INK, stroke=None) for x in (6, 10, 14) for y in (10.5, 14)) + path("M6.5 2.5v3M13.5 2.5v3", sw=1.4)) + + +@glyph("result") +def _result(s=True): + fill = G_LEFT if s else N_TOP + return path("M2 17.5H18", sw=1.3) + rect(3, 11, 3.4, 6.5, fill) + rect(8, 6, 3.4, 11.5, fill) + rect(13, 2.5, 3.4, 15, fill) + + +@glyph("table") +def _table(s=True): + return (rect(2, 3, 16, 14, WHITE, rx=1) + rect(2, 3, 16, 4, G_LEFT if s else N_LEFT, rx=1) + + path("M2 11.5H18M7.5 3V17M13 3V17", sw=0.9)) + + +@glyph("textMap") +def _text_map(s=True): + return (rect(1.5, 3, 6, 14, WHITE, rx=1) + rect(12.5, 3, 6, 14, G_TOP if s else N_TOP, rx=1) + + path("M3.5 7h2M3.5 10h2M3.5 13h2M14.5 7h2M14.5 10h2M14.5 13h2", sw=1.1) + path("M8.5 10H11.5M10.3 8.8L11.5 10L10.3 11.2", sw=1.1)) + + +@glyph("value") +def _tag(s=True): + return poly([(2, 10), (7, 4.5), (17.5, 4.5), (17.5, 15.5), (7, 15.5)], G_LEFT if s else N_TOP) + circle(7, 10, 1.4, WHITE) + + +@glyph("name") +def _name(s=True): + return rect(1.5, 5, 16, 10, WHITE, rx=2) + path("M4.5 8.5h10M4.5 11.5h6", stroke=DEEP if s else INK, sw=1.8) + + +@glyph("json") +def _json(s=True): + c = DEEP if s else INK + return (path("M7 3C4.5 3 5.5 9 3 10C5.5 11 4.5 17 7 17", stroke=c, sw=1.8) + path("M13 3C15.5 3 14.5 9 17 10C14.5 11 15.5 17 13 17", stroke=c, sw=1.8) + + circle(10, 10, 1.4, G_LEFT if s else INK, stroke=None)) + + +@glyph("file") +def _file(s=True): + return (path("M3.5 2H11.5L16 6.5V18H3.5Z", fill=WHITE) + path("M11.5 2V6.5H16", fill=G_LEFT if s else N_LEFT) + + path("M6 10h7.5M6 13h7.5M6 16h4", stroke=DEEP if s else INK, sw=1.1)) + + +@glyph("log") +def _log(s=True): + return (path("M3.5 2H16V18H3.5Z", fill=WHITE) + path("M6 5.5h7.5M6 8.5h7.5M6 11.5h7.5M6 14.5h4", sw=1.1) + + rect(3.5, 2, 2, 16, G_LEFT if s else N_LEFT)) + + +@glyph("colour") +def _colour(s=True): + return rect(2, 2, 6.5, 6.5, RED) + rect(6.5, 6.5, 6.5, 6.5, YELLOW) + rect(11, 11, 6.5, 6.5, BLUE) + + +@glyph("number") +def _number(s=True): + return (rect(1.5, 7, 17, 7, G_TOP if s else N_TOP, rx=1) + + path("M4.5 7v3M7.5 7v2M10.5 7v3M13.5 7v2M16.5 7v3", sw=1)) + + +@glyph("text") +def _text(s=True): + fill = G_LEFT if s else INK + return (path("M3 12.5a3 3 0 1 1 3 3c0 0 0 2.5-2.5 3.5c1-1.5 1-2 0.8-2.5A3 3 0 0 1 3 12.5z", fill=fill, sw=0.9) + + path("M10 12.5a3 3 0 1 1 3 3c0 0 0 2.5-2.5 3.5c1-1.5 1-2 0.8-2.5A3 3 0 0 1 10 12.5z", fill=fill, sw=0.9) + + path("M3 5.5h14", sw=1.3)) + + +@glyph("list") +def _list(s=True): + return "".join(circle(3.5, y, 1.5, G_LEFT if s else INK, stroke=None) + path(f"M7 {y}H17", sw=1.6) for y in (4.5, 10, 15.5)) + + +@glyph("tree") +def _tree(s=True): + fill = G_LEFT if s else N_TOP + return (path("M4 10H8M8 4V16M8 4H12M8 10H12M8 16H12", sw=1.4) + circle(3.5, 10, 2, fill) + + circle(14, 4, 2, fill) + circle(14, 10, 2, fill) + circle(14, 16, 2, fill)) + + +@glyph("folder") +def _folder(s=True): + return path("M2 5.5a1 1 0 0 1 1-1H8l1.8 2H17a1 1 0 0 1 1 1V16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z", fill=G_LEFT if s else N_TOP) + + +@glyph("object") +def _object(s=True): + # echo of the SAM-BIM emblem: the green rounded tile (no lettering) + return rect(2.5, 2.5, 14, 14, EMBLEM if s else N_TOP, rx=2.5) + path("M6 8h7M6 11h4.5", stroke=WHITE if s else INK, sw=1.6) + + +@glyph("relation") +def _relation(s=True): + fill = G_LEFT if s else N_TOP + return (path("M5 14L10 4.5L15.5 13Z", sw=1.3) + circle(10, 4.5, 2.6, fill) + + circle(4.5, 14.5, 2.6, fill) + circle(15.5, 13, 2.6, fill)) + + +@glyph("group") +def _group(s=True): + return (rect(1.5, 2.5, 17, 15, "none", rx=2.5, extra=' stroke-dasharray="2 1.4"') + + rect(4, 5, 5.5, 5.5, G_LEFT if s else N_TOP, rx=1) + rect(10.5, 9, 5.5, 5.5, G_LEFT if s else N_TOP, rx=1)) + + +@glyph("indexed") +def _indexed(s=True): + return "".join(rect(2 + 5.5 * i, 7, 4.6, 6, G_LEFT if s and i == 1 else N_TOP, rx=0.8) for i in range(3)) + path( + "M4.3 4.5v1M9.8 4.5v1M15.3 4.5v1", sw=1.2) + + +@glyph("type") +def _type(s=True): + return (circle(5.5, 6, 3.5, N_TOP) + rect(10, 2.5, 7, 7, G_LEFT if s else N_TOP, rx=0.5) + + poly([(3, 17.5), (7.5, 10.5), (12, 17.5)], N_TOP)) + + +@glyph("settings") +def _settings(s=True): + return (path("M2 6H18M2 14H18", sw=1.4) + circle(12.5, 6, 2.6, G_LEFT if s else N_TOP) + + circle(6.5, 14, 2.6, G_LEFT if s else N_TOP)) + + +@glyph("compass") +def _compass(s=True): + return circle(10, 10, 7.5, WHITE) + poly([(10, 3.5), (12.4, 10), (7.6, 10)], G_LEFT if s else INK, sw=0.9) + poly( + [(10, 16.5), (12.4, 10), (7.6, 10)], N_LEFT, sw=0.9) + + +@glyph("heat") +def _heat(s=True): + c = DEEP if s else INK + return (rect(8, 2, 4, 16, N_LEFT) + path("M1.5 10H17.5M14.5 7L17.5 10L14.5 13", stroke=c, sw=1.8) + + path("M3 4.5q1.2 1.2 0 2.4t0 2.4", stroke=c, sw=1.1)) + + +@glyph("daylight") +def _daylight(s=True): + P = iso(11, 8) + return (poly([P(0, 0, 0), P(4, 0, 0), P(4, 4, 0), P(0, 4, 0)], G_LEFT if s else N_TOP) + + path("M5 5.5L9 11.5M8 3.5L13 11", stroke=INK, sw=1.1, extra=' stroke-dasharray="1.5 1"') + _sun(3.5, 3.5, 2.3, rays=False)) + + +@glyph("grid") +def _grid(s=True): + P = iso(10, 7) + out = [poly([P(0, 0, 0), P(5, 0, 0), P(5, 5, 0), P(0, 5, 0)], G_TOP if s else N_TOP)] + out += [circle(*P(i, j, 0), 0.9, INK, stroke=None) for i in (1, 2.5, 4) for j in (1, 2.5, 4)] + return "".join(out) + + +@glyph("normal") +def _normal(s=True): + return poly([(6, 4), (16, 9), (16, 17), (6, 12)], N_LEFT) + path("M11 10.5L2.5 14.8M3.4 12.2L2.5 14.8L5.2 15.4", stroke=DEEP if s else INK, sw=1.8) + + +@glyph("matrix") +def _matrix(s=True): + return (path("M4.5 3H2.5V17H4.5M15.5 3H17.5V17H15.5", sw=1.4) + + "".join(circle(x, y, 1.3, G_RIGHT if s and x == y else INK, stroke=None) for x in (6, 10, 14) for y in (6, 10, 14))) + + +@glyph("info") +def _info(s=True): + return rect(2.5, 2.5, 15, 15, EMBLEM, rx=2.5) + circle(10, 6.3, 1.4, WHITE, stroke=None) + path("M10 9.5V14.5", stroke=WHITE, sw=2.2) + + +@glyph("constructionManager") +def _manager(s=True): + return scaled(G["apertureConstruction"](False), 0.72, 6.5, -2.5) + scaled(G["construction"](s), 0.78, -2, 3) + + +# ---------------------------------------------------------------- composition modifiers +def scaled(svg, k, tx, ty): + """Place a glyph scaled by k while keeping the outline weight constant (SW at 24 px).""" + import re + svg = re.sub(r'stroke-width="([\d.]+)"', lambda m: f'stroke-width="{f(float(m.group(1)) / k)}"', svg) + return f'{svg}' + + +def compose_plural(fn, s): + return scaled(fn(s), 0.78, 5, -1.5) + scaled(fn(s), 0.78, -0.5, 3.6) + + +def compose_add(fn, s, plural=False): + """ADD: an existing (neutral) instance with the new one(s) in SAM green in front of it.""" + if plural: + return scaled(fn(False), 0.66, 7, -2) + scaled(fn(s), 0.66, 3.2, 1.2) + scaled(fn(s), 0.66, -0.6, 4.6) + return scaled(fn(False), 0.78, 5, -1.5) + scaled(fn(s), 0.78, -0.5, 3.6) + + +def compose_library(fn, s, add=False): + cards = (rect(7.5, 1.5, 12, 12, WHITE, rx=1.2) + rect(5.5, 3.5, 12, 12, WHITE, rx=1.2) + + path("M9.5 4.5h7M9.5 6.5h5", sw=1)) + if add: # a new green item going into an existing (neutral) library + return cards + scaled(fn(False), 0.74, -0.5, 4.2) + scaled(fn(s), 0.5, 10, -0.5) + return cards + scaled(fn(s), 0.74, -0.5, 4.2) + + +def effect_split(fn, s): + a = '' + b = '' + return (f'{a}{b}{fn(s)}' + f'{fn(s)}') + + +def effect_intersect(fn, s): + return (f'{_silhouette(fn)}' + + scaled(fn(False), 0.78, 5, -1.5) + scaled(fn(False), 0.78, -0.5, 3.6) + + f'{scaled(fn(True), 0.78, 5, -1.5)}') + + +def effect_difference(fn, s): + return scaled(fn(True), 0.78, -0.5, 3.6) + scaled(_cutter(fn), 0.78, 5, -1.5) + + +def _silhouette(fn): + import re + return re.sub(r' stroke="[^"]*"', "", fn(False)) + + +def _cutter(fn): + import re + s = fn(False) + s = re.sub(r'fill="(?!none)[^"]*"', 'fill="#FFFFFF"', s) + return s.replace("' + + +BADGES = { + # op: (family, glyph(c)) + "create": ("construct", lambda c: _g("M19 16.6v4.8M16.6 19h4.8", c, 1.7)), + "add": ("construct", lambda c: _g("M19 16.6v4.8M16.6 19h4.8", c, 1.7)), + "copy": ("construct", lambda c: _g("M16.9 17.8h2.7v3.3h-2.7zM18.4 17.8v-1.2h2.7v3.3h-1.5", c, 1.1)), + "get": ("query", lambda c: _g("M17 21L21 17M18.2 17H21V19.8", c)), + "filter": ("query", lambda c: _g("M16.5 17H21.5L19.8 19.2V21.4L18.2 20.6V19.2Z", c, 1.1, fill=c)), + "inspect": ("query", lambda c: _g("M18.4 18.4m-1.7 0a1.7 1.7 0 1 0 3.4 0a1.7 1.7 0 1 0-3.4 0M19.7 19.7L21.2 21.2", c, 1.3)), + "list": ("query", lambda c: _g("M16.8 17.3h4.4M16.8 19h4.4M16.8 20.7h4.4", c, 1.1)), + "display": ("query", lambda c: _g("M16.2 19Q19 15.6 21.8 19Q19 22.4 16.2 19Z", c, 1.1) + f''), + "set": ("change", lambda c: _g("M21.2 16.8L17.3 20.7M16.9 17.8V21.1H20.2", c, 1.7)), + "modify": ("change", lambda c: _g("M19 16.2L21.6 21H16.4Z", c, 1.6)), + "update": ("change", lambda c: _g("M21.3 18.6A2.4 2.4 0 1 1 20.1 16.9", c, 1.7) + ''), + "merge": ("change", lambda c: _g("M16.6 16.6L19 19.2L21.4 16.6M19 19.2V21.6", c, 1.8)), + "split": ("change", lambda c: _g("M19 16.4V18.8L16.6 21.4M19 18.8L21.4 21.4", c, 1.8)), + "section": ("change", lambda c: _g("M16.9 16.9h4.2v4.2h-4.2z", c, 1.2) + _g("M15.9 19h6.2", c, 1.7)), + "union": ("change", lambda c: _g("M16.9 16.6v2.4a2.1 2.1 0 0 0 4.2 0v-2.4", c, 1.7)), + "intersect": ("change", lambda c: _g("M16.9 21.4v-2.4a2.1 2.1 0 0 1 4.2 0v2.4", c, 1.7)), + "difference": ("change", lambda c: _g("M16.6 16.6h4.8v2.4h-2.4v2.4h-2.4z", c, 1.1, fill=c)), + "offset": ("change", lambda c: _g("M16.5 16.5h5v5h-5z", c, 1.3) + ''), + "transform": ("change", lambda c: _g("M17.6 19h2.8", c, 1.7) + ''), + "snap": ("change", lambda c: _g("M21 16.4V21.6", c, 1.8) + f''), + "align": ("change", lambda c: _g("M16.8 16.4V21.6", c, 1.6) + _g("M18.5 17.6h3.1M18.5 20.4h1.8", c, 1.8)), + "extend": ("change", lambda c: _g("M16.3 19h3", c, 1.8) + _g("M21.6 16.5V21.5", c, 1.6) + ''), + "flip": ("change", lambda c: _g("M17.6 21.4V17.6M20.4 16.6V20.4", c, 1.6) + ''), + "triangulate": ("change", lambda c: _g("M19 16.3L21.6 21.2H16.4ZM19 16.3V21.2", c, 1.4)), + "sort": ("change", lambda c: _g("M16.6 17.1h4.8M16.6 19h3.2M16.6 20.9h1.6", c, 1.6)), + "remove": ("remove", lambda c: _g("M16.6 19h4.8", c, 1.8)), + "error": ("remove", lambda c: _g("M19 16.7v2.7", c, 1.6) + f''), + "calculate": ("evaluate", lambda c: _g("M16.8 17.9h4.4M16.8 20.1h4.4", c, 1.4)), + "sum": ("evaluate", lambda c: _g("M19 16.8v4.4M16.8 19h4.4", c, 1.4)), + "multiply": ("evaluate", lambda c: _g("M17.3 17.3l3.4 3.4M20.7 17.3l-3.4 3.4", c, 1.4)), + "divide": ("evaluate", lambda c: _g("M16.8 19h4.4", c, 1.3) + f''), + "analyse": ("evaluate", lambda c: _g("M17.2 21.2v-1.8M19 21.2v-4.2M20.8 21.2v-2.8", c, 1.3)), + "validate": ("evaluate", lambda c: _g("M16.9 19.1L18.4 20.6L21.2 17.4", c, 1.5)), + "convert": ("transfer", lambda c: _g("M16.7 17.6H19.8M21.3 20.4H18.2", c, 1.5) + ''), + "import": ("transfer", lambda c: _g("M19 16.1V19.4", c, 2.0) + ''), + "export": ("transfer", lambda c: _g("M19 21.9V18.6", c, 2.0) + ''), + "value": ("value", lambda c: _g("M16.9 17.3h4.2M16.9 19h4.2M16.9 20.7h4.2", c, 1.3)), +} + + +def badge(op): + fam, g = BADGES[op] + disc, gc, ring = FAMILIES[fam] + r = f' stroke="{ring}" stroke-width="0.9"' if ring else "" + return (f'' + f'' + g(gc)) + + +# ---------------------------------------------------------------- icon +def icon_svg(obj, op=None, plural=False, container=None, subject=True, comment=""): + fn = G[obj] + if container == "library": + base = lambda s: compose_library(fn, s, add=op == "add") # noqa: E731 + elif op == "add": + base = lambda s: compose_add(fn, s, plural) # noqa: E731 + elif plural: + base = lambda s: compose_plural(fn, s) # noqa: E731 + else: + base = fn + body = EFFECTS[op](base, subject) if op in EFFECTS else base(subject) + b = badge(op) if op else "" + note = f"" if comment else "" + return (f'{note}' + f'{body}{b}\n') diff --git a/design/grasshopper-icons/tools/icons_ext.py b/design/grasshopper-icons/tools/icons_ext.py new file mode 100644 index 0000000..b222722 --- /dev/null +++ b/design/grasshopper-icons/tools/icons_ext.py @@ -0,0 +1,223 @@ +"""SAM-BIM extension glyphs for the Grasshopper icon grammar (shared by every SAM-BIM plugin repo). + +`icons.py` is the frozen SAM design system (SAM-BIM/SAM#166, vendored verbatim). This module only +*adds* object glyphs for domains that SAM itself has no noun for (psychrometrics, topology, +optimisation, scripting, acoustics, Revit documents, HVAC plant, solar). It adds one verb, `run` +(Evaluate family), and does not change any colour, family, badge, modifier or existing glyph. +The file is byte-identical in every SAM-BIM repository that uses it; see ICON_DESIGN_SYSTEM_EXT.md. +""" +import math + +import icons +from icons import (BLUE, DEEP, EMBLEM, G_LEFT, G_RIGHT, G_TOP, INK, MUTED, N_FLAT, N_LEFT, N_RIGHT, N_TOP, WHITE, # noqa: F401 + YELLOW, _g, _sun, box, circle, f, glyph, iso, path, poly, rect, tones) + +EXT_VERSION = "1" + + +# ---------------------------------------------------------------- psychrometrics (Mollier) +def _chart(s, strong=True): + """Psychrometric chart frame: axes + saturation curve. Green area when the chart is the subject.""" + out = "" + if strong: + out += path("M3.8 15.2C8.6 14.6 12.4 10.8 15.2 3.2V15.2Z", fill=G_TOP if s else N_FLAT, stroke="none") + out += path("M3 2.5V16H17", sw=1.3) + out += path("M3.8 15.2C8.6 14.6 12.4 10.8 15.2 3.2", stroke=DEEP if (s and strong) else (MUTED if strong else N_RIGHT), sw=1.6 if strong else 1.1) + return out + + +def _arrow(x0, y0, x1, y1, c=DEEP, sw=2.2, head=2.8): + """Bold line with a filled arrowhead at (x1, y1).""" + a = math.atan2(y1 - y0, x1 - x0) + bx, by = x1 - head * math.cos(a), y1 - head * math.sin(a) + l = (bx + head * 0.62 * math.sin(a), by - head * 0.62 * math.cos(a)) + r = (bx - head * 0.62 * math.sin(a), by + head * 0.62 * math.cos(a)) + return (path(f"M{f(x0)} {f(y0)}L{f(bx)} {f(by)}", stroke=c, sw=sw) + + f'') + + +@glyph("mollierChart") +def _mollier_chart(s=True): + return _chart(s) + + +@glyph("mollierPoint") +def _mollier_point(s=True): + return _chart(False, strong=False) + circle(9, 9.5, 2.6, G_LEFT if s else N_TOP, sw=1.2) + + +def _process(s, body): + # process arrows use the mid subject green (#659335): it reads on the light, orange and dark GH bodies + return _chart(False, strong=False) + body(G_RIGHT if s else INK) + + +@glyph("mollierProcess") +def _mollier_process(s=True): + # generic / undefined process: two states joined (no direction) + c = G_LEFT if s else N_TOP + return _chart(False, strong=False) + path("M5.5 12.5L13 6", stroke=G_RIGHT if s else INK, sw=2.2) + circle(5.5, 12.5, 2.1, c) + circle(13, 6, 2.1, c) + + +@glyph("processHeating") +def _p_heating(s=True): + return _process(s, lambda c: _arrow(4.5, 10, 14.5, 10, c)) + + +@glyph("processCooling") +def _p_cooling(s=True): + return _process(s, lambda c: _arrow(14.5, 10, 4.5, 10, c)) + + +@glyph("processHumidification") +def _p_humid(s=True): + return _process(s, lambda c: _arrow(9, 14.5, 9, 4, c)) + + +@glyph("processAdiabatic") +def _p_adiabatic(s=True): + return _process(s, lambda c: _arrow(13.5, 13.5, 5.5, 5, c)) + + +@glyph("processRoom") +def _p_room(s=True): + return _process(s, lambda c: _arrow(5, 13.5, 13.5, 5, c)) + + +@glyph("processMixing") +def _p_mixing(s=True): + return _process(s, lambda c: _arrow(4, 13.5, 8.2, 10.2, c, sw=1.8, head=2.3) + _arrow(14.5, 5, 10.2, 8.6, c, sw=1.8, head=2.3) + + circle(9.2, 9.4, 1.3, G_LEFT if s else N_TOP, sw=0.9)) + + +@glyph("processHeatRecovery") +def _p_heat_recovery(s=True): + return _process(s, lambda c: _arrow(4.5, 7.5, 14, 7.5, c, sw=1.8, head=2.3) + _arrow(14, 12, 4.5, 12, c, sw=1.8, head=2.3)) + + +@glyph("processFan") +def _p_fan(s=True): + def body(c): + out = _arrow(8.5, 10, 15, 10, c) + out += circle(5.8, 10, 2.9, WHITE, sw=1) + for a in (30, 150, 270): + r = math.radians(a) + out += poly([(5.8, 10), (5.8 + 2.4 * math.cos(r), 10 + 2.4 * math.sin(r)), + (5.8 + 1.6 * math.cos(r + 0.9), 10 + 1.6 * math.sin(r + 0.9))], c, sw=0.5) + return out + return _process(s, body) + + +# ---------------------------------------------------------------- topology +@glyph("cellComplex") +def _cell_complex(s=True): + # one solid subdivided into cells: 2 x 2 x 2 cube with the cell seams drawn + P = iso(10, 10) + t = tones(s) + out = box(P, 0, 0, 0, 5, 5, 7, t) + seams = [(P(2.5, 0, 7), P(2.5, 5, 7)), (P(0, 2.5, 7), P(5, 2.5, 7)), # top + (P(0, 5, 3.5), P(5, 5, 3.5)), (P(2.5, 5, 0), P(2.5, 5, 7)), # left face + (P(5, 0, 3.5), P(5, 5, 3.5)), (P(5, 2.5, 0), P(5, 2.5, 7))] # right face + return out + path("".join(f"M{f(a[0])} {f(a[1])}L{f(b[0])} {f(b[1])}" for a, b in seams), sw=0.9) + + +# ---------------------------------------------------------------- optimisation +@glyph("algorithm") +def _algorithm(s=True): + # objective curve converging on its optimum (green) + return (path("M3 2.5V16H17", sw=1.3) + path("M4.8 3.8C6.6 12.6 8.4 13.6 10.2 13.6S14 10.6 16.4 5.4", stroke=MUTED, sw=1.3) + + circle(10.2, 13.2, 2.3, G_LEFT if s else N_TOP, sw=1.1)) + + +@glyph("objective") +def _objective(s=True): + return circle(10, 10, 7.5, WHITE) + circle(10, 10, 5, G_LEFT if s else N_TOP) + circle(10, 10, 2.4, WHITE) + circle(10, 10, 0.9, INK, stroke=None) + + +# ---------------------------------------------------------------- scripting / automation +@glyph("script") +def _script(s=True): + c = G_TOP if s else WHITE + return (rect(1.5, 3, 17, 14, INK if s else MUTED, rx=1.5) + path("M4.5 7L8 10L4.5 13", stroke=c, sw=1.8) + + path("M9.5 13.2H14", stroke=c, sw=1.8)) + + +@glyph("tasks") +def _tasks(s=True): + # parallel tasks (multitasking): three lanes running side by side + out = "" + for y in (4, 9.5, 15): + out += rect(2, y - 2, 11, 4, G_LEFT if s else N_TOP, rx=1) + out += path(f"M13.5 {f(y)}H16.5", sw=1.4) + poly([(16.2, y - 1.6), (18.4, y), (16.2, y + 1.6)], INK, sw=0.6) + return out + + +# ---------------------------------------------------------------- acoustics +@glyph("sound") +def _sound(s=True): + return (poly([(2, 7.5), (5.5, 7.5), (10, 3.5), (10, 16.5), (5.5, 12.5), (2, 12.5)], G_LEFT if s else N_TOP) + + path("M12.6 7.2a4 4 0 0 1 0 5.6M14.9 4.9a7.2 7.2 0 0 1 0 10.2", sw=1.5)) + + +# ---------------------------------------------------------------- BIM documents (Revit) +@glyph("view") +def _view(s=True): + # drawing sheet: plan drawing (green) + title block + return (rect(1.5, 2.5, 17, 14, WHITE, rx=1) + path("M4 5H11V8.5H13.5V13.5H4Z", fill=G_TOP if s else N_FLAT, stroke=DEEP if s else INK, sw=1.2) + + path("M15.5 2.5V16.5", sw=1)) + + +@glyph("element") +def _element(s=True): + # a single BIM element instance: cube on a neutral base plate + P = iso(10, 11.5) + base = poly([P(-1, -1, 0), P(5.5, -1, 0), P(5.5, 5.5, 0), P(-1, 5.5, 0)], N_FLAT, sw=0.9) + return base + box(P, 0.5, 0.5, 0, 4, 4, 4.5, tones(s)) + + +# ---------------------------------------------------------------- HVAC plant (SAM Systems) +@glyph("energyCentre") +def _energy_centre(s=True): + # central plant building with a stack + P = iso(9, 12) + n = (N_TOP, N_LEFT, N_RIGHT) + return box(P, 3.4, 0.2, 0, 1.2, 1.2, 10, n) + box(P, 0, 1.6, 0, 5, 3, 4.5, tones(s)) + + +@glyph("plantRoom") +def _plant_room(s=True): + # cutaway room corner (the SAM `floor` walls, neutral floor) with a plant unit standing in it (green) + P = iso(9, 11.5) + walls = poly([(9, 8), (9, 2), (1, 6), (1, 12)], "#EFEFEF") + poly([(9, 8), (17, 12), (17, 6), (9, 2)], N_RIGHT) + floor = poly([(9, 8), (17, 12), (9, 16), (1, 12)], N_TOP) + return walls + floor + box(P, -0.4, 0, 0, 3.4, 2.4, 4.6, tones(s)) + + +@glyph("connector") +def _connector(s=True): + fill = G_LEFT if s else N_TOP + return (path("M6.5 10H13.5", stroke=DEEP if s else INK, sw=2.4) + rect(1.5, 6.5, 5, 7, fill, rx=1) + + rect(13.5, 6.5, 5, 7, fill, rx=1) + circle(10, 10, 1.5, WHITE, sw=1)) + + +# ---------------------------------------------------------------- solar +@glyph("sunPath") +def _sun_path(s=True): + # sun position -> direction vector (green) + return (path("M2 17H18", sw=1.3) + _sun(13.8, 5.2, 2.4, rays=True) + + _arrow(11.6, 7.4, 4.2, 14.6, DEEP if s else INK, sw=2.0)) + + +@glyph("solar") +def _solar(s=True): + # solar irradiance: parallel rays onto a surface (green) + return (poly([(10, 5), (17, 8.5), (17, 16), (10, 12.5)], G_LEFT if s else N_LEFT) + _sun(4, 4, 2.4, rays=False) + + _arrow(5.2, 7.6, 9.6, 10, INK, sw=1.3, head=1.9) + _arrow(7.8, 4.6, 12.4, 7.2, INK, sw=1.3, head=1.9)) + + +# ---------------------------------------------------------------- verb: run / simulate (Evaluate family) +icons.BADGES["run"] = ("evaluate", lambda c: f'') + +EXT_GLYPHS = ["mollierChart", "mollierPoint", "mollierProcess", "processHeating", "processCooling", "processHumidification", + "processAdiabatic", "processRoom", "processMixing", "processHeatRecovery", "processFan", "cellComplex", "algorithm", + "objective", "script", "tasks", "sound", "view", "element", "energyCentre", "plantRoom", "connector", "sunPath", "solar"] +EXT_BADGES = ["run"] diff --git a/design/grasshopper-icons/tools/integrate.py b/design/grasshopper-icons/tools/integrate.py new file mode 100644 index 0000000..c19f8d8 --- /dev/null +++ b/design/grasshopper-icons/tools/integrate.py @@ -0,0 +1,208 @@ +"""Wire the generated 24x24 PNGs into this repository's Grasshopper projects, using each project's EXISTING icon mechanism. + +Per project (project_dir from the manifest): + * copies png/24/.png -> /Resources/Icons/.png (only the icons that project uses) + * mechanism "resx" (SAM convention): ResXFileRef entries in Properties/Resources.resx + strongly-typed properties in + Properties/Resources.Designer.cs, typed byte[] or Bitmap to match the resources the project's Icon getters use today + * mechanism "holder" (e.g. SAMOCCTIcon): generated properties inside `// ` markers of the holder class, + loading manifest resources the same way the holder already does, plus one csproj EmbeddedResource line + * replaces the icon token inside each component's `Icon` getter (Resources.OLD -> Resources.NEW); nothing else. +Idempotent: re-running replaces previously generated SAM_GH_* entries. Old resources are left untouched. +""" +import json +import os +import re +import shutil +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +DESIGN = os.path.dirname(HERE) +REPO = os.path.dirname(os.path.dirname(DESIGN)) +PNG = os.path.join(DESIGN, "png", "24") +PREFIX = "SAM_GH_" + +TYPE_BITMAP = "System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" +TYPE_BYTES = "System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" + + +def read(p): + raw = open(p, "rb").read() + bom = raw.startswith(b"\xef\xbb\xbf") + text = raw.decode("utf-8-sig") + nl = "\r\n" if "\r\n" in text else "\n" + return text.replace("\r\n", "\n"), bom, nl + + +def write(p, text, bom, nl): + data = text.replace("\n", nl).encode("utf-8") + open(p, "wb").write((b"\xef\xbb\xbf" if bom else b"") + data) + + +# ------------------------------------------------------------------ resx mechanism (identical to SAM#166) +def update_resx(path, names, as_bytes): + text, bom, nl = read(path) + text = re.sub(r' \n [^<]*\n \n', "", text) + t = TYPE_BYTES if as_bytes else TYPE_BITMAP + entries = "".join( + f' \n' + f' ..\\Resources\\Icons\\{n}.png;{t}\n \n' for n in sorted(names)) + text = text.replace("", entries + "") + write(path, text, bom, nl) + + +def update_designer(path, names, as_bytes): + text, bom, nl = read(path) + text = re.sub(r" \n /// \n /// Looks up a localized resource of type [^\n]+\n /// \n" + r" (?:internal|public) static [^\n]+ " + PREFIX + r"\w+ \{\n(?: [^\n]*\n)+ \}\n", "", text) + typ = "byte[]" if as_bytes else "System.Drawing.Bitmap" + # match the generator in use (ResXFileCodeGenerator = internal, PublicResXFileCodeGenerator = public) + vis = "public" if re.search(r"\n public static [\w.\[\]]+ \w+\s*\{\s*\n\s*get", text) else "internal" + kind = "System.Byte[]" if as_bytes else "System.Drawing.Bitmap" + props = "".join( + f" \n /// \n /// Looks up a localized resource of type {kind}.\n /// \n" + f" {vis} static {typ} {n} {{\n get {{\n" + f" object obj = ResourceManager.GetObject(\"{n}\", resourceCulture);\n" + f" return (({typ})(obj));\n }}\n }}\n" for n in sorted(names)) + idx = text.rstrip().rfind("}") # namespace close + idx = text.rfind(" }", 0, idx) # class close + text = text[:idx].rstrip(" ") + props + text[idx:] + write(path, text, bom, nl) + + +def resource_is_bytes(designer, name): + """Type of an existing resource in Resources.Designer.cs (True = byte[], False = Bitmap).""" + text = read(designer)[0] + m = re.search(r"(?:internal|public) static ([\w.\[\]]+) " + re.escape(name) + r"\s*\{", text) + assert m, (designer, name) + return m.group(1) == "byte[]" + + +# ------------------------------------------------------------------ holder mechanism (manifest-resource icon class) +def update_holder(holder_path, names, csproj): + text, bom, nl = read(holder_path) + m = re.search(r'GetManifestResourceStream\("([\w.]+)\.Resources\.\w+\.png"\)', text) + assert m, holder_path + root = m.group(1) + text = re.sub(r"\n[ \t]*// .*?// \n", "\n", text, flags=re.S) + cls = re.search(r"\n([ \t]*)(?:internal|public) static class \w+\s*\n\1\{\n", text) + ind = cls.group(1) + " " + body = [f"{ind}// generated by design/grasshopper-icons/tools/integrate.py (SAM GH icon redesign)", + f"{ind}private static Bitmap LoadIcon(string name)", f"{ind}{{", + f"{ind} using (Stream stream = typeof({re.search(r'static class (\w+)', text).group(1)}).Assembly.GetManifestResourceStream(\"{root}.Resources.Icons.\" + name + \".png\"))", + f"{ind} {{", f"{ind} if (stream == null)", f"{ind} {{", f"{ind} return null;", f"{ind} }}", "", + f"{ind} using (Bitmap bitmap = new Bitmap(stream))", f"{ind} {{", f"{ind} return new Bitmap(bitmap);", + f"{ind} }}", f"{ind} }}", f"{ind}}}", ""] + body += [f"{ind}internal static Bitmap {n} => LoadIcon(\"{n}\");" for n in sorted(names)] + body += [f"{ind}// "] + close = text.rfind("\n" + cls.group(1) + "}") # class close brace + text = text[:close] + "\n\n" + "\n".join(body) + text[close:] + write(holder_path, text, bom, nl) + ctext, cbom, cnl = read(csproj) + line = '' + if line not in ctext: + mm = re.search(r'\n([ \t]*)', ctext) + assert mm, csproj + ctext = ctext[:mm.end()] + "\n" + mm.group(1) + line + ctext[mm.end():] + write(csproj, ctext, cbom, cnl) + + +# ------------------------------------------------------------------ Icon getter +CLASS_DECL = r"^\s*(?:\[[^\]]*\]\s*)*(?:public|internal)[\w\s]*\bclass\s+{}\b" + + +def update_icon(src_path, cls, holder, resource, guid, bytes_expr=None): + text, bom, nl = read(src_path) + # locate the class by its (unique) ComponentGuid: a file may declare the same class name twice + gpos = re.search(r'ComponentGuid[^\n]*"' + re.escape(guid) + '"', text, re.I) + assert gpos, (src_path, cls, guid) + m = None + for mm in re.finditer(CLASS_DECL.format(cls), text[:gpos.start()], re.M): + m = mm + assert m, (src_path, cls) + nxt = re.search(r"^\s*(?:public|internal)[\w\s]*\bclass\s+\w+", text[m.end():], re.M) + end = m.end() + nxt.start() if nxt else len(text) + body = text[m.end():end] + ic = re.search(r"override\s+(?:System\.Drawing\.)?Bitmap\s+Icon\b", body) + assert ic, (src_path, cls, "no Icon override") + seg_start = ic.end() + seg = body[seg_start: seg_start + 300] + # first .X that is not inside a // comment + hit = next((mm for mm in re.finditer(r"\b" + re.escape(holder) + r"\.\w+", seg) + if "//" not in seg[seg.rfind("\n", 0, mm.start()) + 1: mm.start()]), None) + assert hit, (src_path, cls) + new_seg = seg[:hit.start()] + holder + "." + resource + seg[hit.end():] + body = body[:seg_start] + new_seg + body[seg_start + 300:] + text = text[:m.end()] + body + text[end:] + write(src_path, text, bom, nl) + + +def verify(rows): + """Re-parse the C# source: every object must now reference its manifest resource, and the PNG must exist.""" + raw_path = os.path.join(DESIGN, "manifest_raw.json") + original = open(raw_path, "rb").read() # keep the pre-redesign inventory (records the old icons) + try: + subprocess.run([sys.executable, os.path.join(HERE, "inventory.py")], check=True, stdout=subprocess.DEVNULL) + now = {r["guid"] + "|" + r["source"]: r for r in json.load(open(raw_path, encoding="utf-8"))} + finally: + open(raw_path, "wb").write(original) + bad = [] + for r in rows: + cur = now[r["guid"] + "|" + r["source"]]["icon_expr"] + png = os.path.join(REPO, r["project_dir"], "Resources", "Icons", r["resource"] + ".png") + if cur != r["resource"] or not os.path.isfile(png): + bad.append((r["class"], cur, r["resource"])) + assert not bad, bad + assert len(now) == len(rows), (len(now), len(rows)) + print(f"verified: {len(rows)}/{len(rows)} objects reference their SAM_GH_* icon resource") + + +def main(): + rows = json.load(open(os.path.join(DESIGN, "manifest.json"), encoding="utf-8")) + by_proj = {} + for r in rows: + by_proj.setdefault(r["project_dir"], []).append(r) + for pdir_rel, items in sorted(by_proj.items()): + pdir = os.path.join(REPO, pdir_rel) + icon_dir = os.path.join(pdir, "Resources", "Icons") + if os.path.isdir(icon_dir): + shutil.rmtree(icon_dir) + os.makedirs(icon_dir) + names = {r["resource"]: r["icon_id"] for r in items} + for res, iid in names.items(): + shutil.copyfile(os.path.join(PNG, iid + ".png"), os.path.join(icon_dir, res + ".png")) + holders = {r["icon_holder"] for r in items} + assert len(holders) == 1, (pdir_rel, holders) + holder = holders.pop() + if holder == "Resources": + designer = os.path.join(pdir, "Properties", "Resources.Designer.cs") + kinds = {resource_is_bytes(designer, r["current_icon"]) for r in items if not r["current_icon"].startswith(PREFIX)} + if not kinds: # re-run: all getters already point at generated resources + kinds = {resource_is_bytes(designer, items[0]["resource"])} + assert len(kinds) == 1, (pdir_rel, "mixed byte[]/Bitmap icon resources") + as_bytes = kinds.pop() + update_resx(os.path.join(pdir, "Properties", "Resources.resx"), names, as_bytes) + update_designer(designer, names, as_bytes) + mech = "resx/" + ("byte[]" if as_bytes else "Bitmap") + else: + hp = [os.path.join(root, f) for root, _, fs in os.walk(pdir) for f in fs + if f == holder + ".cs" and "obj" not in root.split(os.sep)] + assert len(hp) == 1, (pdir_rel, holder, hp) + csproj = [os.path.join(pdir, f) for f in os.listdir(pdir) if f.endswith(".csproj")][0] + update_holder(hp[0], names, csproj) + mech = "holder/" + holder + for r in items: + update_icon(os.path.join(REPO, r["source"]), r["class"], holder, r["resource"], r["guid"]) + r["status"] = "integrated" + print(f"{r['project']}: {len(items)} objects, {len(names)} icon resources ({mech})") + verify(rows) + json.dump(rows, open(os.path.join(DESIGN, "manifest.json"), "w", encoding="utf-8"), indent=1) + import csv + with open(os.path.join(DESIGN, "manifest.csv"), "w", newline="", encoding="utf-8") as fh: + w = csv.DictWriter(fh, fieldnames=list(rows[0].keys())) + w.writeheader() + w.writerows(rows) + + +if __name__ == "__main__": + main() diff --git a/design/grasshopper-icons/tools/inventory.py b/design/grasshopper-icons/tools/inventory.py new file mode 100644 index 0000000..894ad7e --- /dev/null +++ b/design/grasshopper-icons/tools/inventory.py @@ -0,0 +1,147 @@ +"""Inventory every Grasshopper document object (components + params) in this repository. + +Same parser as SAM/design/grasshopper-icons/tools/inventory.py (SAM#166), generalised to any +repository layout: every .cs file under a project whose .csproj references Grasshopper is scanned. +An object is inventoried when a non-abstract class declares `ComponentGuid` in a file the project compiles +(files excluded by `` are skipped). +Writes ../manifest_raw.json. +""" +import json +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +DESIGN = os.path.dirname(HERE) +REPO = os.path.dirname(os.path.dirname(DESIGN)) +SKIP_DIRS = {"obj", "bin", ".git", "packages", "design", "build"} + +CLASS_RE = re.compile(r"^\s*(?:\[[^\]]*\]\s*)*((?:public|internal|private|protected|abstract|sealed|static|partial|\s)+)class\s+(\w+)(?:<[^>{]*>)?\s*(?::\s*([^{\n]+))?", re.M) +GUID_RE = re.compile(r"ComponentGuid\s*(?:=>|\{\s*get\s*\{\s*return)\s*new\s*(?:Guid\s*)?\(\s*\"([0-9a-fA-F-]+)\"") # new Guid("..") and target-typed new("..") +ICON_RE = re.compile(r"override\s+(?:System\.Drawing\.)?Bitmap\s+Icon\b(.{0,300})", re.S) +ICON_TOKEN_RE = re.compile(r"\b(?:Properties\.)?Resources\.(\w+)|\b(\w+Icon)\.(\w+)") +EXPOSURE_RE = re.compile(r"override\s+GH_Exposure\s+Exposure\s*=>\s*([^;]+);") + + +def long_path(p): + p = os.path.abspath(p) + return "\\\\?\\" + p if os.name == "nt" and not p.startswith("\\\\") else p + + +def base_args(text, pos): + """Split the argument list of the `base(` call starting at/after pos (string- and paren-aware).""" + i = text.index("(", pos) + 1 + args, depth, cur, instr = [], 0, "", False + while i < len(text): + ch = text[i] + if instr: + cur += ch + if ch == "\\": + cur += text[i + 1]; i += 1 + elif ch == '"': + instr = False + elif ch == '"': + instr = True; cur += ch + elif ch in "([{": + depth += 1; cur += ch + elif ch in ")]}": + if depth == 0: + args.append(cur.strip()); return args + depth -= 1; cur += ch + elif ch == "," and depth == 0: + args.append(cur.strip()); cur = "" + else: + cur += ch + i += 1 + return args + + +def lit(a): + a = a.strip() + if re.fullmatch(r'"(?:[^"\\]|\\.)*"', a): + return a[1:-1] + return "{" + a + "}" + + +def compile_removed(pdir): + """Files excluded from compilation by the project's `` globs (SDK-style csproj).""" + import fnmatch + csproj = [f for f in os.listdir(pdir) if f.endswith(".csproj")] + pats = [] + for f in csproj: + txt = open(long_path(os.path.join(pdir, f)), encoding="utf-8-sig", errors="replace").read() + pats += [m.replace("\\", "/") for m in re.findall(r'): not a GH object of this plugin + src = open(long_path(path), encoding="utf-8-sig").read() + if "ComponentGuid" not in src: + continue + # strip // comments to avoid template/commented code + code = re.sub(r"(?m)(^|[\s;{}])//[^\n]*", r"\1", src) + classes = list(CLASS_RE.finditer(code)) + for i, cm in enumerate(classes): + body = code[cm.end(): classes[i + 1].start() if i + 1 < len(classes) else len(code)] + g = GUID_RE.search(body) + if not g or "abstract" in cm.group(1): + continue + ic = ICON_RE.search(body) + tok = ICON_TOKEN_RE.search(ic.group(1)) if ic else None + ex = EXPOSURE_RE.search(body) + ctor = re.search(r"\b" + cm.group(2) + r"\s*\(\s*\)\s*(?=:\s*base\()", body) + b = base_args(body, ctor.end()) if ctor else None + if b and b[-1].startswith("GH_ParamAccess"): + b = b[:-1] + obsolete = bool(re.search(r"\[Obsolete", code[max(0, cm.start() - 200): cm.end()])) or "_Obsolete" in cm.group(2) + items.append({ + "project": proj, + "project_dir": os.path.relpath(pdir, REPO).replace("\\", "/"), + "class": cm.group(2), + "base_type": (cm.group(3) or "").strip(), + "guid": g.group(1).lower(), + "name": lit(b[0]) if b else None, + "nickname": lit(b[1]) if b and len(b) > 1 else None, + "category": lit(b[-2]) if b and len(b) >= 4 else None, + "subcategory": lit(b[-1]) if b and len(b) >= 4 else None, + "icon_expr": (tok.group(1) or tok.group(3)) if tok else None, + "icon_holder": (tok.group(2) if tok and tok.group(2) else ("Resources" if tok else None)), + "icon_via_bytes": bool(ic and ("ToBitmap" in ic.group(1) or "MemoryStream" in ic.group(1))), + "exposure": ex.group(1).strip() if ex else None, + "obsolete": obsolete, + "source": os.path.relpath(path, REPO).replace("\\", "/"), + }) + out = os.path.join(DESIGN, "manifest_raw.json") + json.dump(items, open(out, "w", encoding="utf-8"), indent=1) + print(len(items), "objects ->", out) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/design/grasshopper-icons/tools/render.py b/design/grasshopper-icons/tools/render.py new file mode 100644 index 0000000..8dae71a --- /dev/null +++ b/design/grasshopper-icons/tools/render.py @@ -0,0 +1,132 @@ +"""Rasterise SVGs to exact 24x24 PNG and build review sheets, using headless Edge + PIL. + +Headless Edge is the reference renderer (same engine family as the WebView used to review +the sheets). Output PNGs are 24x24 RGBA with straight alpha. +""" +import html +import os +import subprocess +import tempfile + +from PIL import Image + +EDGE = r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" +PITCH = 32 # sprite cell (icons placed at integer offsets -> no resampling) + + +def _shot(html_path, png_path, w, h, scale=1): + profile = tempfile.mkdtemp(prefix="edge-prof-") # isolated profile: safe to run in parallel + try: + _shot_with(html_path, png_path, w, h, scale, profile) + finally: + import shutil + shutil.rmtree(profile, ignore_errors=True) + + +def _shot_with(html_path, png_path, w, h, scale, profile): + subprocess.run([ + EDGE, "--headless=new", "--disable-gpu", "--hide-scrollbars", f"--user-data-dir={profile}", + f"--force-device-scale-factor={scale}", "--default-background-color=00000000", + "--allow-file-access-from-files", f"--window-size={w},{h}", "--virtual-time-budget=5000", + f"--screenshot={png_path}", "file:///" + html_path.replace("\\", "/"), + ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +def rasterise(svg_paths, out_paths, size=24, workers=8): + """svg_paths[i] -> out_paths[i] at size x size. + + Each icon is rendered alone at the same page position (0,0), so its pixels depend only on its + own SVG. (A shared sprite made antialiasing depend on the icon's position in the sprite.) + """ + from concurrent.futures import ThreadPoolExecutor + + def one(pair): + svg, out = pair + with tempfile.TemporaryDirectory() as tmp: + page = os.path.join(tmp, "one.html") + src = "file:///" + os.path.abspath(svg).replace("\\", "/") + open(page, "w", encoding="utf-8").write( + "" + f"") + shot = os.path.join(tmp, "one.png") + _shot(page, shot, 64, 64) + os.makedirs(os.path.dirname(out), exist_ok=True) + Image.open(shot).convert("RGBA").crop((0, 0, size, size)).save(out, optimize=True) + + with ThreadPoolExecutor(max_workers=workers) as ex: + list(ex.map(one, zip(svg_paths, out_paths))) + + +def rasterise_sprite(svg_paths, out_paths, size=24): + """Legacy sprite rasteriser (position-sensitive antialiasing; kept for reference only).""" + cols = 40 + rows = (len(svg_paths) + cols - 1) // cols + with tempfile.TemporaryDirectory() as tmp: + page = os.path.join(tmp, "sprite.html") + imgs = [] + for i, p in enumerate(svg_paths): + x, y = (i % cols) * PITCH, (i // cols) * PITCH + src = "file:///" + os.path.abspath(p).replace("\\", "/") + imgs.append(f'') + open(page, "w", encoding="utf-8").write( + "" + "".join(imgs) + "") + shot = os.path.join(tmp, "sprite.png") + _shot(page, shot, cols * PITCH, max(rows * PITCH, 64)) + sheet = Image.open(shot).convert("RGBA") + for i, out in enumerate(out_paths): + x, y = (i % cols) * PITCH, (i // cols) * PITCH + os.makedirs(os.path.dirname(out), exist_ok=True) + sheet.crop((x, y, x + size, y + size)).save(out, optimize=True) + + +CSS = """ +:root{--ink:#111;--mut:#424242;--line:#BDBDBD} +*{box-sizing:border-box}body{margin:0;background:#fff;color:var(--ink);font:12px/1.3 'Segoe UI',system-ui,sans-serif;padding:20px 24px;width:%dpx} +h1{font-size:18px;margin:0 0 2px;text-transform:uppercase;letter-spacing:.02em}.bar{height:5px;width:120px;background:linear-gradient(90deg,#D32F2F 33%%,#F6C90E 33%% 66%%,#1565C0 66%%);margin:6px 0 10px} +h2{font-size:13px;margin:16px 0 6px;border-bottom:2px solid #111;padding-bottom:3px;text-transform:uppercase;letter-spacing:.03em} +.sub{color:var(--mut);margin:0 0 8px}.grid{display:grid;grid-template-columns:repeat(%d,1fr);gap:6px} +.c{border:1px solid var(--line);padding:6px;display:flex;gap:8px;align-items:center;min-height:%dpx} +.n{display:flex;flex-direction:column;gap:4px;align-items:center} +.gh{width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:3px} +.l{background:linear-gradient(#e6e6e6,#c9c9c9);border:1px solid #8a8a8a}.d{background:linear-gradient(#4a4a4a,#2e2e2e);border:1px solid #111} +img.px{image-rendering:pixelated}.t{font-size:10.5px;word-break:break-word;line-height:1.25}.t b{font-size:11px}.t i{color:var(--mut);font-style:normal} +""" + + +def sheet(title, subtitle, groups, out_png, cols=4, big=72, width=1400, mini=False): + """groups: [(heading, [(png24, label_html)])]. Writes an HTML next to out_png and screenshots it.""" + base = os.path.dirname(os.path.abspath(out_png)) + parts = [f"

{html.escape(title)}

{subtitle}

"] + n = 0 + for head, items in groups: + parts.append(f"

{html.escape(head)} ({len(items)})

") + for png, label in items: + rel = os.path.relpath(png, base).replace("\\", "/") + if mini: + parts.append(f"
" + f"
" + f"
{label}
") + else: + parts.append(f"
" + f"
" + f"
{label}
") + n += 1 + parts.append("
") + doc = f"{html.escape(title)}{''.join(parts)}" + html_path = os.path.splitext(out_png)[0] + ".html" + open(html_path, "w", encoding="utf-8").write(doc) + rows = sum((len(items) + cols - 1) // cols for _, items in groups) + est = 90 + len(groups) * 40 + rows * (big + 24) + _shot(os.path.abspath(html_path), os.path.abspath(out_png), width, est, scale=1) + _trim(out_png) + return html_path + + +def _trim(png): + im = Image.open(png).convert("RGBA") + bbox = im.getchannel("A").getbbox() + if bbox: + im = im.crop((0, 0, im.width, min(im.height, bbox[3] + 16))) + bg = Image.new("RGBA", im.size, (255, 255, 255, 255)) + bg.alpha_composite(im) + bg.convert("RGB").save(png, optimize=True) diff --git a/design/grasshopper-icons/tools/repo_rules.py b/design/grasshopper-icons/tools/repo_rules.py new file mode 100644 index 0000000..66d4ec4 --- /dev/null +++ b/design/grasshopper-icons/tools/repo_rules.py @@ -0,0 +1,15 @@ +"""Repository-specific classification decisions for SAM_Multitasker (the only non-shared tool file). + +OVERRIDES : component display name -> (object glyph, op, extra) extra: None | "plural" | "library" | note +PARAM_OBJECTS : param type key (GooParam class or typeof(X) name) -> glyph | (glyph, container, plural) +OBJECTS/VERBS : extra noun/verb rules tried before the shared ones (same shapes as SAM's OBJECTS/VERBS) +""" +OVERRIDES = { + "SAMMultitasker.Run": ("tasks", "run", None), + "SAMMultitasker.DefaultScripts": ("script", "get", "library"), + "SAMMultitasker.MultitaskerInput": ("value", "create", "multitasker input"), + "SAMMultitasker.MultitaskerVariable": ("settings", "create", "multitasker variable (varied setting)"), +} +PARAM_OBJECTS = {"MultitaskerInput": "value", "MultitaskerOutput": "result", "MultitaskerVariable": "settings"} +OBJECTS = [] +VERBS = [] diff --git a/design/grasshopper-icons/tools/sam_classify.py b/design/grasshopper-icons/tools/sam_classify.py new file mode 100644 index 0000000..16b2e56 --- /dev/null +++ b/design/grasshopper-icons/tools/sam_classify.py @@ -0,0 +1,422 @@ +"""Classify every inventoried GH object into (object glyph, operation badge, modifiers). + +Input : ../manifest_raw.json (from inventory.py) +Output: ../manifest.json + ../manifest.csv (the source of truth for the icon library) + +Rules are ordered and deterministic; per-component exceptions live in OVERRIDES +(keyed by component display name) with a reason. +""" +import csv +import json +import os +import re + +HERE = os.path.dirname(os.path.abspath(__file__)) +DESIGN = os.path.dirname(HERE) + +# ------------------------------------------------------------------ operation verbs (leading word) +VERBS = [ + # (regex on the short name, op) + (r"^(Create|New)", "create"), (r"^Add", "add"), (r"^(Copy|Duplicate)", "copy"), + (r"^Get", "get"), (r"^(Filter|Select)", "filter"), (r"^Inspect", "inspect"), (r"^Report", "list"), + (r"^Set", "set"), (r"^(Update|Map|Replace|Apply)", "update"), + (r"^(Modify|Fix|Adjust|Rename|Round|Convex|Fill|Resolve|Prepare)", "modify"), + (r"^(Merge|Join|Combine)", "merge"), (r"^(Split|Cut|Trim)", "split"), (r"^Section", "section"), + (r"^Offset", "offset"), (r"^(Transform|Move)", "transform"), (r"^Snap", "snap"), (r"^Align", "align"), + (r"^Extend", "extend"), (r"^Flip", "flip"), (r"^Triangulate", "triangulate"), (r"^Sort", "sort"), + (r"^Remove", "remove"), (r"^Calculate", "calculate"), (r"^(Check|Is[A-Z])", "validate"), + (r"^(Label|Display)", "display"), (r"^(Bake|Export|Save|Write)", "export"), (r"^(Load|Import|Read|From)", "import"), + (r"^To[A-Z]", "convert"), +] + +# ------------------------------------------------------------------ object nouns (first match wins; order matters) +OBJECTS = [ + ("ApertureConstructionLibrary", "apertureConstruction", "library"), ("ConstructionLibrary", "construction", "library"), + ("MaterialLibrary", "material", "library"), ("InternalConditionLibrary", "internalCondition", "library"), + ("DegreeOfActivityLibrary", "degreeOfActivity", "library"), ("ProfileLibrary", "profile", "library"), + ("SystemTypeLibrary", "system", "library"), ("SAMLibrary", "object", "library"), ("DefaultLibrary", "object", "library"), + ("ConstructionManager", "constructionManager", None), ("ApertureConstruction", "apertureConstruction", None), + ("ConstructionLayer", "constructionLayer", None), ("MaterialLayer", "materialLayer", None), + ("OpeningProperties", "openingProperties", None), ("Opening", "aperture", None), + ("AdjacencyCluster", "cluster", None), ("AnalyticalModel", "model", None), ("BuildingModel", "model", None), + ("ExternalSpace", "externalSpace", None), ("PerimeterSpace", "space", None), ("Space", "space", None), ("Room", "space", None), + ("Zone", "zone", None), ("IZAM", "izam", None), ("Aperture", "aperture", None), ("Window", "aperture", None), + ("FeatureShade", "shade", None), ("Shade", "shade", None), ("AirPanel", "panel", None), ("AirPartition", "panel", None), + ("Panel", "panel", None), ("Roof", "panel", None), ("HostPartition", "panel", None), ("Partition", "panel", None), + ("Construction", "construction", None), ("TransparentMaterial", "transparentMaterial", None), + ("GasMaterial", "gasMaterial", None), ("OpaqueMaterial", "material", None), ("Material", "material", None), + ("InternalCondition", "internalCondition", None), ("DegreeOfActivity", "degreeOfActivity", None), + ("OccupancyGain", "internalCondition", None), ("LightingGain", "lighting", None), ("EquipmentGain", "equipment", None), + ("InfiltrationGain", "infiltration", None), ("PollutantGain", "pollutant", None), ("SetPoint", "thermometer", None), + ("VentilationProfile", "airflow", None), ("Ventilation", "fan", None), ("AirFlow", "airflow", None), ("Airflow", "airflow", None), + ("AirHandlingUnit", "ahu", None), ("MechanicalSystem", "system", None), ("SystemType", "system", None), ("System", "system", None), + ("Profile", "profile", None), ("NCM", "internalCondition", None), + ("HeatTransferCoefficient", "heat", None), ("HeatFlow", "heat", None), ("Daylight", "daylight", None), + ("DesignDay", "designDay", None), ("WeatherData", "weatherData", None), ("WeatherYear", "weatherYear", None), + ("WeatherHour", "weatherData", None), ("Weather", "weather", None), ("Comfort", "thermometer", None), ("TM52", "thermometer", None), + ("Adaptive", "thermometer", None), ("OutdoorAirTemperature", "thermometer", None), + ("Level", "level", None), ("Elevation", "level", None), + ("Shell", "shell", None), ("Face3D", "face", None), ("PlanarBoundary", "face", None), ("Mesh", "mesh", None), + ("Plane", "plane", None), ("Transform3D", "transform3D", None), ("Segment", "segment", None), ("Point", "points", None), + ("Polycurve", "polyloop", None), ("Loop", "polyloop", None), ("SAMGeometry2D", "geometry2D", None), ("NTS", "geometry2D", None), + ("Geometry", "geometry", None), ("Vector", "vector", None), ("Normal", "normal", None), ("Grid", "grid", None), + ("Location", "location", None), ("Address", "address", None), ("CountryCode", "location", None), ("CoutryCode", "location", None), + ("Azimuth", "compass", None), ("Direction", "compass", None), + ("Result", "result", None), ("DesignExplorer", "result", None), ("Case", "case", None), ("Scenario", "case", None), + ("DelimitedFileTable", "table", None), ("Csv", "table", None), ("TableModifier", "table", None), + ("TextMap", "textMap", None), ("Json", "json", None), ("File", "file", None), ("Log", "log", None), + ("Color", "colour", None), ("Uint", "colour", None), ("ARGB", "colour", None), + ("HourOfYear", "clock", None), ("DateTime", "clock", None), ("DayOfYear", "calendar", None), ("Period", "clock", None), + ("Number", "number", None), ("String", "text", None), ("Text", "text", None), ("Name", "name", None), + ("Value", "value", None), ("Parameter", "value", None), ("Key", "list", None), ("List", "list", None), + ("DataTree", "tree", None), ("Path", "folder", None), ("Files", "folder", None), + ("RelationCluster", "relation", None), ("Related", "relation", None), ("Type", "type", None), ("Settings", "settings", None), + ("Object", "object", None), ("Guid", "object", None), +] + +PLURAL_RE = r"(Spaces|Panels|Apertures|Zones|Shells|Face3Ds|Materials|Constructions|Profiles|Levels|Objects|Results|Points|Point3Ds|Segment3Ds|Openings|Values|Names|Cases|Scenarios|IZAMs|Systems|Units|Elevations|Layers|Days|Geometries|Keys|Types|Rooms|Hours|Temperatures)" + +# ------------------------------------------------------------------ explicit exceptions: name -> (object, op, extra) +OVERRIDES = { + # --- about / diagnostics + "SAM.About": ("info", "value", "About enum: SAM emblem tile + info (identity exception)"), + "SAM.Version": ("info", "get", "Version: emblem tile + get"), + "SAM.Test": ("object", "validate", "test harness component"), + "SAMCore.Error": ("log", "error", None), + "LogToFile": ("log", "export", None), + "Inspect": ("object", "inspect", None), + "SAMCore.Convert": ("object", "convert", None), + "SAMCore.Update": ("object", "update", None), + "SAMCore.ReplaceByGuid": ("object", "update", None), + "SAMCore.GetType": ("type", "get", None), + "SAMCore.FilterByType": ("type", "filter", None), + "SAMCore.ParameterByType": ("value", "get", None), + "SAMCore.CombineResults": ("result", "merge", None), + "SAMCore.StringReplace": ("text", "update", None), + "SAMCore.TextMapValues": ("textMap", "get", None), + "SAMCore.NumberFilter": ("number", "filter", None), + "SAMCore.Round": ("number", "modify", None), + "SAMCore.TableModifier ": ("table", "create", None), + "SAMCore.CreateDelimitedFileTableBySAMObjects": ("table", "create", None), + "SAMCore.LoadMaterialLibrary": ("material", "import", "library"), + "SAMCore.SaveMaterialLibrary": ("material", "export", "library"), + "SAMCore.AddMaterials": ("material", "add", "library"), + "SAMCore.CopyMaterials": ("material", "copy", "library"), + "GetNames": ("name", "get", None), + "SelectByName": ("name", "filter", None), + "GetValue": ("value", "get", None), "SetValue": ("value", "set", None), "SetValues": ("value", "set", "plural"), + "RemoveValue": ("value", "remove", None), "GetValueFilter": ("value", "filter", None), + "ToList": ("list", "convert", None), "ToJson": ("json", "export", None), "FromJson": ("json", "import", None), + "ToFile": ("file", "export", None), "FromFile": ("file", "import", None), "ToCsv": ("table", "export", None), + "Csv.ToDelimitedFileTable": ("table", "import", None), + "DelimitedFileTable.GetValue": ("table", "get", None), "DelimitedFileTable.SetValue": ("table", "set", None), + "DelimitedFileTable.SetColumnNames": ("table", "set", None), "DelimitedFileTable.Map": ("table", "update", None), + "DelimitedFileTable.Sort": ("table", "sort", None), + "RelationCluster.AddObjects": ("relation", "add", None), "RelationCluster.Objects": ("relation", "get", None), + "RelationCluster.RelatedObjects": ("relation", "get", "plural"), "SAMLibrary.AddObjects": ("object", "add", "library"), + "SAMAnalytical.Objects": ("object", "get", "plural"), "Analytical.RelatedObjects": ("relation", "get", "plural"), + "SAMArchitectural.RelatedObjects": ("relation", "get", "plural"), + "AdjacencyCluster.UpdateObjects": ("cluster", "update", None), + "ARGBToUint": ("colour", "convert", None), "ColorToUint": ("colour", "convert", None), + "UintToARGB": ("colour", "convert", None), "UintToColor": ("colour", "convert", None), + "SAMCore.DateTimeToHourOfYear": ("clock", "convert", None), "SAMCore.DayOfYearToHourOfYear": ("calendar", "convert", None), + "SAMAnalytical.HourOfYearToDateTime": ("clock", "convert", None), + "SAMHydra.ExportFile": ("file", "export", None), + # --- enum value lists (GH_SAMEnumComponent): object of the enum + value badge + "SAMCore.CombineType": ("settings", "value", None), "SAMCore.CoutryCode": ("location", "value", None), + "SAMCore.Direction": ("compass", "value", None), "SAMCore.NumberComparisonType": ("number", "value", None), + "SAMCore.ParameterType": ("value", "value", None), "SAMCore.Period": ("clock", "value", None), + "SAMCore.TextComparisonType": ("text", "value", None), "SAMAnalytical.ApertureType": ("aperture", "value", None), + "SAMAnalytical.DefaultGasType": ("gasMaterial", "value", None), "SAMAnalytical.HeatFlowDirection": ("heat", "value", None), + "SAMAnalytical.InternalConditionParameter": ("internalCondition", "value", None), + "SAMAnalytical.LightingOccupancyControls": ("lighting", "value", None), + "SAMAnalytical.LightingPhotoelectricControls": ("lighting", "value", None), + "SAMAnalytical.NCMSystemType": ("system", "value", None), "SAMAnalytical.PanelGroup": ("panel", "value", None), + "SAMAnalytical.PanelType": ("panel", "value", None), "SAMAnalytical.ProfileGroup": ("profile", "value", None), + "SAMAnalytical.ProfileType": ("profile", "value", None), "SAMAnalytical.TM52BuildingCategory": ("thermometer", "value", None), + "SAMAnalytical.ZoneType": ("zone", "value", None), "SAMAnalytical.HostPartitionCategory": ("panel", "value", None), + "SAMWeather.WeatherDataType": ("weatherData", "value", None), + # --- analytical specials + "SAMAnalytical.AdjacentSpaces": ("cluster", "get", None), "SAMAnalytical.GetAdjacentSpaceNames": ("cluster", "list", None), + "SAMAnalytical.Airflow": ("airflow", "calculate", None), "SAMAnalytical.ApplyTargetedDesignAirFlow": ("airflow", "set", None), + "SAMAnalytical.ResolveTargetedDesignAirFlow": ("airflow", "calculate", None), "SAMAnalytical.SetAirflow": ("airflow", "set", None), + "SAMAnalytical.ApertureConstructions": ("apertureConstruction", "get", "plural"), + "SAMAnalytical.Bake": ("model", "export", None), "SAMAnalytical.Check": ("model", "validate", None), + "SAMAnalytical.CalculateDaylightFactor": ("daylight", "calculate", None), "SAMAnalytical.DaylightFactor": ("daylight", "analyse", None), + "SAMAnalytical.CalculateSpacePanelArea": ("space", "calculate", None), + "SAMAnalytical.CalculateGlazingValueByAperture": ("aperture", "calculate", None), + "SAMAnalytical.CalculateGlazingValueByConstruction": ("apertureConstruction", "calculate", None), + "SAMAnalytical.CreateCaseByShade": ("case", "create", None), "SAMAnalytical.CreateCaseByShade1": ("case", "create", None), + "SAMAnalytical.CreateCaseByVentilation": ("case", "create", None), "SAMAnalytical.CreateCaseByWindowSize": ("case", "create", None), + "SAMAnalytical.CreateOverheatingScenarios": ("case", "create", None), "SAMAnalytical.PreparePartOIteration": ("case", "modify", None), + "SAMAnalytical.CreateDegreeOfActivityByTemperature": ("degreeOfActivity", "create", None), + "SAMAnalytical.CreateIZAM": ("izam", "create", None), "SAMAnalytical.CreateIZAMBySetPoint": ("izam", "create", None), + "SAMAnalytical.CreateIZAMBySpaces": ("izam", "create", None), "SAMAnalytical.RemoveIZAMs": ("izam", "remove", None), + "SAMAnalytical.CreateLevels": ("level", "create", "plural"), "SAMArchitectural.CreateLevel": ("level", "create", None), + "SAMAnalytical.CreateShells": ("shell", "create", "plural"), "SAMAnalytical.CreateShellsByElevations": ("shell", "create", "plural"), + "SAMAnalytical.CreateShellsByElevationsAndAuxiliaryElevations": ("shell", "create", "plural"), + "SAMAnalytical.DailyIndoorComfortTemperatures": ("thermometer", "calculate", None), + "SAMAnalytical.DataTree": ("tree", "convert", None), "SAMAnalytical.DefaultColor": ("colour", "get", None), + "SAMAnalytical.DefaultConstructionManager": ("constructionManager", "get", None), + "SAMAnalytical.ExternalPanels": ("panel", "filter", "plural"), "SAMAnalytical.ExternalVector3D": ("vector", "get", None), + "SAMAnalytical.FillFloorsAndRoofs": ("panel", "add", "plural"), "SAMAnalytical.Filter": ("object", "filter", None), + "SAMAnalytical.Geometry": ("geometry", "convert", None), "SAMAnalytical.GetDefaultLibrary": ("object", "get", "library"), + "SAMAnalytical.GetInternalConditionTextMap": ("textMap", "get", None), "SAMAnalytical.GetSortedKeys": ("list", "sort", None), + "SAMAnalytical.GetValuesByReference": ("value", "get", "plural"), "SAMAnalytical.Grid": ("grid", "create", None), + "SAMAnalytical.HeatTransferCoefficientByDefaultGasType": ("heat", "calculate", None), + "SAMAnalytical.HourlyValues": ("weatherData", "get", "plural"), "SAMAnalytical.InsideSpace": ("space", "validate", None), + "SAMAnalytical.JoinSpacesByPanels": ("space", "merge", "plural"), "SAMAnalytical.LabelPanel": ("panel", "display", None), + "SAMAnalytical.LabelSpace": ("space", "display", None), "SAMAnalytical.MapAdjacencyCluster": ("cluster", "update", None), + "SAMAnalytical.Merge": ("model", "merge", None), "SAMAnalytical.MergeSettings": ("settings", "create", None), + "SAMAnalytical.TypeMergeSettings": ("settings", "create", None), "SAMAnalytical.ModifyObject": ("object", "modify", None), + "SAMAnalytical.NormalsDisplay": ("normal", "display", None), "SAMAnalytical.UpdateNormals": ("normal", "update", None), + "SAMAnalytical.FlipPanel": ("normal", "flip", None), "SAMAnalytical.PanelDistance": ("panel", "calculate", None), + "SAMAnalytical.PanelLocation": ("panel", "get", None), "SAMAnalytical.PanelSpacing": ("panel", "calculate", "plural"), + "SAMAnalytical.PanelTypeByText": ("panel", "convert", None), "SAMAnalytical.PanelsDifference": ("panel", "difference", None), + "SAMAnalytical.PanelsIntersection": ("panel", "intersect", None), "SAMAnalytical.PlaneIntersection": ("plane", "intersect", None), + "SAMAnalytical.Paths": ("folder", "list", None), "SAMAnalytical.Files": ("file", "list", None), + "SAMAnalytical.PerimeterSpaces": ("space", "filter", "plural"), "SAMAnalytical.PlanarBoundary3D": ("face", "get", None), + "SAMAnalytical.ProfileBySpace": ("profile", "get", None), "SAMAnalytical.ProfileDivide": ("profile", "divide", None), + "SAMAnalytical.ProfileMultiply": ("profile", "multiply", None), "SAMAnalytical.ProfileSum": ("profile", "sum", None), + "SAMAnalytical.SeasonProfileBySetPoint": ("profile", "create", None), "SAMAnalytical.ReplaceObject": ("object", "update", None), + "SAMAnalytical.ReportRooms": ("space", "list", "plural"), "SAMAnalytical.ReportRoomsList": ("space", "list", "plural"), + "SAMAnalytical.ReportSpaces": ("space", "list", "plural"), "SAMAnalytical.RoundAzimuth": ("compass", "modify", None), + "SAMAnalytical.Section": ("sectionBox", "section", None), "SAMAnalytical.SetAdiabatic": ("panel", "set", None), + "SAMAnalytical.SetLocation": ("location", "set", None), "SAMAnalytical.SnapPoints": ("points", "snap", None), + "SAMAnalytical.SystemTypes": ("system", "get", "plural"), "SAMAnalytical.TM52SpaceExtendedResultByDaysOfYears": ("thermometer", "analyse", None), + "SAMAnalytical.Transform": ("model", "transform", None), "SAMAnalytical.TriangulateConcavePanels": ("panel", "triangulate", None), + "SAMAnalytical.Convex": ("panel", "modify", None), "SAMAnalytical.RemoveInternalEdges": ("panel", "remove", None), + "SAMAnalytical.UpdateByModel": ("model", "update", None), "SAMAnalytical.UpdateGeometry": ("geometry", "update", None), + "SAMAnalytical.UpdateLibraries": ("object", "update", "library"), "SAMAnalytical.UpdateName": ("name", "update", None), + "SAMAnalytical.UpdateTypesByMap": ("type", "update", None), "SAMAnalytical.UpdateTypesByName": ("type", "update", None), + "SAMAnalytical.FixNames": ("name", "modify", None), "SAMAnalytical.RenameAnalyticalModel": ("model", "modify", None), + "SAMAnalytical.RenameConstruction": ("construction", "modify", None), "SAMAnalytical.UpdateSpaceNames": ("name", "update", "plural"), + "SAMAnalytical.UpdateSpaceLoactionPoint": ("space", "update", None), "SAMAnalytical.AlignSpaceLocation": ("space", "align", None), + "SAMAnalytical.UpdateNCM": ("internalCondition", "update", None), "SAMAnalytical.ModifyZone": ("zone", "modify", None), + "SAMAnalytical.MapPanels": ("panel", "update", "plural"), "SAMAnalytical.MapInternalConditions": ("internalCondition", "update", "plural"), + "SAMAnalytical.OverlapPanels": ("panel", "intersect", None), "SAMAnalytical.AddResults": ("result", "add", None), + "SAMAnalytical.AddMissingMaterials": ("material", "add", "library"), "SAMAnalytical.AddMaterials": ("material", "add", "library"), + "SAMAnalytical.AddMissingProfiles": ("profile", "add", "library"), + "SAMAnalytical.RemoveFromConstructionManager": ("constructionManager", "remove", None), + "SAMAnalytical.CreateHostPartitionType": ("construction", "create", None), + "SAMAnalytical.UpdateHostPartitionType": ("construction", "update", None), + "SAMAnalytical.CreateMaterialLayersByNames": ("materialLayer", "create", "plural"), + "SAMAnalytical.CreateConstructionLayersByMaterials": ("constructionLayer", "create", "plural"), + "SAMAnalytical.CreateConstructionLayersByNames": ("constructionLayer", "create", "plural"), + "SAMAnalytical.GetInternalConstructionLayers": ("constructionLayer", "get", "plural"), + "SAMAnalytical.CopyApertureConstructionLayers": ("apertureConstruction", "copy", None), + "SAMAnalytical.CopyConstructionLayers": ("constructionLayer", "copy", "plural"), + "SAMAdjacencyCluster.DuplicateConstructionByPanelType": ("construction", "copy", None), + "SAMAdjacencyCluster.SetDefaultApertureConstructionLayers": ("apertureConstruction", "set", None), + "SAMAnalytical.SetConstructionLayersByPanelType": ("constructionLayer", "set", "plural"), + "SAMAnalytical.SetDefaultConstructionLayerByPanelType": ("constructionLayer", "set", None), + "SAMAdjacencyCluster.PanelTypeUpdate": ("panel", "update", None), + "SAMAnalytical.UpdateBuildingModel": ("model", "update", None), + "SAMAnalytical.SetOccupancyGains": ("internalCondition", "set", None), + "SAMAnalytical.SetInternalConditionPerArea": ("internalCondition", "set", None), + "SAMAnalytical.GetExternalMaterials": ("material", "get", "plural"), "SAMAnalytical.GetInternalMaterials": ("material", "get", "plural"), + "SAMAnalytical.GetDefaultGasMaterials": ("gasMaterial", "get", "plural"), + "SAMAnalytical.CreatePerimeterZones": ("zone", "create", "plural"), "SAMAnalytical.CreatePerimeterZones_2": ("zone", "create", "plural"), + "SAMAnalytical.OffsetAperturesOnEdge": ("aperture", "offset", "plural"), + "SAMAnalytical.AddOpeningsByAzimuth": ("aperture", "add", "plural"), "SAMAnalytical.AddOpenings": ("aperture", "add", "plural"), + "SAMAnalytical.CreateOpeningsBy3DGeometry": ("aperture", "create", "plural"), + "SAMAnalytical.AddOpeningProperties": ("openingProperties", "add", None), + "SAMAnalytical.AddOpeningPropertiesByPartO": ("openingProperties", "add", None), + "SAMAnalytical.RemoveOpeningProperties": ("openingProperties", "remove", None), + "SAMAnalytical.AddVentilationPropertiesByPartF": ("openingProperties", "add", None), + "SAMAnalytical.AddTransferAirDoorsByPartF": ("aperture", "add", "plural"), + "SAMAnalytical.CreateBuildingModelBy2DGeometries(": ("model", "create", None), + "SAMAnalytical.CreateBuildingModelByShells": ("model", "create", None), + "SAMAnalytical.DesignDays": ("designDay", "create", "plural"), "SAMAnalytical.LoadWeatherData": ("weatherData", "import", None), + "SAMWeather.HourlyValue": ("weatherData", "get", None), "SAMWeather.HourlyValues": ("weatherData", "get", "plural"), + "SAMWeather.ModifyWeatherData": ("weatherData", "modify", None), + "SAMWeather.WeatherHoursByHourOfYear": ("weatherData", "filter", None), + "SAMWeather.WeatherHoursByPercentage": ("weatherData", "filter", None), + "SAMWeather.AdaptiveSetpointACCI": ("thermometer", "calculate", None), + "SAMWeather.AdaptiveSetpointACCIByTemperature": ("thermometer", "calculate", None), + "SAMWeather.PrevailingMeanOutdoorAirTemperatures": ("thermometer", "calculate", "plural"), + "SAMAnalytical.AddMechanicalSystems": ("system", "add", "plural"), + "SAMAnalytical.AddVentilationSystem": ("fan", "add", None), "SAMAnalytical.AddVentilationSystemsByZones": ("fan", "add", "plural"), + "SAMAnalytical.ModifyVentilationSystemsByZone": ("fan", "modify", "plural"), + "SAMAnalytical.SetSystemTypeNames": ("system", "set", None), + "SAMAnalytical.AddSpacesByBrep": ("space", "add", "plural"), "SAMAnalytical.RemoveSpacesByBrep": ("space", "remove", "plural"), + "SAMAnalytical.AddAirPanels": ("panel", "add", "plural"), "SAMAnalytical.AddAirPartitions": ("panel", "add", "plural"), + "SAMAnalytical.AddPanels": ("panel", "add", "plural"), "SAMAnalytical.AddFeatureShade": ("shade", "add", None), + "SAMAnalytical.CreateShade": ("shade", "create", None), "SAMAnalytical.CreateShadeByFeatureShade": ("shade", "create", None), + "SAMAnalytical.CreateFeatureShade": ("shade", "create", None), + "SAMAnalytical.MergeSpacesByAirPanels": ("space", "merge", "plural"), "SAMAnalytical.MergeSpacesByPanels": ("space", "merge", "plural"), + "SAMAnalytical.MergeSpacesByZones": ("space", "merge", "plural"), + "SAMAnalytical.CreateDesignExplorer": ("result", "create", None), + "SAMAnalytical.CalculateFloorArea": ("floor", "calculate", None), + "SAMAnalytical.FilterByMaxRectangle3D": ("panel", "filter", "plural"), "SAMAnalytical.Remove": ("object", "remove", None), + "SAMAnalytical.SnapByLines": ("panel", "snap", "plural"), "SAMAnalytical.SnapByOffset": ("panel", "snap", "plural"), + "SAMAnalytical.SplitByInternalEdges": ("panel", "split", None), + "SAMAnalytical.FilterByElevation": ("panel", "filter", "plural"), "SAMAnalytical.SnapByElevations": ("panel", "snap", "plural"), + "SAMAnalytical.FilterByBoundaryType": ("panel", "filter", "plural"), "SAMAnalytical.FilterByPanelType": ("panel", "filter", "plural"), + "SAMAnalytical.FilterByPanelAreaAndThinnessRatio": ("panel", "filter", "plural"), "SAMAnalytical.SnapByPoints": ("panel", "snap", "plural"), + "SAMAnalytical.CheckPartFCompliance": ("openingProperties", "validate", None), + "SAMAnalytical.SetPartFCommissioningData": ("openingProperties", "set", None), + "SAMAnalytical.CreateCaseByApertureByAzimuths": ("case", "create", None), "SAMAnalytical.CreateCaseByApertureConstruction": ("case", "create", None), + "SAMAnalytical.CreateCaseByOpening": ("case", "create", None), "SAMAnalytical.CreateCaseByWeather": ("case", "create", None), + "SAMAnalytical.FilterByAzimuth": ("panel", "filter", "plural"), "SAMAnalytical.FilterByGeometry": ("object", "filter", None), + "SAMAnalytical.FilterByPoints": ("object", "filter", None), "SAMCore.Samples": ("folder", "list", None), + # --- geometry + "Create.SAMTransform3DOriginToPlane": ("transform3D", "create", None), "Create.SAMTransform3DPlaneToOrigin": ("transform3D", "create", None), + "Create.SAMTransform3DPlaneToPlane": ("transform3D", "create", None), + "Geometry.PolycurveLoop2D": ("polyloop", "create", None), "Geometry.SAMGeometry": ("geometry", "convert", None), + "Geometry.MeshClose": ("mesh", "modify", None), "Geometry.MeshReduce": ("mesh", "modify", None), + "Geometry.ModifyDifference": ("shell", "difference", None), "Geometry.ModifyIntersection": ("shell", "intersect", None), + "Geometry.ModifyUnion": ("shell", "union", "plural"), "Geometry.Triangulate": ("face", "triangulate", None), + "Geometry.SnapByDistance": ("geometry", "snap", None), "Geometry.SnapByPoints": ("geometry", "snap", None), + "Geometry.ShellsSplitFace3Ds": ("face", "split", None), "Geometry.CutShell": ("shell", "split", None), + "NTS.SAMGeometry2D": ("geometry2D", "convert", None), "SAMGeometry.SAMGeometry2D": ("geometry2D", "convert", None), + "SAMGeometry2D.SAMGeometry": ("geometry2D", "convert", None), "SAMGeometry2D.ToNTS": ("geometry2D", "convert", None), + "SAMGeometry.Geometry": ("geometry", "convert", None), "SAMGeometry.Fill": ("face", "modify", None), + "SAMGeometry.FlipNormal": ("normal", "flip", None), "SAMGeometry.IsClosed": ("shell", "validate", None), + "SAMGeometry.ShellContains": ("shell", "validate", None), "SAMGeometry.NakedSegment3Ds": ("segment", "get", "plural"), + "SAMGeometry.ThinnessRatio": ("face", "calculate", None), "SAMGeometry.Face3DSpacing": ("face", "calculate", None), + "SAMGeometry.MergeOverlaps": ("face", "merge", None), "SAMGeometry.PlaneIntersection": ("plane", "intersect", None), + "SAMGeometry.Segment2DIntersection": ("segment", "intersect", None), + "SAMGeometry.SectionByFace3D": ("sectionBox", "section", None), "SAMGeometry.SectionByShell": ("sectionBox", "section", None), + "SAMGeometry.ShellDifference": ("shell", "difference", None), "SAMGeometry.ShellIntersection": ("shell", "intersect", None), + "SAMGeometry.ShellsUnion": ("shell", "union", "plural"), "SAMGeometry.ShellsSplit": ("shell", "split", None), + "SAMGeometry.ShellsSplitByFace3Ds": ("shell", "split", None), "SAMGeometry.Split": ("geometry", "split", None), + "SAMGeometry.SplitCoplanarFace3Ds": ("face", "split", None), "SAMGeometry.Transform": ("geometry", "transform", None), + "SAMGeometry.CreateShellsByOffset": ("shell", "offset", None), +} + +# Param (GH_PersistentParam / GH_Param) type-name -> object glyph +PARAM_OBJECTS = { + "Group": "group", "IndexedObjects": "indexed", "Object": "object", "Result": "result", "System": "system", + "SystemType": "system", "TableModifier": "table", "Address": "address", "AdjacencyCluster": "cluster", + "AirHandlingUnit": "ahu", "AnalyticalModel": "model", "Aperture": "aperture", "ApertureConstruction": "apertureConstruction", + "ApertureConstructionLibrary": "apertureConstruction", "BuildingModel": "model", "Construction": "construction", + "ConstructionLayer": "constructionLayer", "ConstructionLibrary": "construction", "ConstructionManager": "constructionManager", + "DegreeOfActivity": "degreeOfActivity", "DegreeOfActivityLibrary": "degreeOfActivity", "DelimitedFileTable": "table", + "DesignDay": "designDay", "ExternalSpace": "externalSpace", "FeatureShade": "shade", "FloorType": "construction", + "HostPartitionType": "construction", "IAnalyticalEquipment": "equipment", "IAnalyticalObject": "object", + "IOpening": "aperture", "IPartition": "panel", "IRelationCluster": "relation", "ISAMGeometry": "geometry", + "ISection": "sectionBox", "IWeatherObject": "weather", "InternalCondition": "internalCondition", + "InternalConditionLibrary": "internalCondition", "Level": "level", "Location": "location", "Log": "log", + "Material": "material", "MaterialLayer": "materialLayer", "MaterialLibrary": "material", "Matrix": "matrix", + "MergeSettings": "settings", "OpeningProperties": "openingProperties", "OpeningType": "apertureConstruction", + "Panel": "panel", "PlanarBoundary3D": "face", "Profile": "profile", "ProfileLibrary": "profile", "RoofType": "construction", + "SAMObject": "object", "Space": "space", "SystemTypeLibrary": "system", "T": "object", "TextMap": "textMap", + "Transform3D": "transform3D", "TypeMergeSettings": "settings", "WallType": "construction", "WeatherData": "weatherData", + "WeatherYear": "weatherYear", "Text3d": "text", +} +PARAM_LIBRARY = {"ApertureConstructionLibrary", "ConstructionLibrary", "DegreeOfActivityLibrary", "InternalConditionLibrary", + "MaterialLibrary", "ProfileLibrary", "SystemTypeLibrary"} + +# Batches (review sheets) by object family +BATCHES = [ + ("01 Model, cluster & cases", {"model", "cluster", "case", "sectionBox", "level"}), + ("02 Spaces & zones", {"space", "floor", "externalSpace", "zone", "izam"}), + ("03 Panels, apertures & shading", {"panel", "aperture", "openingProperties", "shade", "normal"}), + ("04 Constructions & materials", {"construction", "constructionLayer", "materialLayer", "apertureConstruction", "constructionManager", + "material", "transparentMaterial", "gasMaterial"}), + ("05 Profiles, internal conditions & gains", {"profile", "internalCondition", "degreeOfActivity", "lighting", "equipment", + "infiltration", "pollutant", "thermometer"}), + ("06 Systems & airflow", {"system", "ahu", "fan", "airflow", "heat", "daylight"}), + ("07 Geometry", {"shell", "face", "geometry", "geometry2D", "plane", "transform3D", "mesh", "polyloop", "segment", "points", "vector", "grid"}), + ("08 Weather, location & time", {"weather", "weatherData", "weatherYear", "designDay", "location", "address", "compass", "clock", "calendar"}), + ("09 Data, IO & utilities", {"object", "relation", "group", "indexed", "type", "value", "name", "text", "number", "list", "tree", + "table", "textMap", "json", "file", "folder", "log", "colour", "settings", "matrix", "result", "info"}), +] + + +def short(name): + return re.sub(r"^(SAM(Analytical|Geometry|Core|Weather|Architectural|Hydra|AdjacencyCluster)?|Geometry)\.", "", name) + + +def classify(x): + name = x["name"] or "" + if x["category"] == "Params" or x["base_type"].startswith(("GH_PersistentParam", "GH_Param")): + t = re.sub(r"^\{typeof\((?:global::[\w.]+\.)?(\w+)\)\.Name\}$", r"\1", name) + t = t.split(".")[-1] + obj = PARAM_OBJECTS[t] + return dict(kind="param", object=obj, op=None, plural=False, + container="library" if t in PARAM_LIBRARY else None, rule="param") + if name in OVERRIDES: + obj, op, extra = OVERRIDES[name] + return dict(kind="component", object=obj, op=op, plural=extra == "plural", + container="library" if extra == "library" else None, rule="override", + note=extra if extra not in (None, "plural", "library") else None) + s = short(name) + op = next((o for rx, o in VERBS if re.match(rx, s)), None) + body = re.split(r"By[A-Z]", s, 1)[0] + obj, container = None, None + for key, o, c in OBJECTS: + if key in body: + obj, container = o, c + break + if obj is None: + for key, o, c in OBJECTS: + if key in s: + obj, container = o, c + break + if x["base_type"].startswith("GH_SAMEnumComponent") and op is None: + op = "value" + plural = bool(re.search(PLURAL_RE + r"(By|$|[A-Z])", body)) and container is None + return dict(kind="component", object=obj, op=op, plural=plural, container=container, rule="rule") + + +def normalise(c): + """Grammar rules applied after classification (see ICON_DESIGN_SYSTEM.md).""" + if c["op"] == "add": + c["plural"] = False # ADD always draws existing (neutral) + new (green); plurality is not drawn + return c + + +def icon_id(c): + parts = [c["object"]] + if c["container"]: + parts.append(c["container"]) + if c["plural"]: + parts.append("plural") + if c["op"]: + parts.append(c["op"]) + return "_".join(parts) + + +def resource_name(iid): + return "SAM_GH_" + "".join(p[:1].upper() + p[1:] for p in iid.split("_")) + + +def main(): + raw = json.load(open(os.path.join(DESIGN, "manifest_raw.json"), encoding="utf-8")) + out, problems = [], [] + for x in raw: + c = normalise(classify(x)) + if not c["object"] or (c["kind"] == "component" and not c["op"]): + problems.append((x["name"], c)) + continue + iid = icon_id(c) + batch = next((b for b, objs in BATCHES if c["object"] in objs), "10 Other") + if c["kind"] == "param": + batch = "11 Parameters" + out.append({ + "class": x["class"], "name": x["name"], "nickname": x["nickname"], "category": x["category"], + "subcategory": x["subcategory"], "project": x["project"], "source": x["source"], "guid": x["guid"], + "kind": c["kind"], "exposure": x["exposure"], "obsolete": x["obsolete"], + "current_icon": x["icon_expr"] or "(inherited from A_SAMAnalyticalBase)", + "object": c["object"], "op": c["op"], "plural": c["plural"], "container": c["container"], + "rule": c["rule"], "note": c.get("note"), "icon_id": iid, "resource": resource_name(iid), + "icon_file": f"svg/{iid}.svg", "batch": batch, "status": "pending", + }) + if problems: + for n, c in problems: + print("UNCLASSIFIED", n, c) + out.sort(key=lambda r: (r["batch"], r["object"], r["op"] or "", r["name"] or "")) + json.dump(out, open(os.path.join(DESIGN, "manifest.json"), "w", encoding="utf-8"), indent=1) + cols = ["batch", "class", "name", "nickname", "category", "subcategory", "project", "source", "guid", "kind", "exposure", + "obsolete", "current_icon", "object", "op", "plural", "container", "rule", "note", "icon_id", "resource", "icon_file", "status"] + with open(os.path.join(DESIGN, "manifest.csv"), "w", newline="", encoding="utf-8") as fh: + w = csv.DictWriter(fh, fieldnames=cols) + w.writeheader() + w.writerows(out) + print(len(out), "classified,", len(problems), "unclassified,", len({r['icon_id'] for r in out}), "distinct icons") + + +if __name__ == "__main__": + main() From 191db406ac4a332ae2ac9d382b03fd2e03bf8302 Mon Sep 17 00:00:00 2001 From: SAM-BIM-AI Date: Mon, 28 Sep 2026 18:50:23 +0200 Subject: [PATCH 2/6] docs(gh-icons): SAM_Multitasker icon redesign PR record Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 36b483a734872bc53765de50db8f5be927b1f19c) --- docs/GH-IconRedesign.md | 58 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 docs/GH-IconRedesign.md diff --git a/docs/GH-IconRedesign.md b/docs/GH-IconRedesign.md new file mode 100644 index 0000000..355b12f --- /dev/null +++ b/docs/GH-IconRedesign.md @@ -0,0 +1,58 @@ +# SAM Grasshopper icon redesign — SAM_Multitasker PR record + +Branch `feature/sam-gh-icon-redesign`, based on `sow/2026-Q3` @ `5533796`. PR: (to be opened). +Propagates the SAM icon design system from SAM-BIM/SAM#166 (head `cf4d924a`, open, not merged) to this repository. + +## Current status +All **9** Grasshopper objects in this repo (5 components + 4 params) use redesigned icons: **9 / 9**. +Built and validated; ready for review. **Not merged.** + +## Work completed +- `design/grasshopper-icons/`: the shared SAM-BIM icon kit. `icons.py`, `render.py`, `sam_classify.py` and `ICON_DESIGN_SYSTEM.md` are vendored **verbatim** from SAM#166 (hash-checked). `icons_ext.py` and `ICON_DESIGN_SYSTEM_EXT.md` are the frozen SAM-BIM extension v1 (identical in every SAM-BIM repo). `tools/repo_rules.py` holds this repo's explicit decisions. +- **Inventory**: `tools/inventory.py` parses C# source (every non-abstract class declaring `ComponentGuid`). +- **Manifest** (source of truth): `manifest.json` / `manifest.csv` — per object: GUID, class, source, project, object glyph, operation, modifiers, icon id, resource, glyph/badge origin. +- **Generation**: 9 canonical SVGs → 24×24 PNGs; review sheet `review/contact_sheet.png` (native 24 px on GH normal / orange-warning / dark bodies + 3×) and `review/REVIEW.md`. +- **Integration**: each project's existing mechanism; only the icon token inside each `Icon` getter changes. + +| Project | Objects | Icon resources | Mechanism | +|---|---|---|---| +| `SAM.Core.Grasshopper.Multitasker` | 9 | 9 | resx / byte[] | + +## Design reuse +- **Reused SAM object families (3)**: `result`, `settings`, `value` +- **New SAM-BIM ext v1 families used (2)**: `script`, `tasks` +- **Verbs**: `create`, `get`, `run`, `value`; new ext verb: `run` +- Distinct icons: **9** (5 on SAM glyphs, 4 on ext glyphs). Icon ids shared with SAM render pixel-identically to SAM's. + +## Decisions and assumptions +- Grammar, palette, badge families and construction rules are unchanged (SAM#166). No text, no new colours. +- Qualifier variants (`…By`) share an icon intentionally (see `review/REVIEW.md`). +- Interop direction: external → SAM = import ↓, SAM → external = export ↑. +- Multitasker uses ext `tasks` (+ ext verb `run`); scripts use ext `script`; Input/Output/Variable reuse SAM `value`/`result`/`settings`. +- This project stores icons as `byte[]` resources (like SAM.Analytical.Grasshopper); the generated resources match. +- Both `Component/SAMCoreReadExcel.cs` files are excluded from compilation (``); they hold stale copies of SAM core's `GetNames` (same GUID), so they are not inventoried and not changed. `SAM.Analytical.Grasshopper.Multitasker` therefore has no GH objects. +- Legacy icon resources are kept (still referenced by context menus / AssemblyInfo); no GUID, name, nickname, category, subcategory, parameter or behaviour change. + +## Files changed +- New: `design/grasshopper-icons/**`, `/Resources/Icons/SAM_GH_*.png`, `docs/GH-IconRedesign.md`. +- Modified: 9 component/param `.cs` files (one icon token each), 1× `Resources.resx`, 1× `Resources.Designer.cs`. No csproj change. + +## Validation +| Check | Result | +|---|---| +| `tools/classify.py` | 9 classified, 0 unclassified | +| `tools/build.py` identical-pixel collision check | 0 groups (9 distinct icons; 0 intentionally shared icon(s) for qualifier variants, listed in `review/REVIEW.md`) | +| Icon ids shared with SAM#166 vs SAM's `png/24` | 3 shared, 3 byte-identical | +| `tools/integrate.py` re-parse | 9/9 objects reference their `SAM_GH_*` resource; every PNG exists | +| `tools/check_source.py` vs `origin/sow/2026-Q3` | vendored files OK; icon-token swaps: 9, non-icon changes: 0; base 12, now 12 -> UNCHANGED | +| `dotnet build SAM_Multitasker.sln -c Debug` | Build succeeded, 0 errors | +| `tools/check_assemblies.py` | every assembly embeds every required 24×24 icon → OK | +| `tests/GhIconTest` (real Rhino 8 / Grasshopper, Rhino.Testing) | 9/9 objects verified: 9 load in real Rhino 8 / Grasshopper by GUID (name/category match); icon = manifest PNG (max diff 1 level, premultiplied-alpha rounding) | +| Repository test projects | none in this repository | +| Visual review (`review/contact_sheet.png`, 24 px on normal / warning / dark bodies) | all icons legible; no collisions | + +## Unresolved issues / risks +- Built against sibling repos as checked out locally (SAM on `feature/sam-gh-icon-redesign` = SAM#166); icon changes are API-neutral. + +## Recommended next step +Review this PR (compare `review/contact_sheet.png`), then merge by the maintainer. After merge, add the `PROJECT_PROGRESS.md` closeout entry on `sow/2026-Q3` with the merge SHA. SAM#166 (the reference design system) remains open. From fc8988a7df43beb73464c3798a190d1f568fdd1d Mon Sep 17 00:00:00 2001 From: SAM-BIM-AI Date: Mon, 28 Sep 2026 18:50:28 +0200 Subject: [PATCH 3/6] docs(gh-icons): record PR number Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 44e2f5600d3c1cee08cfc80fb3334dee38c4afed) --- docs/GH-IconRedesign.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/GH-IconRedesign.md b/docs/GH-IconRedesign.md index 355b12f..9997d8c 100644 --- a/docs/GH-IconRedesign.md +++ b/docs/GH-IconRedesign.md @@ -1,6 +1,6 @@ # SAM Grasshopper icon redesign — SAM_Multitasker PR record -Branch `feature/sam-gh-icon-redesign`, based on `sow/2026-Q3` @ `5533796`. PR: (to be opened). +Branch `feature/sam-gh-icon-redesign`, based on `sow/2026-Q3` @ `5533796`. PR: SAM-BIM/SAM_Multitasker#8. Propagates the SAM icon design system from SAM-BIM/SAM#166 (head `cf4d924a`, open, not merged) to this repository. ## Current status From 487acf3939177fed318ae2ceacc24aa92c91afa2 Mon Sep 17 00:00:00 2001 From: SAM-BIM-AI Date: Mon, 28 Sep 2026 20:04:18 +0200 Subject: [PATCH 4/6] chore(spdx): add the repository SPDX/copyright header to .cs files changed by the icon PR The spdx-check workflow requires the LGPL-3.0-or-later SPDX line and the copyright line in every changed .cs file. Adds the standard header to the 10 pre-existing files touched by the icon-token swaps (and to the kit's IconTests.cs). No other change. check_source.py now accepts exactly this header as the only non-icon addition and diffs against the merge base. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 670b41b17b1ff93fe3dd755e2e14973c65041605) --- .../Classes/GooMultitaskerInput.cs | 5 +++- .../Classes/GooMultitaskerOutput.cs | 5 +++- .../Classes/GooMultitaskerVariable.cs | 5 +++- .../Classes/GooScript.cs | 5 +++- .../Component/SAMMultitaskerDefaultScripts.cs | 5 +++- .../SAMMultitaskerMultitaskerInput.cs | 5 +++- .../SAMMultitaskerMultitaskerMode.cs | 5 +++- .../SAMMultitaskerMultitaskerVariable.cs | 5 +++- .../Component/SAMMultitaskerRun.cs | 5 +++- .../Properties/Resources.Designer.cs | 5 +++- .../tests/GhIconTest/IconTests.cs | 3 +++ .../grasshopper-icons/tools/check_source.py | 26 +++++++++++++++---- docs/GH-IconRedesign.md | 3 +++ 13 files changed, 67 insertions(+), 15 deletions(-) diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerInput.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerInput.cs index 2cfdf87..3a557c8 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerInput.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerInput.cs @@ -1,4 +1,7 @@ -using Grasshopper.Kernel; +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors + +using Grasshopper.Kernel; using Grasshopper.Kernel.Types; using SAM.Core.Grasshopper.Multitasker.Properties; using SAM.Core.Multitasker; diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerOutput.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerOutput.cs index 5c951e4..ab28649 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerOutput.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerOutput.cs @@ -1,4 +1,7 @@ -using Grasshopper.Kernel; +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors + +using Grasshopper.Kernel; using Grasshopper.Kernel.Types; using SAM.Core.Grasshopper.Multitasker.Properties; using SAM.Core.Multitasker; diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerVariable.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerVariable.cs index bf67699..e37aa0a 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerVariable.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerVariable.cs @@ -1,4 +1,7 @@ -using Grasshopper.Kernel; +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors + +using Grasshopper.Kernel; using Grasshopper.Kernel.Types; using SAM.Core.Grasshopper.Multitasker.Properties; using SAM.Core.Multitasker; diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooScript.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooScript.cs index c69fa9e..1bd4da0 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooScript.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooScript.cs @@ -1,4 +1,7 @@ -using Grasshopper.Kernel; +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors + +using Grasshopper.Kernel; using Grasshopper.Kernel.Types; using SAM.Core.Grasshopper.Multitasker.Properties; using SAM.Core.Multitasker; diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerDefaultScripts.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerDefaultScripts.cs index 036487c..55df7a6 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerDefaultScripts.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerDefaultScripts.cs @@ -1,4 +1,7 @@ -using Grasshopper.Kernel; +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors + +using Grasshopper.Kernel; using Grasshopper.Kernel.Parameters; using SAM.Core.Grasshopper.Multitasker.Properties; using SAM.Core.Multitasker; diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerInput.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerInput.cs index c923ba5..c45fe1d 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerInput.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerInput.cs @@ -1,4 +1,7 @@ -using Grasshopper.Kernel; +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors + +using Grasshopper.Kernel; using Grasshopper.Kernel.Parameters; using Grasshopper.Kernel.Types; using SAM.Core.Grasshopper.Multitasker.Properties; diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerMode.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerMode.cs index 8fe2b45..fffde74 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerMode.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerMode.cs @@ -1,4 +1,7 @@ -using SAM.Core.Grasshopper.Multitasker.Properties; +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors + +using SAM.Core.Grasshopper.Multitasker.Properties; using SAM.Core.Multitasker; using System; using System.Drawing; diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerVariable.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerVariable.cs index 8a0f845..0022c74 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerVariable.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerMultitaskerVariable.cs @@ -1,4 +1,7 @@ -using Grasshopper.Kernel; +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors + +using Grasshopper.Kernel; using Grasshopper.Kernel.Parameters; using Grasshopper.Kernel.Types; using SAM.Core.Grasshopper.Multitasker.Properties; diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerRun.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerRun.cs index 7cce162..5121f07 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerRun.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Component/SAMMultitaskerRun.cs @@ -1,4 +1,7 @@ -using Grasshopper.Kernel; +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors + +using Grasshopper.Kernel; using Grasshopper.Kernel.Parameters; using SAM.Core.Grasshopper.Multitasker.Properties; using SAM.Core.Multitasker; diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Properties/Resources.Designer.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Properties/Resources.Designer.cs index fca7708..c8dab6a 100644 --- a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Properties/Resources.Designer.cs +++ b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Properties/Resources.Designer.cs @@ -1,4 +1,7 @@ -//------------------------------------------------------------------------------ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors + +//------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 diff --git a/design/grasshopper-icons/tests/GhIconTest/IconTests.cs b/design/grasshopper-icons/tests/GhIconTest/IconTests.cs index 4a54218..31233ac 100644 --- a/design/grasshopper-icons/tests/GhIconTest/IconTests.cs +++ b/design/grasshopper-icons/tests/GhIconTest/IconTests.cs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors + using System; using System.Collections.Generic; using System.Drawing; diff --git a/design/grasshopper-icons/tools/check_source.py b/design/grasshopper-icons/tools/check_source.py index a10345a..7a2fdb4 100644 --- a/design/grasshopper-icons/tools/check_source.py +++ b/design/grasshopper-icons/tools/check_source.py @@ -1,10 +1,11 @@ """Guard rails for the icon redesign diff. - python check_source.py [base] (default base: origin/sow/2026-Q3) + python check_source.py [base] (default: merge-base of origin/sow/2026-Q3 and HEAD, so upstream moves are ignored) 1. Vendored design-system files are byte-identical to SAM-BIM/SAM#166 @ cf4d924a (SHA-256). 2. Every changed line in a component .cs file is an icon-token swap inside an `Icon` getter (`.OLD` -> `.SAM_GH_*`); generated Resources.Designer.cs / holder blocks are excluded. + The only other allowed change is the repository SPDX policy header (2 lines + blank) added at the top of a file. 3. The set of ComponentGuid values, names, nicknames, categories and exposures is unchanged vs base. """ import hashlib @@ -22,6 +23,8 @@ "sam_classify.py": "317064816c964e2268e9406aee9ca5f7d82c2bdb34da9c3225a344ca2464b864", # SAM classify.py } TOKEN = re.compile(r"\b(?:\w+\.)+\w+") # any dotted chain, e.g. Properties.Resources.SAM_Small3 +SPDX_HEADER = ("// SPDX-License-Identifier: LGPL-3.0-or-later", + "// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors") KEEP_RE = re.compile(r'ComponentGuid|base\(\s*"|GH_Exposure|Category|NickName|new Guid\(|new\("') @@ -30,13 +33,14 @@ def git(*args): def main(base): + base = git("merge-base", base, "HEAD").strip() ok = True for f, h in VENDORED.items(): got = hashlib.sha256(open(os.path.join(HERE, f), "rb").read().replace(b"\r\n", b"\n")).hexdigest() # autocrlf-safe print(f"vendored {f}: {'OK' if got == h else 'MODIFIED'}") ok &= got == h files = [f for f in git("diff", "--name-only", base, "--", "*.cs").split("\n") if f and not f.startswith("design/")] - swaps, bad = 0, [] + swaps, bad, headers = 0, [], 0 for f in files: if f.endswith("Resources.Designer.cs") or f.startswith("design/"): continue @@ -54,8 +58,19 @@ def main(base): if outside: bad.append((f, outside[:4])) continue - minus = re.findall(r"^-(?!--)(.*)$", diff, re.M) - plus = re.findall(r"^\+(?!\+\+)(.*)$", diff, re.M) + minus = [x.lstrip("") for x in re.findall(r"^-(?!--)(.*)$", diff, re.M)] + plus = [x.lstrip("") for x in re.findall(r"^\+(?!\+\+)(.*)$", diff, re.M)] + top = open(os.path.join(REPO, f), encoding="utf-8-sig").read().replace("\r\n", "\n").split("\n")[:3] + if ([t.strip() for t in top] == [*SPDX_HEADER, ""] and all(any(p.strip() == h for p in plus) for h in SPDX_HEADER) + and all(h not in "\n".join(minus) for h in SPDX_HEADER)): + # repository SPDX policy header added at the top of the file (the only allowed non-icon addition) + for line in (*SPDX_HEADER, ""): + plus.remove(next(p for p in plus if p.strip() == line)) + headers += 1 + for m in list(minus): # a line displaced by the header insertion (e.g. the BOM moving) is unchanged + if m in plus: + minus.remove(m) + plus.remove(m) if len(minus) != len(plus): bad.append((f, "added/removed lines")) continue @@ -70,7 +85,8 @@ def in_icon_getter(line): # the line sits inside an `Icon` getter (single- or m bad.append((f, a.strip(), b.strip())) else: swaps += 1 - print(f"component .cs files changed: {len(files)}; icon-token swaps: {swaps}; non-icon changes: {len(bad)}") + print(f"component .cs files changed: {len(files)}; icon-token swaps: {swaps}; SPDX headers added: {headers}; " + f"non-icon changes: {len(bad)}") for b in bad[:20]: print(" NON-ICON CHANGE:", b) ok &= not bad diff --git a/docs/GH-IconRedesign.md b/docs/GH-IconRedesign.md index 9997d8c..895f86f 100644 --- a/docs/GH-IconRedesign.md +++ b/docs/GH-IconRedesign.md @@ -56,3 +56,6 @@ Built and validated; ready for review. **Not merged.** ## Recommended next step Review this PR (compare `review/contact_sheet.png`), then merge by the maintainer. After merge, add the `PROJECT_PROGRESS.md` closeout entry on `sow/2026-Q3` with the merge SHA. SAM#166 (the reference design system) remains open. + +## SPDX header policy (CI `spdx-check`) +The repository SPDX check requires the LGPL-3.0-or-later SPDX line and the copyright line in every `.cs` file a PR changes. The icon-token swaps touched 10 older files that predated the policy (components, `Resources.Designer.cs`), and the kit test `IconTests.cs` had no header. The standard 2-line header was added to them; nothing else changed. `tools/check_source.py` accepts exactly this header as the only non-icon addition and compares against the merge base. From 761b0a4340c86e74573eeb26613fb6f8cc1eb98e Mon Sep 17 00:00:00 2001 From: Michal Dengusiak Date: Tue, 6 Oct 2026 20:14:31 +0200 Subject: [PATCH 5/6] docs(gh-icons): record the Q4 migration PR Point the PR record at the replacement Q4 PR and base, keep the old Q3 PR referenced for provenance, and note the Q4 re-validation. Documentation only. Co-Authored-By: Claude Sonnet 5.5 --- docs/GH-IconRedesign.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/GH-IconRedesign.md b/docs/GH-IconRedesign.md index 895f86f..bc26c45 100644 --- a/docs/GH-IconRedesign.md +++ b/docs/GH-IconRedesign.md @@ -1,6 +1,6 @@ # SAM Grasshopper icon redesign — SAM_Multitasker PR record -Branch `feature/sam-gh-icon-redesign`, based on `sow/2026-Q3` @ `5533796`. PR: SAM-BIM/SAM_Multitasker#8. +Branch `feature/sam-gh-icon-redesign-q4`, based on `sow/2026-Q4` @ `0a4b781c`. PR: SAM-BIM/SAM_Multitasker#10. Q4 migration of SAM-BIM/SAM_Multitasker#8 (`feature/sam-gh-icon-redesign` @ `670b41b`, based on `sow/2026-Q3` @ `5533796`, kept open for provenance): the same commits replayed onto `sow/2026-Q4`; Q3 history was not imported. Propagates the SAM icon design system from SAM-BIM/SAM#166 (head `cf4d924a`, open, not merged) to this repository. ## Current status @@ -55,7 +55,10 @@ Built and validated; ready for review. **Not merged.** - Built against sibling repos as checked out locally (SAM on `feature/sam-gh-icon-redesign` = SAM#166); icon changes are API-neutral. ## Recommended next step -Review this PR (compare `review/contact_sheet.png`), then merge by the maintainer. After merge, add the `PROJECT_PROGRESS.md` closeout entry on `sow/2026-Q3` with the merge SHA. SAM#166 (the reference design system) remains open. +Review this PR (compare `review/contact_sheet.png`), then merge by the maintainer. After merge, add the `PROJECT_PROGRESS.md` closeout entry on `sow/2026-Q4` with the merge SHA. SAM#166 (the reference design system) remains open. ## SPDX header policy (CI `spdx-check`) The repository SPDX check requires the LGPL-3.0-or-later SPDX line and the copyright line in every `.cs` file a PR changes. The icon-token swaps touched 10 older files that predated the policy (components, `Resources.Designer.cs`), and the kit test `IconTests.cs` had no header. The standard 2-line header was added to them; nothing else changed. `tools/check_source.py` accepts exactly this header as the only non-icon addition and compares against the merge base. + +## Q4 migration validation +Re-validated on `sow/2026-Q4` @ `0a4b781c`: `design/grasshopper-icons/tools/check_source.py origin/sow/2026-Q4` OK (icon-token swaps: 9, SPDX headers added: 9, non-icon changes: 0, ComponentGuid declarations unchanged); `dotnet build SAM_Multitasker.sln -c Debug` succeeded with 0 errors; `check_assemblies.py build` OK; tests: no test project in this repository. The Q4 feature diff (before this record commit) has the same patch-id, file set and blobs as the Q3 PR's feature diff. From 3b24de358e06d733d3756684c56f95f8bdbc801e Mon Sep 17 00:00:00 2001 From: Michal Dengusiak Date: Sun, 11 Oct 2026 12:03:39 +0100 Subject: [PATCH 6/6] Complete icon migration and record current validation --- .../Icon-Migration-Validation-2026-10-11.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 documentation/Icon-Migration-Validation-2026-10-11.md diff --git a/documentation/Icon-Migration-Validation-2026-10-11.md b/documentation/Icon-Migration-Validation-2026-10-11.md new file mode 100644 index 0000000..9912f2d --- /dev/null +++ b/documentation/Icon-Migration-Validation-2026-10-11.md @@ -0,0 +1,19 @@ +# Icon migration validation — 2026-10-11 + +Base: `sow/2026-Q4`. Scope: SAM-BIM only. Reference: [SAM#179](https://github.com/SAM-BIM/SAM/pull/179); [SAM#166](https://github.com/SAM-BIM/SAM/pull/166) is the closed historical design source. + +## Changes and decisions + +- Grasshopper manifest coverage: 9 concrete objects. GUIDs, APIs, namespaces, component metadata and behavior are preserved. +- Reuse the frozen SAM glyph/palette/badge grammar and frozen SAM-BIM extension v1. Preserve plugin identity, external integration logos, severity symbols and historical attribution. +- Source comparison against current base: 9 icon resource token swaps; 0 other production C# changes, apart from standard SPDX headers and additive resource properties. +- Validated 9 manifest resource references; 0 errors. + +## Fresh validation + +- VS MSBuild Restore,Build, Release: **PASS**. Scratch APPDATA/USERPROFILE; current sibling SAM-BIM dependencies, OCCT native build skipped. +- Regression suites: No completed regression test result recorded yet; no pass claimed. +- Re-parsed compiled component declarations: 9/9 manifest rows; 0 coverage/canonical 24x24 RGBA PNG errors. +- Built assembly resource presence and exact embedded PNG bytes checked. + +Next step: verify final-head GitHub checks and merge only if green and safe under the owner authorization. No force-push or branch deletion. Hosted Rhino smoke is tracked separately; prior historical results are not fresh runs.