diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerInput.cs b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Classes/GooMultitaskerInput.cs index 27f214e..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; @@ -73,7 +76,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..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; @@ -41,7 +44,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..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; @@ -73,7 +76,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..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; @@ -68,7 +71,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..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; @@ -24,7 +27,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..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; @@ -25,7 +28,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..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; @@ -21,7 +24,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..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; @@ -25,7 +28,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..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; @@ -28,7 +31,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..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 @@ -79,5 +82,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 0000000..5ecc83b Binary files /dev/null and b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_Result.png differ 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 0000000..0234a72 Binary files /dev/null and b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_Script.png differ diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_ScriptLibraryGet.png b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_ScriptLibraryGet.png new file mode 100644 index 0000000..1b8a7d9 Binary files /dev/null and b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_ScriptLibraryGet.png differ diff --git a/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_Settings.png b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_Settings.png new file mode 100644 index 0000000..45843a9 Binary files /dev/null and b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_Settings.png differ 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 0000000..1cab885 Binary files /dev/null and b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_SettingsCreate.png differ 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 0000000..2aa0733 Binary files /dev/null and b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_TasksRun.png differ 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 0000000..3161c91 Binary files /dev/null and b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_TasksValue.png differ 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 0000000..5d03a7a Binary files /dev/null and b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_Value.png differ 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 0000000..ac6fa61 Binary files /dev/null and b/Grasshopper/SAM.Core.Grasshopper.Multitasker/Resources/Icons/SAM_GH_ValueCreate.png differ diff --git a/design/grasshopper-icons/.gitignore b/design/grasshopper-icons/.gitignore new file mode 100644 index 0000000..990b07d --- /dev/null +++ b/design/grasshopper-icons/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +tests/GhIconTest/bin/ +tests/GhIconTest/obj/ diff --git a/design/grasshopper-icons/ICON_DESIGN_SYSTEM.md b/design/grasshopper-icons/ICON_DESIGN_SYSTEM.md new file mode 100644 index 0000000..83b5ea5 --- /dev/null +++ b/design/grasshopper-icons/ICON_DESIGN_SYSTEM.md @@ -0,0 +1,163 @@ +# SAM Grasshopper Icon Design System + +Status: **frozen v1** (2026-09-28). It builds on the approved exploratory direction ("Opus 5.5 proposal", 5 icons), +refined toward the SAM-BIM identity and scaled to the whole library. +`tools/icons.py` implements this file. If they disagree, fix whichever one is wrong. Don't hand-edit generated SVG/PNG. + +## 1. Grammar + +``` +icon = object glyph + SAM-green subject + operation badge + (what) (what is produced (what the component does) + or affected) +``` + +* **Object glyph**: one per SAM object family (§3). It sits in the top-left ~18×18 px and has a strong silhouette. +* **Subject**: the part the component produces, returns or changes is drawn in SAM green. Context is neutral grey/white. +* **Operation badge**: a 10 px disc, always at the bottom-right (centre 19,19). Its colour gives the operation *family*, its glyph the *verb* (§4). +* **Parameters** (`GH_PersistentParam`): the object glyph alone, all green, with no badge. A missing badge means "this is data, not an operation". + +A user learns roughly 40 object shapes and 7 badge colours, and can then read any SAM component. + +## 2. Palette (from https://sambim.xyz, inspected 2026-09-28) + +| Role | Hex | Source | +|---|---|---| +| Ink (outlines, dark glyphs) | `#111111` | site `--text` / `--line` | +| **SAM emblem green** (subject) | `#81BC44` | `assets/SAM_BIM_logo.png`, averaged tile pixels | +| Subject light / shade faces | `#ADD485` / `#659335` | derived from emblem green (35 % tint / 22 % shade) | +| SAM deep green | `#277A45` | site `--green` | +| Blue | `#1565C0` | site `--blue` | +| Yellow | `#F6C90E` | site `--yellow` | +| Red | `#D32F2F` | site `--red` | +| Muted | `#424242` | site `--muted` | +| Neutral faces | `#FFFFFF` / `#E3E3E3` / `#BDBDBD` | site `--bg` / derived / site border grey | + +Rules: +- Flat fills only: no gradients, glows or text. +- Every icon uses the emblem green for its subject. This is the SAM identity. +- The site's primary accents (blue, yellow, red, deep green, black) are used **only** in operation badges, plus two semantic exceptions: the sun is yellow, and the colour glyph uses the site's red/yellow/blue stripe. +- 3D glyphs use 3-tone flat shading (top / left / right), in green when the part is the subject and neutral when it's context. + +## 3. Object families + +| Family | Glyph | Motif | +|---|---|---| +| AnalyticalModel / BuildingModel | `model` | isometric house block (gable roof) | +| AdjacencyCluster | `cluster` | two joined rooms with a green shared-wall seam (the adjacency) | +| Case / scenario | `case` | two models (variants) | +| Space | `space` | isometric room cube | +| ExternalSpace | `externalSpace` | dashed ghost cube | +| Floor (area) | `floor` | cutaway room corner with a green floor (approved exploratory icon) | +| Zone | `zone` | green floor plate carrying two rooms | +| IZAM (inter-zone air) | `izam` | two rooms with an air arc between them | +| Panel | `panel` | upright wall plane | +| Aperture / opening | `aperture` | wall plane with a green window | +| OpeningProperties | `openingProperties` | window with an open green sash | +| Shade | `shade` | overhang plate on a wall | +| Normal | `normal` | wall with a normal arrow | +| Construction (+ Wall/Floor/Roof/HostPartitionType) | `construction` | layered wall block: neutral, green core, neutral | +| ConstructionLayer / MaterialLayer | `constructionLayer` / `materialLayer` | single layer slab (material layer is hatched) | +| ApertureConstruction / OpeningType | `apertureConstruction` | double-glazing unit, green glass | +| ConstructionManager | `constructionManager` | construction + glazing unit together | +| Material (opaque / transparent / gas) | `material` / `transparentMaterial` / `gasMaterial` | sample sphere: solid / clear / dotted | +| Profile | `profile` | step schedule on axes | +| InternalCondition, occupancy, NCM | `internalCondition` | person | +| DegreeOfActivity | `degreeOfActivity` | person with motion ticks | +| Lighting / equipment / infiltration / pollutant gains | `lighting` / `equipment` / `infiltration` / `pollutant` | bulb / monitor / wind / cloud with particles | +| Set-point, comfort, TM52, adaptive | `thermometer` | thermometer | +| Ventilation system | `fan` | fan | +| Airflow | `airflow` | three air arrows | +| AirHandlingUnit, equipment object | `ahu` | box with fan port | +| System / SystemType | `system` | duct unit | +| Heat transfer | `heat` | arrow through a wall | +| Daylight | `daylight` | sun rays onto a floor | +| Level | `level` | datum line with a level marker | +| Shell | `shell` | hexagonal prism (closed solid) | +| Face3D / PlanarBoundary3D | `face` | flat polygon plate | +| Geometry (generic) / Geometry2D | `geometry` / `geometry2D` | hexagon with vertices / flat L-polygon | +| Plane, Transform3D, Mesh, Polyloop, Segment, Points, Vector, Grid | same names | plane + normal, axis triad, fan mesh, loop, segment, 3 points, arrow, dotted floor | +| Section | `sectionBox` | solid split open, green cut face (approved exploratory icon) | +| Weather / WeatherData / WeatherYear / DesignDay | `weather` / `weatherData` / `weatherYear` / `designDay` | sun + cloud / sun + bars / calendar + sun / sun on day arc | +| Location / Address / direction | `location` / `address` / `compass` | map pin / pin + card / compass | +| Time / day | `clock` / `calendar` | clock / calendar | +| Result, DesignExplorer | `result` | bar chart | +| SAM object (generic) | `object` | the SAM emblem tile (green rounded square), no lettering | +| RelationCluster / Group / IndexedObjects / Type | `relation` / `group` / `indexed` / `type` | node graph / dashed group / indexed tiles / shape trio | +| Value / parameter, Name, Text, Number | `value` / `name` / `text` / `number` | tag / label / quotes / ruler | +| Table (DelimitedFileTable, Csv, TableModifier), TextMap | `table` / `textMap` | grid with a green header / two mapped columns | +| JSON, File, Folder, Log, List, DataTree | `json` / `file` / `folder` / `log` / `list` / `tree` | braces / page / folder / log page / bullets / tree | +| Colour | `colour` | the site's red/yellow/blue squares | +| Settings (merge settings, combine type) | `settings` | sliders | +| Matrix | `matrix` | bracketed dot grid | +| About / Version | `info` | emblem tile with "i" (identity exception) | + +## 4. Operation families (badges) + +| Family | Disc | Verbs → glyph | +|---|---|---| +| **Construct** | blue `#1565C0`, white glyph | Create `+` · Add `+` · Copy/Duplicate ⧉ | +| **Query** | deep green `#277A45`, white glyph | Get ↗ (out) · Filter/Select funnel · Inspect magnifier · Report/List ≡ · Display/Label eye | +| **Change** | yellow `#F6C90E`, ink glyph | Set ↙ (in) · Modify/Fix/Adjust/Rename Δ · Update/Map/Replace ↻ · Merge/Join/Combine ⋎ · Split/Cut/Trim ⋏ · Section ⊟ · Union ∪ · Intersect ∩ · Difference ⌐ · Offset ⧈ · Transform/Move ↔ · Snap •\| · Align · Extend →\| · Flip ⇅ · Triangulate · Sort | +| **Remove** | red `#D32F2F`, white glyph | Remove − · Error ! | +| **Evaluate** | ink `#111111`, white glyph | Calculate `=` · Sum `+` · Multiply × · Divide ÷ · Analyse bars · Validate/Check/Is ✓ | +| **Transfer** | white, ink ring and glyph | Convert/To ⇄ · Import/Read/Load/From ↓ · Export/Write/Save/Bake/To-file ↑ | +| **Value** | white, ink ring and glyph | enum value lists (`GH_SAMEnumComponent`) ≡ | + +The badge carries shape **and** colour, so it stays readable with colour-blindness and in greyscale. Mirrored pairs are intentional: Get ↗ out / Set ↙ in, Merge ⋎ / Split ⋏, Union ∪ / Intersect ∩, Import ↓ / Export ↑. + +## 5. Modifiers (object-layer composition) + +| Modifier | Drawing | Used for | +|---|---|---| +| plural | two instances (0.78 scale, fixed offsets) | `GetSpaces` vs `GetSpaceByName`, `…Panels` vs `…Panel` | +| library | two cards behind the object (0.74) | `*Library` objects, default libraries | +| add | neutral existing instance + green new instance | every `Add…` (plurality not drawn) | +| library + add | green item going into a neutral library | `AddMaterials`, `AddMissingProfiles` | +| split effect | glyph cut diagonally and pulled apart | Split/Cut/Trim | +| intersect effect | two neutral instances, overlap in green | Intersection | +| difference effect | green instance with a dashed white cutter | Difference | + +Outline weight stays 1.1 px under every scale: `scaled()` compensates the stroke width. + +## 6. Construction rules + +1. Canvas 24×24, one outline weight (1.1 px ink), round joins. +2. Isometric glyphs use 2:1 pixel isometry, so every solid shares one camera. +3. The bottom-right quadrant (the badge disc, r = 5 with a white ring) is reserved. Glyphs may pass under it, but can't rely on anything there. +4. Minimal detail: no element under ~1.5 px, and no text inside icons. +5. On light Grasshopper bodies the ink outline carries the silhouette. On dark bodies the light/green fills carry it. The white badge ring separates the badge on both. +6. Qualifiers (`…By`) 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 0000000..5ecc83b Binary files /dev/null and b/design/grasshopper-icons/png/24/result.png differ diff --git a/design/grasshopper-icons/png/24/script.png b/design/grasshopper-icons/png/24/script.png new file mode 100644 index 0000000..0234a72 Binary files /dev/null and b/design/grasshopper-icons/png/24/script.png differ diff --git a/design/grasshopper-icons/png/24/script_library_get.png b/design/grasshopper-icons/png/24/script_library_get.png new file mode 100644 index 0000000..1b8a7d9 Binary files /dev/null and b/design/grasshopper-icons/png/24/script_library_get.png differ diff --git a/design/grasshopper-icons/png/24/settings.png b/design/grasshopper-icons/png/24/settings.png new file mode 100644 index 0000000..45843a9 Binary files /dev/null and b/design/grasshopper-icons/png/24/settings.png differ 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 0000000..1cab885 Binary files /dev/null and b/design/grasshopper-icons/png/24/settings_create.png differ 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 0000000..2aa0733 Binary files /dev/null and b/design/grasshopper-icons/png/24/tasks_run.png differ 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 0000000..3161c91 Binary files /dev/null and b/design/grasshopper-icons/png/24/tasks_value.png differ diff --git a/design/grasshopper-icons/png/24/value.png b/design/grasshopper-icons/png/24/value.png new file mode 100644 index 0000000..5d03a7a Binary files /dev/null and b/design/grasshopper-icons/png/24/value.png differ 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 0000000..ac6fa61 Binary files /dev/null and b/design/grasshopper-icons/png/24/value_create.png differ diff --git a/design/grasshopper-icons/review/REVIEW.md b/design/grasshopper-icons/review/REVIEW.md new file mode 100644 index 0000000..aaf774f --- /dev/null +++ b/design/grasshopper-icons/review/REVIEW.md @@ -0,0 +1,7 @@ +# SAM_Multitasker - icon review + +- Objects: **9** (5 components, 4 params) +- Distinct icons: **9** (5 on SAM glyphs, 4 on SAM-BIM extension glyphs) +- Identical-pixel groups (different icon ids): **0** +- Icons shared by several objects (intentional: qualifier variants / same noun+verb): **0** + diff --git a/design/grasshopper-icons/review/contact_sheet.html b/design/grasshopper-icons/review/contact_sheet.html new file mode 100644 index 0000000..f0398cd --- /dev/null +++ b/design/grasshopper-icons/review/contact_sheet.html @@ -0,0 +1,12 @@ +SAM_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 0000000..6e09c76 Binary files /dev/null and b/design/grasshopper-icons/review/contact_sheet.png differ 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..31233ac --- /dev/null +++ b/design/grasshopper-icons/tests/GhIconTest/IconTests.cs @@ -0,0 +1,101 @@ +// 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; +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..7a2fdb4 --- /dev/null +++ b/design/grasshopper-icons/tools/check_source.py @@ -0,0 +1,113 @@ +"""Guard rails for the icon redesign diff. + + 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 +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 +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\("') + + +def git(*args): + return subprocess.run(["git", "-C", REPO, *args], check=True, capture_output=True, text=True, encoding="utf-8").stdout + + +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, headers = 0, [], 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 = [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 + 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}; SPDX headers added: {headers}; " + f"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() diff --git a/docs/GH-IconRedesign.md b/docs/GH-IconRedesign.md new file mode 100644 index 0000000..895f86f --- /dev/null +++ b/docs/GH-IconRedesign.md @@ -0,0 +1,61 @@ +# 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. +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. + +## 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.