I’ve been using Grasshopper3D for commercial work long enough to develop some practical habits. I want to share one of them. Sometimes questions come with diligent but slightly overcomplicated definitions as examples – asking how to build a pattern or a truss. This usually involves geometry that is based on one or more vector surfaces. I wanted to share some less than obvious information that might help some people (beginners) with their projects.

With plugins like Lunchbox and Somnium its really easy to get surface breakups – hex, diamond etc. The natural thing is to assume that further extension of the definition would be done on top of that geometry. That is true in some cases – but in practice its much easier to control your geometry if you use a 2D projection and map it onto your complex target surface. This method also helps with managing complexity as definition grows.

You can always reference your 2D to make sense of the 3D. You would use your projection to set up complex relationships and rigs, then project them to your target surface. Working with attractor points and curves is also much easier in 2D. Imagine trying to wrap a sphere with a curve, or move a point around a sphere. Pain in the ass. Lets look at an easier way.

If you want to follow along, you’ll need **Rhino**, **Grasshopper3D** and **Lunchbox**, the definitions for each step are **here**.

There are two specific components that come in useful. One is **Evaluate Surface** and another is **Map To Surface**. With **Evaluate Surface** we get both point position within the UV of the surface and a vector/normal perpendicular to the surface. Vector is very useful in getting the right direction for all of your lofts and extrusions based on your design.

**Step 1:**

Initial definition is fairly straight forward.

I am creating a basic flat square with my reusable cluster – then using that as a surface that feeds to the Lunchbox’s **Hexagonal Cells**.

Notice the highlighted purple components – **Remap Numbers** and **Evaluate Surface**. We start with a base rectangle of the domain -20.0 to 20.0 and then we map center points of each cell to the more complex surface above it. Our reparametarized surface has coordinate range from 0.0 to 1.0. To scale the center points over we remap each point to that coordinate space using **Remap Numbers**. Notice what happens to the numbers in the yellow boxes.

We get the following result. Centers mapped to the surface pointing outward in their individual directions.

For vectors we’ll keep it simple and just use the center points of our hexagons to build the geometry. If we were doing a more precise definition on curved surfaces we would use every point of the underlying 2D geometry. But for this exercize just center points will do. We can use **Polygon Center** to get the centers of polygons or simply feed points to **Average** component, but Lunchbox already supplies center points – so lets use those. We then evaluate each center point to the target surface – notice that *we re-parametrize the target surface* to reset everything to 0.0 to 1.0 range. **Remap Numbers** scales our domain of points to new coordinates. I drew arbitrary lines to show the relationship between reference and target geometry.

**Step 2:**

Lets use **Map to Surface** component – which takes curves we want to map, a reference surface (our flat rectangle) and target surface (curved surface) to translate (in essence project locally) the hexagons. The only change below is addition of **Map to Surface**.

Now we have a base to build fun stuff on. I won’t go over attractors in this article, but feel free to examine the custom cluster I use – its fairly straight forward. Lets add a curve positioned on the Z plane.

The curve will be used to extract values and scale all our hexagons based on their proximity to it. I am using my reusable attractor cluster, its fairly basic if you want to check inside, but you don’t have to. The newly added components are in purple.

The result is two sets of hexagons mapped to our target surface.

**Step 3:**

Now that there is a solid base to build our design on we can finish it up with some real geometry. We have two sets of curves mapped to a surface, and we have vectors perpendicular to the surface at the center of each cell. Lets **Loft** between them, and add another attractor to move the inner hexagons out using the vectors we mapped.

Resulting geometry is a basic rendition of scaled hexagons that follow the curvature of an attractor curve.

You can download definition and rhino files **here**.

Someone requested a fiddle, so here it is, full code included below.

function wrapCanvasText(t, canvas, maxW, maxH, justify) { if (typeof maxH === "undefined") { maxH = 0; } var words = t.text.split(" "); var formatted = ''; // This works only with monospace fonts justify = justify || 'left'; // clear newlines var sansBreaks = t.text.replace(/(\r\n|\n|\r)/gm, ""); // calc line height var lineHeight = new fabric.Text(sansBreaks, { fontFamily: t.fontFamily, fontSize: t.fontSize }).height; // adjust for vertical offset var maxHAdjusted = maxH > 0 ? maxH - lineHeight : 0; var context = canvas.getContext("2d"); context.font = t.fontSize + "px " + t.fontFamily; var currentLine = ''; var breakLineCount = 0; n = 0; while (n < words.length) { var isNewLine = currentLine == ""; var testOverlap = currentLine + ' ' + words[n]; // are we over width? var w = context.measureText(testOverlap).width; if (w < maxW) { // if not, keep adding words if (currentLine != '') currentLine += ' '; currentLine += words[n]; // formatted += words[n] + ' '; } else { // if this hits, we got a word that need to be hypenated if (isNewLine) { var wordOverlap = ""; // test word length until its over maxW for (var i = 0; i < words[n].length; ++i) { wordOverlap += words[n].charAt(i); var withHypeh = wordOverlap + "-"; if (context.measureText(withHypeh).width >= maxW) { // add hyphen when splitting a word withHypeh = wordOverlap.substr(0, wordOverlap.length - 2) + "-"; // update current word with remainder words[n] = words[n].substr(wordOverlap.length - 1, words[n].length); formatted += withHypeh; // add hypenated word break; } } } while (justify == 'right' && context.measureText(' ' + currentLine).width < maxW) currentLine = ' ' + currentLine; while (justify == 'center' && context.measureText(' ' + currentLine + ' ').width < maxW) currentLine = ' ' + currentLine + ' '; formatted += currentLine + '\n'; breakLineCount++; currentLine = ""; continue; // restart cycle } if (maxHAdjusted > 0 && (breakLineCount * lineHeight) > maxHAdjusted) { // add ... at the end indicating text was cutoff formatted = formatted.substr(0, formatted.length - 3) + "...\n"; currentLine = ""; break; } n++; } if (currentLine != '') { while (justify == 'right' && context.measureText(' ' + currentLine).width < maxW) currentLine = ' ' + currentLine; while (justify == 'center' && context.measureText(' ' + currentLine + ' ').width < maxW) currentLine = ' ' + currentLine + ' '; formatted += currentLine + '\n'; breakLineCount++; currentLine = ""; } // get rid of empy newline at the end formatted = formatted.substr(0, formatted.length - 1); var ret = new fabric.Text(formatted, { // return new text-wrapped text obj left: t.left, top: t.top, fill: t.fill, fontFamily: t.fontFamily, fontSize: t.fontSize, originX: t.originX, originY: t.originY, angle: t.angle, }); return ret; }

]]>