Single line continuous Art

I’ve made a little component that can help you achieve what you’re looking for:

SingleLineArt.gha (11 KB) (8.0 KB)
Maryam.jpg (43.0 KB)

Sourse Code:

namespace SingleLineArt
    public class SingleLineArtGhc : GH_Component
        private GH_MemoryBitmap _bmpMemory;
        private Transform _scale;
        private Transform _orient;
        private Rectangle3d _rec;
        private Point3dList _pts;
        public SingleLineArtGhc()
          : base("SingleLineArt", "SingleLineArt", "SingleLineArt", "Extra", "SingleLineArt") { }
        protected override void RegisterInputParams(GH_InputParamManager pManager)
            pManager.AddBooleanParameter("Reset", "Reset", "Reset", GH_ParamAccess.item);
            var paramFilePath = new Param_FilePath
                FileFilter = "All image files|*.bmp;*.jpg;*.jpeg;*.png;*.tif;*.tiff",
                ExpireOnFileEvent = true
            pManager.AddParameter(paramFilePath, "File", "File", "Location of image file", GH_ParamAccess.item);
            pManager.AddRectangleParameter("Rectangle", "Rectangle", "Rectangle", GH_ParamAccess.item);
            pManager.AddCurveParameter("Curve", "Curve", "Starting Curve", GH_ParamAccess.item);
            pManager.AddIntegerParameter("Start", "Start", "Number of starting circles", GH_ParamAccess.item);
            pManager.AddIntegerParameter("End", "End", "Maximum number of circles", GH_ParamAccess.item);
            pManager.AddNumberParameter("Min", "Min", "Smallest Circle", GH_ParamAccess.item);
            pManager.AddNumberParameter("Max", "Max", "Largest Circle", GH_ParamAccess.item);
            pManager[0].Optional = true;
        protected override void RegisterOutputParams(GH_OutputParamManager pManager)
            pManager.AddPointParameter("Points", "Points", "Points", GH_ParamAccess.list);
            pManager.AddNumberParameter("Radiuses", "Radiuses", "Radiuses", GH_ParamAccess.list);
        protected override void SolveInstance(IGH_DataAccess DA)
            var reset = false;
            DA.GetData(0, ref reset);
            if (reset || _pts is null)
                string path = null;
                if (!DA.GetData(1, ref path) || !File.Exists(path))
                    AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Image file does not exist");

                DA.GetData(2, ref _rec);
                var bmp = new Bitmap(path);
                var imgRec = new Rectangle3d(Plane.WorldXY, bmp.Width, bmp.Height);

                _bmpMemory = new GH_MemoryBitmap(bmp, WrapMode.Clamp);

                var plane1 = _rec.Plane;
                var plane2 = imgRec.Plane;
                plane1.Origin = _rec.Center;
                plane2.Origin = imgRec.Center;
                _scale = Transform.Scale(plane1, imgRec.X.Length / _rec.X.Length, -imgRec.Y.Length / _rec.Y.Length, 1.0);
                _orient = Transform.PlaneToPlane(plane1, plane2);
                Curve curve = null;
                DA.GetData(3, ref curve);
                var start = 0;
                DA.GetData(4, ref start);
                curve.DivideByCount(start, true, out var pts);
                _pts = new Point3dList(pts);
            var rads = new double[_pts.Count];
            var min = double.NaN;
            var max = double.NaN;
            DA.GetData(6, ref min);
            DA.GetData(7, ref max);
            Parallel.For(0, _pts.Count, i =>
                var pt = _pts[i];
                var red = Convert.ToDouble(_bmpMemory.R(Convert.ToInt32(pt.X), Convert.ToInt32(pt.Y))) / 255;
                rads[i] = red * (max - min) + min;
            var counts = new int[_pts.Count];
            var vecs = new Vector3d[_pts.Count];
            var rTree = RTree.CreateFromPointArray(_pts);
            var r = rads.Max() * 2;
            var indices = new List<int>[_pts.Count];
            Parallel.For(0, _pts.Count, i =>
                indices[i] = new List<int>();
                rTree.Search(new Sphere(_pts[i], r), (sender, args) =>
                    if (args.Id > i) indices[i].Add(args.Id);
            Parallel.For(0, _pts.Count, (i) =>
                foreach (var j in indices[i])
                    var vector = _pts[i] - _pts[j];
                    var d2 = vector.SquareLength;
                    if (d2 > Math.Pow(rads[i] + rads[j], 2)) continue;
                    vector *= (rads[i] + rads[j] - Math.Sqrt(d2)) * 0.5;
                    vecs[i] += vector;
                    vecs[j] -= vector;
            Parallel.For(0, _pts.Count, i =>
                if (counts[i] == 0) return;
                vecs[i] /= counts[i];
                if (Math.Min(_pts[i].X, _rec.Width - _pts[i].X) < rads[i]) vecs[i].X = 0;
                if (Math.Min(_pts[i].Y, _rec.Height - _pts[i].Y) < rads[i]) vecs[i].Y = 0;
                _pts[i] += vecs[i];
            DA.SetDataList(0, _pts);
            DA.SetDataList(1, rads);
            var end = 0;
            DA.GetData(5, ref end);
            if (_pts.Count > end) return;
            var newPts = new List<Point3d>();
            var newIndices = new List<int>();
            for (var i = 0; i < _pts.Count - 1; i++)
                var vec = _pts[i] - _pts[i + 1];
                if (vec.SquareLength < Math.Pow((rads[i] + rads[i + 1]), 2)) continue;
                newIndices.Add(i + 1 + newIndices.Count);
                newPts.Add((_pts[i] + _pts[i + 1]) * 0.5);
            for (var i = 0; i < newIndices.Count; i++)
                _pts.Insert(newIndices[i], newPts[i]);
        protected override Bitmap Icon => null;
        public override Guid ComponentGuid => new Guid("83b6bf0b-0312-4184-935c-b48d952f65bd");
} (33.0 KB)


Hello mister wizard Mahdiyar,
Is it possible at all that you can tweak the component to fit a not updated version of rhino6?
Cant run the component because of it being built on sdk 7.3… (?)
Would love to use the component for a picture of a special person…

Thanks so much!

@Mahdiyar thanks for sharing the code. I begin to study it and it is very interesting. I will surely try to make it in 3d applied on a mesh. Did you try in 3D?


I manage to make it on 3d with mesh. Each vertex of the mesh handle a radius. So it could be possible to put an image through the Import Image. The borders are handled through extra points that doesn’t move. Still work to do.

One layer

2 layers


Fun (continuous) thread all :slight_smile:

Kangaroo’s ImageCircles component takes a coloured mesh as input, which can be something 3 dimensional, so can be used for this: (116.8 KB)

This one starts with all the points existing from the beginning, and gradually increases the radius multiplier. More efficient is to start with only a few points and divide curves when they get above a certain length, but that currently needs to be scripted like shown in this example.
I can make another example showing how to combine the image based sizing with this iterative line splitting if it would be of interest.

In a different direction, I also played a little recently with another way of making shaded images from a continuous line, but by adjusting tension in a grid-

I’ll post the script for this one too soon.


Daniel, you have always tons of new really cool stuff. It is funny to see that there are more viewers/likers on Tweeter than on this forum.

I continued to play with Differential growth using multiple closed curves. It could be used to make single skinned or double skinned things.


@Mahdiyar could you please have a look?
Thanks much!

Take an evaluation version of Rhino 7, it will works for 90 days if I am right!

Very interesting approach, adjusting tension in a grid, interested to see the script, when you have time. Great work.

Here’s the script for the brightness based tension one (16.9 KB)

It takes a list of lines, which don’t have to form a square grid. It might be interesting to try starting from radial or spiralling arrangements too.

These type of effects really depend a lot on having a nicely lit source image with good contrast.

The source image used above is this portrait of Giacometti by Irving Penn from here.


Is this error because I am not on the latest service release candidate? I have plankton but not kplankton…


1 Like

You might need to change the referenced assembly location (it needs to be the same one being loaded by the version of Rhino you are running).

1 Like

I understand what you mean but I don’t know how to do it.

Excellent work! Thank you

Right click on C# component > Manage Assemblies… > Add > Search KangarooSolver.dll , I have it in C:\Program Files\Rhino 6\Plug-ins\Grasshopper\Components and its version is 2.5.2.


Thanks! When I opened the gh file it asked me to point to where the KangarooSolver.Dll was. I thought this should sort out the C# too?

It should do. Depending on which Rhino you are running, the directory will usually be one of-

C:\Program Files\Rhino 6\Plug-ins\Grasshopper\Components

C:\Program Files\Rhino WIP\Plug-ins\Grasshopper\Components
(if you first installed v7 when it was a WIP)

C:\Program Files\Rhino 7\Plug-ins\Grasshopper\Components

C:\Program Files\Rhino 8 WIP\Plug-ins\Grasshopper\Components

If you have multiple versions of Rhino installed, you need to reference the dll from the appropriate one.

Ah, so I have probably referenced the R5 one instead of the R7 one!

I can’t do anything from right clicking and managing assemblies…

Any ideas how to change the kangaroo solver from the R5 one to the R7 one?

Ah… deleted the old one and added a new one! :smiley:

1 Like

Yes, I agree the interface here is a little confusing. The only way to delete a reference is by pressing the delete key on the keyboard - there’s no button or menu item to do it with the mouse.

1 Like

Wonderful stuff, Daniel!

I’m always looking for ways to capture images with a single continuous path. This interest stems from my work with sand-plotters which have no pen-up facility. A long sought challenge has been capturing facial images on sand. Here is an attempt from many years ago, where I created a brightness height-map in Blender from an image of my daughter’s face, and then used the resulting Z values to instead perturb respective vertices in the Y direction. Finally, I converted the 2D vertex array to a continuous boustrophedon path. Starting image:

Resulting plot (2.5m diameter field):

Though the likeness is discernable, it is very lighting-specific – it only “works” when lighting comes from the top of the face (as it was when the photo was taken). Here, the predominant source comes in through the nearby windows.

My tweak of your definition yielded: (23.9 KB)

An early try on sand (685mm field, 6mm ball, circumferential LED lighting):

Despite the center artifact (due to homing registration error), I see potential. Probably the greatest limitation (of many) is my “pen” and the “lines” it leaves in its wake – the ball creates a darker furrow with lighter dunes on either side.

I find your suggestion of starting from radial or spiraling arrangements particularly intriguing, since my sand plotters utilize a polar mechanism and therefore cover a circular canvas. I’d be very interested in attempting this if you or others can point the way.