Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c95bb33
update dev from main (#191)
marc-romu Apr 29, 2025
26316da
feat: support for script components in ghget
marc-romu Apr 29, 2025
498942a
refactor: reorganization of json models for clearer structure
marc-romu Apr 29, 2025
699e56b
refactor(aitools): migrated gh_put tool from GhPut component to AiToo…
marc-romu Apr 29, 2025
203e2de
feat(ghjson): fix invalid instanceGuid when deserializing a ghjson
marc-romu May 1, 2025
0d07ca9
fix(dependencygraph): components placed with no predefined pivot poin…
marc-romu May 1, 2025
96dfa1b
fix(dependencygraph): source components are now properly sorted acco…
marc-romu May 1, 2025
c6e2930
feat(dependencygraph): allow for createcomponentgrid for fractional r…
marc-romu May 1, 2025
8ab16aa
fix(ghget): limit ghget connections to components being processed
marc-romu May 1, 2025
ef64c72
fix(dependencygraph): wrong detection of default pivot position
marc-romu May 1, 2025
e653209
fix(dependencygraph): when pivots are provided recalculate positions …
marc-romu May 1, 2025
2dcbd1b
fix(dependencygraph): optimized positioning of components and parameters
marc-romu May 1, 2025
36506e3
feat(ghtidyup): new ghtidy up component to reorganize components
marc-romu May 1, 2025
d493ab4
feat(dependencygraph): new parameter to force calculation of componen…
marc-romu May 1, 2025
cbddc47
feat(componentbase): new component base for component that need the s…
marc-romu May 1, 2025
868bfe2
fix(ghtidyup): constantly moving component on each tidy up
marc-romu May 1, 2025
b75be78
fix(dependencygraph): incorrect last objects' position
marc-romu May 1, 2025
36979a3
fix(moveinstance): skip movement if initial and target positions are …
marc-romu May 2, 2025
f9c6df4
feat(ghjson): new analyzer and fixer methods to ensure consistency of…
marc-romu May 2, 2025
99447bd
docs
marc-romu May 2, 2025
6e58de5
feat(dependencygraph): improved component grid by detecting islands o…
marc-romu May 2, 2025
81ffb64
fix(ghtidyup): significant improvement of the organization algorithm
marc-romu May 3, 2025
38fc9d4
fix(ghtidyup): significant improvement of the organization algorithm
marc-romu May 3, 2025
f4fe89a
feat(ghtidyup): connection length minimization algorithm
marc-romu May 3, 2025
fde2f4f
docs: update changelog with 1 closed issues
actions-user May 3, 2025
9dc69a0
feat(ghtidyup): several improvements in the tidy up functionality and…
marc-romu May 3, 2025
0b229fa
docs: update version badge for dev
actions-user May 3, 2025
d03d28e
docs: update version badge for dev to 0.3.1-dev.250503 (#194)
marc-romu May 3, 2025
6eea98a
Update dev from main (#195)
marc-romu May 6, 2025
bce2c6a
chore: prepare release 0.3.1-alpha with version update and code style…
actions-user May 6, 2025
74f7e2e
chore: prepare release 0.3.1-alpha with version update and code style…
marc-romu May 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/labels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@
- name: "component: GhPut"
color: "000"
description: "Issues related to the Grasshopper Put Components component"
- name: "component: GhRetrieveComponents"
color: "000"
description: "Issues related to the Grasshopper Retrieve Components component"
- name: "component: GhTidyUp"
color: "000"
description: "Issues related to the Grasshopper Tidy Up component"
- name: "component: AI GhGenerate"
color: "000"
description: "Issues related to the AI Grasshopper Generate Definitions component"
Expand Down
37 changes: 34 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.1-alpha] - 2025-05-06

### Added

- Added the "Accepted feature request: Allow for copy-paste the chat in a good format when selecting the text" ([#86](https://github.com/architects-toolkit/SmartHopper/issues/86)).
- Added support for script components in `GhGet`.
- Allow `CreateComponentGrid` for fractional row positions for components, to create a more human-like layout.
- Added `gh_tidy_up` AI tool in `GhObjTools` for arranging selected components into a dependency-based grid layout.
- New `gh_tidy_up` component.
- New `SelectingComponentBase` for components that need the button to select other components.
- New `GHJsonAnalyzer` and `GHJsonFixer` classes for analyzing and fixing GHJSON formats.

### Changed

- Improved chat UI with timestamps for messages, collapsible tool messages, inline metrics per message, button to copy codeblocks to clipboard, and better formatting.
- Reorganization of JSON models for clearer structure.
- Migrated the `GhPut` tool from the `GhPutComponent` to the `GhPutTools` class, using the `AIToolManager`.
- `DeserializeJSON` now fixes invalid InstanceGuids in Grasshopper JSON documents when deserializing.
- Moved `DependencyGraphUtils` and `ConnectionGraphUtils` from `SmartHopper.Core.Graph` to `SmartHopper.Core.Grasshopper.Graph`.
- Improved `CreateComponentGrid` in `DependencyGraphUtils`:
- Now returns original pivots relative to the most top-left component to ensure relative positioning
- Uses a more human-like layout with column widths based on actual component widths
- Uses horizontal margin of 50 and vertical spacing of 80
- Centers Params from their actual center instead of the top-left position
- Improved by detecting islands of components, ensure connected components stay together, and use barycenter heuristic algorithm for initial layer ordering
- Minimizes connection length
- Aligns parents with children
- Improved `MoveInstance`:
- Added a nice animation so that components move smoothly to their new position.
- Skip movement if initial and target positions are the same.
- Modified `GhGetComponents` to use the new `SelectingComponentBase`.
- Implemented the new `GHJsonAnalyzer` and `GHJsonFixer` in `GhPutTools` and `GhPutComponents`.

### Fixed

- Fixed issue with tool calls in chat messages. Now the code provides exactly the json structure expected by MistralAI and OpenAI.
- Fixed tooltip visibility at the bottom of the chat.
- Fixed component placement in `GhPut` tool was too separated.
- Fixed source components in `TopologicalSort` were not sorted in reverse order.
- Limited `ghget` connections to components within the result objects set.
- Fixed `gh_tidy_up` moving components on every execution.
- Fixed `CreateComponentGrid` joining last and last-1 column together.
- (automatically added) Fixes "Panels and params position is calculated from top-left, not from center" ([#184](https://github.com/architects-toolkit/SmartHopper/issues/184)).

## [0.3.0-alpha] - 2025-04-27

Expand All @@ -30,9 +61,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added `ConnectionGraphUtils` class in `SmartHopper.Core.Graph` namespace with method `ExpandByDepth` to expand a set of component IDs by following connections up to the given depth.
- Added `GhRetrieveComponents` component and `ghretrievecomponents` AI tool for listing Grasshopper component types with descriptions, keywords, category filters, and list of inputs and outputs.
- Added `ghcategories` AI tool in `GhTools` to list Grasshopper component categories and subcategories with optional soft string filter.
- Added new `ghtogglepreview` AI tool in `GhObjTools` for toggling Grasshopper component preview by GUID.
- Added new `ghtogglelock` AI tool in `GhObjTools` for toggling Grasshopper component lock state by GUID.
- Added new `ghmoveobj` AI tool in `GhObjTools` for moving Grasshopper component pivot by GUID with absolute or relative position.
- Added new `gh_toggle_preview` AI tool in `GhObjTools` for toggling Grasshopper component preview by GUID.
- Added new `gh_toggle_lock` AI tool in `GhObjTools` for toggling Grasshopper component lock state by GUID.
- Added new `gh_move_obj` AI tool in `GhObjTools` for moving Grasshopper component pivot by GUID with absolute or relative position.
- Added `MoveInstance` method in `GHCanvasUtils` to move existing instances by GUID with absolute or relative pivot positions.
- Improved security in Providers by accepting only signed assemblies.
- Added multiple CI Tests, for example, to ensure unsigned provider assemblies are rejected by `ProviderManager.VerifySignature`, to ensure only signed assemblies are loaded by `ProviderManager.LoadProviderAssembly`, and to ensure only enabled providers are registered by `ProviderManager.RegisterProviders`.
Expand Down
9 changes: 5 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,9 @@ Follow these steps to configure Visual Studio 2022 for SmartHopper development:
4. In **Solution Explorer**, right-click the solution and select **Restore NuGet Packages**.
5. Verify that all projects target **.NET 7** and that Rhino/Grasshopper SDK references resolve.

### Initializing Code Signing (required before the first build)
### Initializing Code Signing

When developing locally, you must generate and apply both strong-name and Authenticode signatures:
When developing locally, you must generate and apply both strong-name and Authenticode signatures.

1. Open **Developer PowerShell for Visual Studio 2022** as Administrator.
2. cd to the repository root.
Expand All @@ -169,6 +169,7 @@ When developing locally, you must generate and apply both strong-name and Authen
```
6. Authenticode-sign provider DLLs (e.g. for Grasshopper testing):
```powershell
.\Sign-Authenticode.ps1 -Base64 '<Base64Pfx>' -Password '<password>' -Sign bin\Debug\net7.0-windows
.\Sign-Authenticode.ps1 -Sign bin\Debug\net7.0-windows -Password '<password>'
```
7. Open the `bin\Debug\net7.0-windows` folder in Grasshopper to load the signed plugin.

**Note:** Repeat steps 1, 2, and 6 after every build to ensure your providers are signed and SmartHopper can load them.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SmartHopper - AI-Powered Grasshopper3D Plugin

[![Version](https://img.shields.io/badge/version-0%2E3%2E1--dev%2E250429-brown)](https://github.com/architects-toolkit/SmartHopper/releases)
[![Status](https://img.shields.io/badge/status-Unstable%20Development-brown)](https://github.com/architects-toolkit/SmartHopper/releases)
[![Version](https://img.shields.io/badge/version-0%2E3%2E1--alpha-orange)](https://github.com/architects-toolkit/SmartHopper/releases)
[![Status](https://img.shields.io/badge/status-Alpha-orange)](https://github.com/architects-toolkit/SmartHopper/releases)
[![Test results](https://img.shields.io/github/actions/workflow/status/architects-toolkit/SmartHopper/.github/workflows/ci-dotnet-tests.yml?label=.NET%20CI&logo=dotnet)](https://github.com/architects-toolkit/SmartHopper/actions/workflows/ci-dotnet-tests.yml)
[![Grasshopper](https://img.shields.io/badge/plugin_for-Grasshopper3D-darkgreen?logo=rhinoceros)](https://www.rhino3d.com/)
[![MistralAI](https://img.shields.io/badge/AI--powered-MistralAI-orange?logo=mistralai)](https://mistral.ai/)
Expand Down Expand Up @@ -40,6 +40,8 @@ SmartHopper is not yet available through Food4Rhino. We will be releasing it soo
|-----------|:-------:|:-----------:|:-------:|:------------------------:|
| Grasshopper Get Components (GhGet)<br><sub>Read the current Grasshopper file and convert it to GhJSON format. Optionally filter them by runtime messages: errors, warnings, remarks</sub> | ⚪ | 🟡 | 🟠 | 🟢 |
| Grasshopper Put Components (GhPut)<br><sub>Place components on the canvas from a GhJSON format</sub> | ⚪ | 🟡 | 🟠 | 🟢 |
| Grasshopper Retrieve Components (GhRetrieveComponents)<br><sub>Retrieve all available Grasshopper components in your environment as JSON with optional category filter.</sub> | ⚪ | 🟡 | 🟠 | 🟢 |
| Grasshopper Tidy Up (GhTidyUp)<br><sub>Reorganize selected components into a clear, dependency-based grid.</sub> | ⚪ | 🟡 | 🟠 | 🟢 |
| AI Grasshopper Generate Definitions (AIGhGenerate)<br><sub>Automatically generate Grasshopper definitions using AI</sub> | ⚪ | - | - | - |
| AI Text Evaluate (AiTextEvaluate)<br><sub>Return a boolean from a text content using AI-powered checks</sub> | ⚪ | 🟡 | 🟠 | 🟢 |
| AI Text Generate (AiTextGenerate)<br><sub>Generate text content using AI language models</sub> | ⚪ | 🟡 | 🟠 | 🟢 |
Expand Down
2 changes: 1 addition & 1 deletion Solution.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<SolutionVersion>0.3.1-dev.250429</SolutionVersion>
<SolutionVersion>0.3.1-alpha</SolutionVersion>
</PropertyGroup>
</Project>
181 changes: 5 additions & 176 deletions src/SmartHopper.Components/Grasshopper/GhGetComponents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,24 @@
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using Grasshopper;
using Grasshopper.GUI;
using Grasshopper.GUI.Canvas;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Attributes;
using Grasshopper.Kernel.Types;
using Newtonsoft.Json.Linq;
using SmartHopper.Components.Properties;
using SmartHopper.Config.Managers;
using SmartHopper.Core.ComponentBase;

namespace SmartHopper.Components.Grasshopper
{
/// <summary>
/// Component that converts selected or all Grasshopper components to GhJSON format.
/// Supports optional filtering by runtime messages (errors, warnings, and remarks), component states (selected, enabled, disabled), preview capability (previewcapable, notpreviewcapable), preview state (previewon, previewoff), and classification by object type via Type filter (params, components, input, output, processing, isolated).
/// </summary>
public class GhGetComponents : GH_Component
public class GhGetComponents : SelectingComponentBase
{
private List<string> lastComponentNames = new List<string>();
private List<string> lastComponentGuids = new List<string>();
private string lastJsonOutput = "";
internal List<IGH_ActiveObject> selectedObjects = new List<IGH_ActiveObject>();
private bool inSelectionMode = false;

public GhGetComponents()
: base("Get Components", "GhGet",
Expand All @@ -44,58 +38,10 @@ public GhGetComponents()
{
}

public override void CreateAttributes()
{
m_attributes = new GhGetComponentsAttributes(this);
}

public override Guid ComponentGuid => new Guid("E7BB7C92-9565-584C-C1DD-425E77651FD8");

protected override Bitmap Icon => Resources.ghget;

public void EnableSelectionMode()
{
// Clear previous selection
selectedObjects.Clear();
inSelectionMode = true;

// Get the Grasshopper canvas
var canvas = Instances.ActiveCanvas;
if (canvas == null) return;

// Enable selection mode
canvas.ContextMenuStrip?.Hide();
Canvas_SelectionChanged();

// Force component update
ExpireSolution(true);
}

private void Canvas_SelectionChanged()
{
if (!inSelectionMode) return;

var canvas = Instances.ActiveCanvas;
if (canvas == null) return;

// Store selected objects
selectedObjects = canvas.Document.SelectedObjects()
.OfType<IGH_ActiveObject>()
.ToList();

// Update message with selection count
Message = $"{selectedObjects.Count} selected";

// Force component update
ExpireSolution(true);
}

protected override void AppendAdditionalComponentMenuItems(System.Windows.Forms.ToolStripDropDown menu)
{
base.AppendAdditionalComponentMenuItems(menu);
Menu_AppendItem(menu, "Select Components", (s, e) => EnableSelectionMode());
}

protected override void RegisterInputParams(GH_InputParamManager pManager)
{
pManager.AddTextParameter("Type filter", "T", "Optional list of classification tokens with include/exclude syntax: 'params', 'components', 'inputcomponents', 'outputcomponents', 'processingcomponents', 'isolatedcomponents'. Prefix '+' to include, '-' to exclude.", GH_ParamAccess.list, "");
Expand Down Expand Up @@ -157,12 +103,12 @@ protected override void SolveInstance(IGH_DataAccess DA)
["attrFilters"] = JArray.FromObject(filters),
["typeFilter"] = JArray.FromObject(typeFilters),
["connectionDepth"] = connectionDepth,
["guidFilter"] = JArray.FromObject(selectedObjects.Select(o => o.InstanceGuid.ToString())),
["guidFilter"] = JArray.FromObject(SelectedObjects.Select(o => o.InstanceGuid.ToString())),
};
var toolResult = AIToolManager.ExecuteTool("ghget", parameters, null).GetAwaiter().GetResult() as JObject;
var toolResult = AIToolManager.ExecuteTool("gh_get", parameters, null).GetAwaiter().GetResult() as JObject;
if (toolResult == null)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Tool 'ghget' did not return a valid result");
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Tool 'gh_get' did not return a valid result");
return;
}
var componentNames = toolResult["names"]?.ToObject<List<string>>() ?? new List<string>();
Expand All @@ -182,121 +128,4 @@ protected override void SolveInstance(IGH_DataAccess DA)
}
}
}

public class GhGetComponentsAttributes : GH_ComponentAttributes
{
private new readonly GhGetComponents Owner;
private Rectangle ButtonBounds;
private bool IsHovering;
private bool IsClicking;

public GhGetComponentsAttributes(GhGetComponents owner) : base(owner)
{
Owner = owner;
IsHovering = false;
IsClicking = false;
}

protected override void Layout()
{
base.Layout();

// Add space for the button at the bottom of the component
const int margin = 5;
var width = (int)Bounds.Width - (2 * margin); // Subtract margins from both sides
var height = 24;
var x = (int)Bounds.X + margin;
var y = (int)Bounds.Bottom;

ButtonBounds = new Rectangle(x, y, width, height);
Bounds = new RectangleF(Bounds.X, Bounds.Y, Bounds.Width, Bounds.Height + height + margin);
}

protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
{
base.Render(canvas, graphics, channel);

if (channel == GH_CanvasChannel.Objects)
{
var button = ButtonBounds;

// Draw button background with different states
var palette = IsClicking ? GH_Palette.White : (IsHovering ? GH_Palette.Grey : GH_Palette.Black);
var capsule = GH_Capsule.CreateCapsule(button, palette);
capsule.Render(graphics, Selected, Owner.Locked, false);
capsule.Dispose();

// Draw button text
var font = GH_FontServer.Standard;
var text = "Select";
var textSize = graphics.MeasureString(text, font);

// Use PointF for text position
var tx = button.X + (button.Width - textSize.Width) / 2;
var ty = button.Y + (button.Height - textSize.Height) / 2;
graphics.DrawString(text, font, IsHovering || IsClicking ? Brushes.Black : Brushes.White, new PointF(tx, ty));

// Draw rectangles around selected components when hovering
if (IsHovering && Owner.selectedObjects.Count > 0)
{
using (var pen = new Pen(Color.DodgerBlue, 2f))
{
pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
foreach (var obj in Owner.selectedObjects)
{
if (obj is IGH_DocumentObject docObj)
{
// Get current bounds of the component
var bounds = docObj.Attributes.Bounds;

// Add a small padding around the component
var padding = 4f;
var highlightBounds = RectangleF.Inflate(bounds, padding, padding);
graphics.DrawRectangle(pen, highlightBounds.X, highlightBounds.Y,
highlightBounds.Width, highlightBounds.Height);
}
}
}
}
}
}

public override GH_ObjectResponse RespondToMouseDown(GH_Canvas sender, GH_CanvasMouseEvent e)
{
if (e.Button == MouseButtons.Left)
{
if (ButtonBounds.Contains((int)e.CanvasLocation.X, (int)e.CanvasLocation.Y))
{
IsClicking = true;
Owner.ExpireSolution(true);
Owner.EnableSelectionMode();
return GH_ObjectResponse.Handled;
}
}
return base.RespondToMouseDown(sender, e);
}

public override GH_ObjectResponse RespondToMouseMove(GH_Canvas sender, GH_CanvasMouseEvent e)
{
bool wasHovering = IsHovering;
IsHovering = ButtonBounds.Contains((int)e.CanvasLocation.X, (int)e.CanvasLocation.Y);

if (wasHovering != IsHovering)
{
Owner.ExpireSolution(true);
}

return base.RespondToMouseMove(sender, e);
}

public override GH_ObjectResponse RespondToMouseUp(GH_Canvas sender, GH_CanvasMouseEvent e)
{
if (IsClicking)
{
IsClicking = false;
Owner.ExpireSolution(true);
}
return base.RespondToMouseUp(sender, e);
}
}
}
Loading
Loading